Using custom request and response serializers in ASP.NET Core

Configuring input and output serailzer/deserialier in ASP.NET Core pipeline

This is not a new subject and the support for handling WebApi input and output was introduced in .NET Core 2.0 and it's been there since then. There are couple of useful pages out there and it is even well documented on Microsoft official page Custom formatters in ASP.NET Core Web API. Although it is well documented I did have some small issues when implementing it on alive project, so I will try to explain how to use this feature in ASP.NET Core WebAPI to serve different content format depending on the headers passed in the request to the API controller method.

For my specific case I needed to support both PROTOBUF and JSON format for the data bidirectional. This means that my endpoint was able to use one serializer to deserialize request and totally different one for serializing the output. This was great because I was still able to test my endpoint and get human readable response from the endpoint and have it also available to handle PROTOBUF messages in communication with other components and services within the system.

Few months ago I wrote about different types of serialization in .NET and thieir pros and cons. I even did a small benchmark to compare size of the output data during serialization which can improve the system speed as data transfer is done through the network which is basically an expensive IO operation. Second factor is the speed of serialization and deserialization which can also impact the communication process.

Size

Speed

You can clearly see that PROTOBUF is pretty much the best option taking size and speed into consideration. For this reason I wanted to develop a micro-service component which will communicate with the rest of the services usng REST but using PROTOBUF as well as it is faster and lighter than default JSON format.

Since the project is in .NET Core, I used InputFormatter and OutputFormatter class to override their base methods and use PROTOBUF libraries to deserailize request and serialize response. Because I still wanted to have JSON format available to test stuff in POSTMAN i left the default JSON formatters in the MVC middleware, but before I switch to the flow of the request let's look at the custom input and output formatters. Let's start with the input formatter class:

using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
using System.Threading.Tasks;
using ProtoBuf;

namespace Core.Serialization.Sample
{
    public class ProtobufInputFormatter : InputFormatter
    {
        public ProtobufInputFormatter()
        {
            this.SupportedMediaTypes.Clear();

            //Look for specific media type declared with Accept header in request
            this.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/x-protobuf"));
        }

        public override bool CanRead(InputFormatterContext context)
        {
            return base.CanRead(context);
        }

        public override Task<InputFormatterResult> ReadRequestBodyAsync(InputFormatterContext context)
        {
            var type = context.ModelType;
            var request = context.HttpContext.Request;
            MediaTypeHeaderValue requestContentType = null;
            MediaTypeHeaderValue.TryParse(request.ContentType, out requestContentType);


            object result = Serializer.Deserialize(type, context.HttpContext.Request.Body);
            return InputFormatterResult.SuccessAsync(result);
        }
    }
}

    

You can see that inside the constructor we are adding application/x-protobuf media type header to be handled. In short this means when the data is sent with Content-Type=application/x-protobuf headers this input format will be triggered and execute it's implementation. In other cases it will leave the request to be handled by other formatters in the MVC middle ware collection. This is a typical example of chain of responsibility pattern.

We have our protobuf handler for the incoming protobuf data, but for the response we still do not have anything to serialize the response to designated format back to the user. For that purpose I wrote custom OutputFormatter class:

using Microsoft.AspNetCore.Mvc.Formatters;
using Microsoft.Net.Http.Headers;
using System.Threading.Tasks;
using ProtoBuf;

namespace Core.Serialization.Sample
{
    public class ProtobufOutputFormatter : OutputFormatter
    {
        public ProtobufOutputFormatter()
        {
            this.SupportedMediaTypes.Clear();

            //Look for specific media type declared with Content-Type header in request
            this.SupportedMediaTypes.Add(MediaTypeHeaderValue.Parse("application/x-protobuf"));           
        }

        public override bool CanWriteResult(OutputFormatterCanWriteContext context)
        {
            return base.CanWriteResult(context);
        }

        public override Task WriteResponseBodyAsync(OutputFormatterWriteContext context)
        {

            var response = context.HttpContext.Response;

            Serializer.Serialize(response.Body, context.Object);
            return Task.FromResult(response);

        }
    }
}

    

Output formatter has the same media type added to the list on the class constructor as the input formatter but this means that the response will be triggered in case Accept=application/x-protobuf header value is sent in the request.

Now let's see how do we engage these two classes in our application request life cycle. Everything in the request pipeline in ASP.NET Core is declared in the Startup.cs class file, so the same goes fr the request and response formatters.

using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;

namespace Core.Serialization.Sample
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddMvc(options =>
            {
                options.InputFormatters.Add(new ProtobufInputFormatter());
                options.OutputFormatters.Add(new ProtobufOutputFormatter());
            });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
                app.UseDeveloperExceptionPage();
            }
            app.UseMvc();
        }
    }
}

    

If you put the break point in the Startup.cs class file inside AddMvc you will see that by default there are two JSON formatters. These are built in formatters. What we are going to do is just adding our custom formatters in the MVC middleware. Now let's jump to the controller and see how this is handled in the WebApi endpoint methods. 

I used project template controller and left only POST and GET HTTP methods handers

using Core.Serialization.Sample.Models;
using Microsoft.AspNetCore.Mvc;

namespace Core.Serialization.Sample.Controllers
{
    [Route("api/[controller]")]
    [ApiController]
    public class ValuesController : ControllerBase
    {

        [HttpGet]
        public IActionResult Get(int id)
        {
            return Ok(new Person()
            {
                Id = id,
                Email = "test@mail.com",
                Name = "John Smith"
            });
        }

        // POST api/values
        [HttpPost]
        public Person Post([FromBody] Person value)
        {
            return value;
        }
    }
}

    

For the model I use auto generated class from the PROTO definition

// This file was generated by a tool; you should avoid making direct changes.
// Consider using 'partial classes' to extend these types
// Input: Person.proto

#pragma warning disable CS1591, CS0612, CS3021, IDE1006
namespace Core.Serialization.Sample.Models
{

    [global::ProtoBuf.ProtoContract()]
    public partial class Person : global::ProtoBuf.IExtensible
    {
        private global::ProtoBuf.IExtension __pbn__extensionData;
        global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
            => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);

        [global::ProtoBuf.ProtoMember(1, Name = @"name", IsRequired = true)]
        public string Name { get; set; }

        [global::ProtoBuf.ProtoMember(2, Name = @"id")]
        public int Id
        {
            get { return __pbn__Id.GetValueOrDefault(); }
            set { __pbn__Id = value; }
        }
        public bool ShouldSerializeId() => __pbn__Id != null;
        public void ResetId() => __pbn__Id = null;
        private int? __pbn__Id;

        [global::ProtoBuf.ProtoMember(3, Name = @"email")]
        [global::System.ComponentModel.DefaultValue("")]
        public string Email
        {
            get { return __pbn__Email ?? ""; }
            set { __pbn__Email = value; }
        }
        public bool ShouldSerializeEmail() => __pbn__Email != null;
        public void ResetEmail() => __pbn__Email = null;
        private string __pbn__Email;

        [global::ProtoBuf.ProtoMember(4, Name = @"phone")]
        public global::System.Collections.Generic.List<PhoneNumber> Phones { get; } = new global::System.Collections.Generic.List<PhoneNumber>();

        [global::ProtoBuf.ProtoMember(5, Name = @"test_packed", IsPacked = true)]
        public int[] TestPackeds { get; set; }

        [global::ProtoBuf.ProtoMember(6, Name = @"test_deprecated")]
        [global::System.Obsolete]
        public int TestDeprecated
        {
            get { return __pbn__TestDeprecated.GetValueOrDefault(); }
            set { __pbn__TestDeprecated = value; }
        }
        public bool ShouldSerializeTestDeprecated() => __pbn__TestDeprecated != null;
        public void ResetTestDeprecated() => __pbn__TestDeprecated = null;
        private int? __pbn__TestDeprecated;

        [global::ProtoBuf.ProtoMember(7, Name = @"foreach")]
        public int Foreach
        {
            get { return __pbn__Foreach.GetValueOrDefault(); }
            set { __pbn__Foreach = value; }
        }
        public bool ShouldSerializeForeach() => __pbn__Foreach != null;
        public void ResetForeach() => __pbn__Foreach = null;
        private int? __pbn__Foreach;

        [global::ProtoBuf.ProtoContract(Name = @"phone_number")]
        public partial class PhoneNumber : global::ProtoBuf.IExtensible
        {
            private global::ProtoBuf.IExtension __pbn__extensionData;
            global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
                => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);

            [global::ProtoBuf.ProtoMember(1, Name = @"number", IsRequired = true)]
            public string Number { get; set; }

            [global::ProtoBuf.ProtoMember(2, Name = @"type")]
            [global::System.ComponentModel.DefaultValue(Person.PhoneType.Home)]
            public Person.PhoneType Type
            {
                get { return __pbn__Type ?? Person.PhoneType.Home; }
                set { __pbn__Type = value; }
            }
            public bool ShouldSerializeType() => __pbn__Type != null;
            public void ResetType() => __pbn__Type = null;
            private Person.PhoneType? __pbn__Type;

        }

        [global::ProtoBuf.ProtoContract(Name = @"phone_type")]
        public enum PhoneType
        {
            [global::ProtoBuf.ProtoEnum(Name = @"mobile")]
            Mobile = 0,
            [global::ProtoBuf.ProtoEnum(Name = @"home")]
            Home = 1,
            [global::ProtoBuf.ProtoEnum(Name = @"work")]
            Work = 2,
        }

    }

    [global::ProtoBuf.ProtoContract(Name = @"opaque_message_list")]
    public partial class OpaqueMessageList : global::ProtoBuf.IExtensible
    {
        private global::ProtoBuf.IExtension __pbn__extensionData;
        global::ProtoBuf.IExtension global::ProtoBuf.IExtensible.GetExtensionObject(bool createIfMissing)
            => global::ProtoBuf.Extensible.GetExtensionObject(ref __pbn__extensionData, createIfMissing);

        [global::ProtoBuf.ProtoMember(1, Name = @"messages_list")]
        public global::System.Collections.Generic.List<byte[]> MessagesLists { get; } = new global::System.Collections.Generic.List<byte[]>();

    }

}

#pragma warning restore CS1591, CS0612, CS3021, IDE1006

    

You can read more about how to generate the C# model class form the PROTOBUF PROTO file from the article Generate C# models from Protobuf proto files directly from Visual Studio.

We have it all setup to take our sample ASP.NET Core WebApi project for a spin and see how does it work. So start the application in debug mode from Visual Studio and let's test the request and response with POSTMAN.

I am first going to test HTTP GET method by passing simple integer as a parameter.

Json Get

This is to confirm that basic built in formatters are still working an they are handling the input and output of the method. Next test will be to perform get by sending the id parameter to a default input formatter but serializing the response in our custom output formatter.

Proto Get

You can see that output is handled by the custom output formatter and response is serialized to PROTOBUF instead to JSON format. We can save this response to a file and use it for the HTTP POST where we'll test out custom input formatter and see how does it handle the PROTOBUF input data. For output we are going to request JSON with Accept headers.

Proto Post Json

To have a more detailed look, if we put the breakpoint in the POST method inside the controller we will see that input is deserialized without any problem and the data matched the data generated in the previous HTTP GET test.

Debug

Instead of doing any format specific coding in the controller we can just add as many formatters as we want to handle different types and our API endpoints will behave depending on the data type sent to them and headers describing the data sent and data format expected from our endpoint.

In this case it is beneficial for the performance for production while it still stays readable by a human for the debug purpose by keeping the default formatter in the MVC middleware options. The whole solution for this article is available on github https://github.com/dejanstojanovic/dotnetcore-api-serialization

References

Disclaimer

Purpose of the code contained in snippets or available for download in this article is solely for learning and demo purposes. Author will not be held responsible for any failure or damages caused due to any other usage.


About the author

DEJAN STOJANOVIC

Dejan is a passionate Software Architect/Developer. He is highly experienced in .NET programming platform including ASP.NET MVC and WebApi. He likes working on new technologies and exciting challenging projects

CONNECT WITH DEJAN  Loginlinkedin Logintwitter Logingoogleplus Logingoogleplus

JavaScript

read more

SQL/T-SQL

read more

Umbraco CMS

read more

PowerShell

read more

Comments for this article