Polymorphic Model Binding in .Net

In this article, we will attempt to expose an endpoint that accepts different models of the derived type. This is a classic example of polymorphic model binding and would require some special handling. Before we get to the solution, let us first see the problem. Consider the following models.

public abstract class Person {
  public string Name {
    get;
    set;
  }
  public int Age {
    get;
    set;
  }
  public string EntityType {
    get;
    set;
  }
}

public class Employee: Person {
  public int Id {
    get;
    set;
  }
  public string JobTitle {
    get;
    set;
  }
}

public class Student: Person {
  public string SchoolName {
    get;
    set;
  }

}

We will go ahead and define our endpoint.

[HttpPost]
[Route("createanimal")]
public string CreateAnimal(Person person) {
  return person.GetType().Name;
}

Attempting to access the createanimal endpoint would return the following error.

System.NotSupportedException: Deserialization of types without a parameterless constructor, a singular parameterized constructor, or a parameterized constructor annotated with 'JsonConstructorAttribute' is not supported. Type 'PolymorphicParameterBinding.Models.Person'.

This is understandable. A person is an abstract class and cannot be instantiated. This is a classical use case for Polymorphic model binding wherein the request value must be bound to a specific derived type.

One solution to the scenario lies in defining your custom model binder. Let us go ahead and define PersonModelBinder, implementing the required IModelBinder interface.

public class PersonModelBinder: IModelBinder 
{
  public async Task BindModelAsync(ModelBindingContext bindingContext) 
  {
    using var sr = new StreamReader(bindingContext.HttpContext.Request.Body);
    var json = await sr.ReadToEndAsync();
    JObject requestJObject = JObject.Parse(json);
    string? type = requestJObject["entityType"]?.ToObject<string>();

    Person? person = type
    switch 
    {
        nameof(Employee) => JsonConvert.DeserializeObject<Employee>(json),
        nameof(Student) => JsonConvert.DeserializeObject<Student>(json),
        _ =>
        throw new Exception()
    };

    bindingContext.Result = ModelBindingResult.Success(person);
  }
}

As you can observe in the PersonModelBinder definition, we are reading the JSON request to detect the entityType. With the type identified, we can deserialize the JSON request to the required specific derived type.

We could now ensure our endpoint uses the model binder by decorating the required parameter in the endpoint with ModelBinderAttribute. Alternatively, we could define Model Binder Provider, which can be added to the ModelBinderProviders collection and used by default (no need to specifically use the ModelBinderAttribute in each model)

public class PersonModelBinderProvider: IModelBinderProvider 
{
  public IModelBinder? GetBinder(ModelBinderProviderContext context) 
  {
    if (context.Metadata.ModelType != typeof(Person)) 
    {
      return null;
    }

    return new PersonModelBinder();
  }
}

Now all that is left to do is to add/register our PersonModelBinderProvider to the ModelBinderProvider collection.

 builder.Services.AddMvc(o=> o.ModelBinderProviders.Insert(0, new PersonModelBinderProvider()));

We are now ready to accept polymorphic models in our endpoint. Test by passing different specific types of Person.


Similar Articles