Wednesday, September 22, 2021

Polymorphic Binding in .NET 5 Web API

In this article, we are going to look at how to implement polymorphic binding in .NET 5 Web API from a JSON request body into derived request models using Newtonsoft.Json.

The Goal

You have a data model that is polymorphic by nature and you want to use the same API endpoint to send in multiple different data models, and have them polymorphically bind to their corresponding request model in the controller.

This is what this implementation of polymorphic binding in .NET 5 will do for us.

Let’s dive into it and explain the solution through a practical problem!

The Problem

For the purpose of this article, let’s say that our product has multiple different types of forms that a user can fill out and submit.

Our goal is to build a Web API that will accept both types of forms in a single POST endpoint and store them in a NoSQL database.

The Data Model

Let’s say we have two types of forms.
The first one is called T1 the other one is called T2.

First, we create an abstract class called Form, and two derived classes for the forms – T1Form and T2Form. Then, we create a FormType enum that will be sent in the request to indicate what form is being submitted.

    public enum FormType { T1, T2 }
    public abstract class Form
    {
        protected abstract FormType FormType { get; }
        protected Guid Id { get; set; }
    }
    public class T1Form : Form
    {
        public string T1Property1 { get; set; }
        public string T1Property2 { get; set; }
        protected override FormType FormType => FormType.T1;
    }
    public class T2Form : Form
    {
        public string T2Property1 { get; set; }
        public string T2Property2 { get; set; }
        protected override FormType FormType => FormType.T2;
    }

Our goal now is to be able to pass in both JSONs below in the request body and have the correct type be instantiated at runtime.

{
    "Id":"00000000-0000-0000-0000-000000000000",
    "FormType":0,
    "T1Property1":"timeaura",
    "T1Property2":"timeaura"
}
{
    "Id":"00000000-0000-0000-0000-000000000000",
    "FormType":1,
    "T2Property1":"timeaura",
    "T2Property2":"timeaura"
}

Luckily, because of polymorphic binding in .NET 5, this is doable!

The Controller

As you would expect, we want the controller to accept a Form object, but be polymorphically bound to either T1Form or T2From at runtime.

    [HttpPost]
    public IActionResult SaveForm([FromBody]Form form)
    {
        return new ObjectResult(new { });
    }

Running this by itself will not produce the desired outcome, since the default System.Text JSON serializer does not know how to do this.

Newtonsoft JSON

The easiest and most painless way to overcome this issue is to use the free MIT – Licensed NuGet package called Newtonsoft.JSON

Installation

To install the NuGet packages, just run the following commands in the NuGet Package Manager.

Install-Package Newtonsoft.Json
Install-Package Microsoft.AspNetCore.Mvc.NewtonsoftJson

Registration

To register Newtonsoft.Json as the JSON serializer for the Web API, go into the Startup.cs file, and append .AddNewtonsoftJson() to the IMvcBuilder pipeline in the ConfigureServices method. We are adding the converter here which we will create next.

 public void ConfigureServices(IServiceCollection services)
 {
     services.AddControllers().AddNewtonsoftJson((setup) => {
     setup.SerializerSettings.Converters = new List<JsonConverter>   
          { new FormCreationConverter() };
     });
 }

Custom Converter

The final thing we need to do is to create a custom JSON converter for our forms. This is the advantage of using Newtonsoft.Json over System.Text.
It allows you to override the creation of the objects very easily.

Create a class called FormCreationConverter, and paste in this code.

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using System;

namespace TimeAura.PolymorphicBinding.JsonConverters
{
    public class FormCreationConverter : JsonConverter
    {
        public override bool CanWrite { get { return false; } }

        public Form Create(JObject jObject)
        {
            var paymentMethodType = GetFormType(jObject);
            if (paymentMethodType == FormType.T1)
            {
                return new T1Form();
            }
            else if (paymentMethodType == FormType.T2)
            {
                return new T2Form();
            }
            throw new ArgumentException($"Cannot convert JSON.");
        }

        public override bool CanConvert(Type objectType) => typeof(Form).IsAssignableFrom(objectType);

        public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
        {
            JObject jObject = JObject.Load(reader);
            var target = Create(jObject);
            serializer.Populate(jObject.CreateReader(), target);
            return target;
        }

        public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
        { throw new NotImplementedException(); }

        private static FormType GetFormType(JObject jObject)
        {
            var paymentMethodType = jObject.GetValue("FormType", StringComparison.OrdinalIgnoreCase);
            return paymentMethodType.ToObject<FormType>();
        }
    }
}

Result

Having all this in place, we can send in the post request and see it happen.

Sending a POST request to our API at route “{URL}/form” with the body:

{
    "Id":"00000000-0000-0000-0000-000000000000",
    "FormType":1,
    "T2Property1":"timeaura",
    "T2Property2":"timeaura"
}

Results in

An image showing a debugger with watch, displaying the type being resolved to T2Form

As you can see in the screenshot the object we have received in the controller is of type T2Form, which means the request has successfully been bound to the object based on the FormType.

You can find the fully working example on my Github:
https://github.com/danilopopovikj/TimeAura.PolymorphicBinding

If you want to see in a similar format how to solve the issue with time zones in .NET 5 Web APIs – please take a look at A solution for handling time zones in Web APIs

Danilo Popovikj
A software engineer willing to share.

Related Articles

Invoke an AWS Lambda Function in .NET 5

AWS Lambda is a serverless compute service that lets you run code without provisioning or managing servers. In this article, I will explain how...

Polymorphic Binding in .NET 5 Web API

In this article, we are going to look at how to implement polymorphic binding in .NET 5 Web API from a JSON request body...

React and .NET Core Web API Authentication using Firebase

Authentication is a key part of any web or mobile application that wants to provide a personalized experience to each of the end-users. It...