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. The same goes for the XML and other formatters. Out of the box, .NET provides some response data formatters. This official documentation described them briefly.

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

We have two abstract classes provided by the framework, InputFormmeter, and OutputFormatter. You would want to extend from these classes to make your custom formatters. To make things a bit easier to work with responses that are simple string representations of data formats there are two other abstract formatter classes i.e. TextInputFormatter and TextOuputFormatter.

When using the Yaml output formatter you would get the response out of the current HttpContext and serialize them into raw Yaml response text. Pretty much the same goes for the input formatter. In this case, you would deserialize the Yaml content from the request and use them in a generic form. Another important thing is that you have to explicitly set a media type header for these formatters. Doing that will activate the formatters whenever a client defines an Accept header (for output) and Content-Type (for input) with that specific media type format e.g. 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 the 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 an 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.

By the way I’m using the YamlDotNet library from @antoineaubry for Yaml’s serializing and desirializing process.

Yaml Output Formatter

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);
    }
}
YamlOutputFormatter.cs

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.

Same goes for the input formatter. In this case, you would serialize the Yaml content from the client's request and use them in a generic form.

Yaml Input Formatter

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 async 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 content = await streamReader.ReadToEndAsync();
                var model = _deserializer.Deserialize(content, type);
                return await InputFormatterResult.SuccessAsync(model);
            }
            catch (Exception)
            {
                return await InputFormatterResult.FailureAsync();
            }
        }
    }
}
YamlInputFormatter.cs

MediaTypeHeaderValues a class where I've set up all the media type headers for my application.

 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  is accepting a Serializer. You can build the serialize and deserializer with some options while configuring the formatters in the Startup.cs.

public void ConfigureServices(IServiceCollection services)
{
    services.AddControllers(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 GET request with Accept header set to application/x-yaml

Request
-------

curl -X 'GET' \
  'http://localhost:5000/api/Geeks' \
  -H 'accept: application/x-yaml'

Response
--------

- id: 1
  name: Fiyaz
  expertise: Javascript
  rating: 4.0
- id: 2
  name: Rick
  expertise: .Net
  rating: 5.0

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

curl -X 'POST' \
  'http://localhost:5000/api/Geeks' \
  -H 'accept: application/x-yaml' \
  -H 'Content-Type: application/x-yaml' \
  -d 'id: 3
name: Jon Doe
expertise: Lorem Ipsum
rating: 5'

FormatterMappings lets you call an action with a specific format defined in a URL segment.

[FormatFilter]
[HttpGet]
[HttpGet("/api/[controller].{format}")]
public IEnumerable<Geek> Get()
{
    return _geeks;
}
GeeksController.cs

You can call the action like, 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.

And that’s it. This is all you need to know about building custom formatters in .NET. You can find a bunch of other formatters from other awesome community members scattered around the web if you want.

fiyazbinhasan/CoreFormatters
.NET Core Custom Formatter for Yaml. Contribute to fiyazbinhasan/CoreFormatters development by creating an account on GitHub.
Format response data in ASP.NET Core Web API
Learn how to format response data in ASP.NET Core Web API.
Custom formatters in ASP.NET Core Web API
Learn how to create and use custom formatters for web APIs in ASP.NET Core.