Click here to Skip to main content
15,395,065 members
Articles / Web Development / ASP.NET / ASP.NET Core
Article
Posted 22 Mar 2017

Stats

7.2K views
3 bookmarked

Building Custom Formatters for .NET Core (YAML Formatters)

Rate me:
Please Sign up or sign in to vote.
0.00/5 (No votes)
22 Mar 2017CPOL4 min read
Know how to create a custom request/response formatter for ASP.NET Core apps

Introduction

Quite recently, I got myself introduced to .NET Core’s formatters. In short, what formatters do is format your response and request in your preferred data formats. For example, Json formatters, if used would format your response (returned value from controller's action) and request (value passed as a parameter to the controller) in Json. Same goes for the XML and other formatters. .NET Core gives you some formatters out of the box. This official documentation described them briefly.

But let’s not talk too much about formatters rather see how we can make our own custom formatters. I think that’s what we are here for, right? Yeah! Let’s get started.

So, we have two abstract classes provided by the framework, InputFormmeter and OutputFormatter. Basically, you would want to use these classes to make your own formatters. But there are other two abstract classes that extend from those two formatters. TextInputFormatter and TextOuputFormatter can work with response that are simple string representations of data formats (data can be in the form of binary too). For example, Json and XML formatters extend these classes. We are going to build two Yaml formatters, one for input and the other one for output formatting.

Now the question is what is YAML? Here is the definition for it directly scraped out of Wikipedia,

YAML is a human-readable data serialization language. It is commonly used for configuration files, but could be used in many applications where data is being stored (e.g. debugging output) or transmitted (e.g. document headers). YAML targets many of the same communications applications as XML, but has taken a more minimal approach which intentionally breaks compatibility with SGML. YAML is a superset of JSON, another minimalist data serialization format where braces and brackets are used instead of indentation.

The idea is very simple. When using the Yaml output formatter, you would get the response (returned value of the controller's action) out of the current HttpContext and Serialize them into raw Yaml response text and send them back to the client. Pretty much same goes for the input formatter. In this case, you would Deserialize the Yaml content from the client's request and use them in a generic form. Another important thing is, you have to explicitly set media type header for these formatters. Doing that will activate these formatters whenever a client defines a Accept header (for output) and Content-Type (for input) with that specific media type format (application/x-yaml).

If you don’t want to use those headers while calling your controller’s actions, you can explicitly define the type of formatter that should be used while getting or posting content. For example, the [Produces(application/x-yaml)] will return the response in Yaml format whether you define a Accept header or not. Again, using the [Consumes(application/x-yaml)] attribute would only accept Yaml content whether you define the Content-Type or not.

‘Nuff history lessons. Here goes the input formatter for Yaml. By the way, I’m using the YamlDotNet library from Antoine Aubry (@antoineaubry) for Yaml’s serializing and desirializing process.

YamlInputFormatter.cs

C#
public class YamlInputFormatter : TextInputFormatter
{
    private readonly Deserializer _deserializer;

    public YamlInputFormatter(Deserializer deserializer)
    {
        _deserializer = deserializer;

        SupportedEncodings.Add(Encoding.UTF8);
        SupportedEncodings.Add(Encoding.Unicode);
        SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationYaml);
        SupportedMediaTypes.Add(MediaTypeHeaderValues.TextYaml);
    }

    public override Task<InputFormatterResult> ReadRequestBodyAsync
                    (InputFormatterContext context, Encoding encoding)
    {
        if (context == null)
        {
            throw new ArgumentNullException(nameof(context));
        }

        if (encoding == null)
        {
            throw new ArgumentNullException(nameof(encoding));
        }

        var request = context.HttpContext.Request;

        using (var streamReader = context.ReaderFactory(request.Body, encoding))
        {
            var type = context.ModelType;

            try
            {
                var model = _deserializer.Deserialize(streamReader, type);
                return InputFormatterResult.SuccessAsync(model);
            }
            catch (Exception)
            {
                return InputFormatterResult.FailureAsync();
            }
        }
    }
}

The code is pretty much self-explanatory. Get the Yaml content from the request body and deserialize them into generic type and you are done.

Here goes the YamlOutputFormatter.cs:

C#
public class YamlOutputFormatter : TextOutputFormatter
    {
        private readonly Serializer _serializer;

        public YamlOutputFormatter(Serializer serializer)
        {
            _serializer = serializer;

            SupportedEncodings.Add(Encoding.UTF8);
            SupportedEncodings.Add(Encoding.Unicode);
            SupportedMediaTypes.Add(MediaTypeHeaderValues.ApplicationYaml);
            SupportedMediaTypes.Add(MediaTypeHeaderValues.TextYaml);
        }

        public override async Task WriteResponseBodyAsync
              (OutputFormatterWriteContext context, Encoding selectedEncoding)
        {
            if (context == null)
            {
                throw new ArgumentNullException(nameof(context));
            }

            if (selectedEncoding == null)
            {
                throw new ArgumentNullException(nameof(selectedEncoding));
            }

            var response = context.HttpContext.Response;
            using (var writer = context.WriterFactory(response.Body, selectedEncoding))
            {
                WriteObject(writer, context.Object);

                await writer.FlushAsync();
            }
        }

        private void WriteObject(TextWriter writer, object value)
        {
            if (writer == null)
            {
                throw new ArgumentNullException(nameof(writer));
            }

            _serializer.Serialize(writer, value);
        }
    }

In case you are wondering, from where the MediaTypeHeaderValues came from? It's a simple class where I've setup all the media type headers for my application.

C#
internal class MediaTypeHeaderValues
{
    public static readonly MediaTypeHeaderValue ApplicationYaml
        = MediaTypeHeaderValue.Parse("application/x-yaml").CopyAsReadOnly();

    public static readonly MediaTypeHeaderValue TextYaml
        = MediaTypeHeaderValue.Parse("text/yaml").CopyAsReadOnly();
}

Notice that the YamlInputFormatter’s constructor is accepting a Deserializer where YamlOutputFormatter’s constructor is accepting a Serializer. We build the Serializer and Deserializer with some options tweaking while configuring the formatters in the Startup.cs’s ConfigureServices method.

C#
public void ConfigureServices(IServiceCollection services)
{
    // Add framework services
    services.AddMvc(options=>
    {
        options.InputFormatters.Add(new YamlInputFormatter(new DeserializerBuilder().
              WithNamingConvention(namingConvention: new CamelCaseNamingConvention()).Build()));
        options.OutputFormatters.Add(new YamlOutputFormatter(new SerializerBuilder().
              WithNamingConvention(namingConvention: new CamelCaseNamingConvention()).Build()));
        options.FormatterMappings.SetMediaTypeMappingForFormat
                       ("yaml", MediaTypeHeaderValues.ApplicationYaml);
    });
}

A simple GET request with Accept header set to application/x-yaml:

Image 1

A simple POST request with Content-Type header set to application/x-yaml:

Image 2

The formatter mapper is a slick option which can come in very handy when calling the actions from a browser client with a specified format. For example, setting up a [HttpGet("/api/[controller].{format}")] attribute will return the action result in the format defined in the browser’s URL.

C#
[FormatFilter]
[HttpGet]
[HttpGet("/api/[controller].{format}")]
public IEnumerable<Geek> Get()
{
    return new List<Geek>()
    {
        new Geek() { Id = 1, Name = "Fiyaz", Expertise="Javascript", Rating = 3.0M },
        new Geek() { Id = 2, Name = "Rick", Expertise = ".Net", Rating = 5.0M }
    };
}

You can call the action like this, http://appurl/geeks.yaml to get the response in Yaml format or you can call it like, http://appurl/geeks.json to get the response in Json format.

Image 3

And that’s it! This is all I know about building custom formatters for .NET Core. You can find a bunch of other formatters from other awesome community members scattered around the web if you want or build your own. I’ve added two other output formatters in my solution provided in the github repository. One for Pdf and the other one for Xlsx format. Here is the link for the repo.

History

  • 22nd March, 2017: Initial version

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)

Share

About the Author

Fiyaz Hasan
Architect Geek Hour
Bangladesh Bangladesh
Tech Enthusiast | Contributing Author on Microsoft Docs | Github Country Leader (C# and Typescript)

A .NET and JavaScript enthusiast. Likes to work with different technologies on different platforms. Loves the logic and structure of coding and always strives to write more elegant and efficient code. Passionate about design patterns and applying Software Engineering best practices.

I'm a young coder who did exceedingly well in my education and was offered an internship by Microsoft. I've tried a range of things and realized that what I need is a super creative challenge. I'm hungry for a real role in a challenging startup, something I can stick with for years

Comments and Discussions

 
-- There are no messages in this forum --