ASP.NET Core MVC Custom Model Binding

What is Model Binding?

Model Binding in MVC refers to how HTTP Requests are intercepted by ASP.NET Core Middleware and translated to the signatures of MVC Controllers that you define and subsequently to represented model objects on those signatures. Consider the following example:

  1. POST /v1/User/23
  2. {
  3.         "name": "Travis",
  4.         "phone": "123-345-4543",
  5.         "age": 6
  6. }
  7.  
  8. ——————————————-
  9. TRANSLATES TO ->
  10. ——————————————-
  11.  
  12. [HttpPost]
  13. [Route("v1/[controller]/{userId:int}")]
  14. void UpdateUser(int userId, User user);
  15.  

Model binding defines how the POST message body is translated to the “User” object, and similarly how the incoming route with a user ID is connected to the integer of the method signature titled “UserId”. Model binding conventions and attributes in ASP.NET Core MVC usually work very well. There is a plethora of flexibility and options that generally make defining API contracts in .NET Core a real pleasure. For an overview of model binding and to get started, have a look at:

https://docs.microsoft.com/en-us/aspnet/core/mvc/models/model-binding?view=aspnetcore-2.1

Customizing Model Binding

As good as it is, there are definitely times when you may want to customize how certain signatures are translated to objects from incoming HTTP requests. Sometimes you want to make a really convenient way of mapping certain objects, or other times you want to validate incoming text for custom business rules. Either way, it is pretty flexible to implement a custom model binding object.

Create a class that extends IModelBinder:

  1. using System.Linq;
  2. using System.Threading.Tasks;
  3. using Microsoft.AspNetCore.Mvc.ModelBinding;
  4.  
  5. namespace Controllers
  6. {
  7.     public class CustomModelBinder : IModelBinder
  8.     {
  9.         public Task BindModelAsync(ModelBindingContext bindingContext)
  10.         {
  11.             // use the http request object to make decisions about how to bind to the model
  12.             var request = bindingContext?.ActionContext?.HttpContext?.Request;
  13.  
  14.             // todo – create some values based on the request (i.e. path, querystring, etc)
  15.             // use the name of the field in the signature if needed for something generic
  16.                         // in this case the value would be "parameter"
  17.             var fieldName = bindingContext.FieldName;
  18.  
  19.             // mark the binding context result to insert into the actual value
  20.             bindingContext.Result = ModelBindingResult.Success("hello world");
  21.  
  22.             return Task.CompletedTask;
  23.         }
  24.     }
  25. }

Apply the new Model Binder to your Controller method Signature:

  1. [HttpGet("/hello")]
  2. public ActionResult<string> GetTest([ModelBinder(typeof(CustomModelBinder))] string parameter)
  3. {
  4.         // adding the custom model binder made the string parameter say "hello world"
  5.         if (parameter == "hello world")
  6.         {
  7.                 return "it worked";
  8.         }
  9.  
  10.     return "it did not work";
  11. }

Creating custom model binding logic is very flexible since it is defined programmatically, outside of the aspect-oriented and declarative syntax. There ARE MANY more ways of applying custom model binding directly to model objects (POCO class objects, etc). For more information get started at: https://docs.microsoft.com/en-us/aspnet/core/mvc/advanced/custom-model-binding?view=aspnetcore-2.1

*NOTE: ASP.NET Core has pretty flexible and complex model binding built in. Just when I think I may need a custom model binder, I find a way to use an existing syntax option to make it work the way I need. Once in a while, this custom flexibility may be useful. However, I’d highly encourage looking for out of the box ways to bind what you need first, and only when necessary build something custom. Writing and unit testing a good custom model binder can be very tedious but worth it for the right scenario.

Example: Enum Member Value Custom Model Binder

*In this particular case, we struggled greatly looking for a resolution to how we wanted to bind the model, but could not find one.

The Problem

Our organization standard is to use “snake_case” for JSON serialization. However, when describing known keys in an enumeration we use “kebab-case”. In the creation of a user preference service, the preference type is an enumeration that we wanted such behaviour. By default, .NET seems to serialize Enum values no problem into any associated value specified in an “EnumMember” attribute (allowing for complete customization of the value). But there seems to be no way in ASP.NET Core for it to deserialize according to the EnumMember attribute, out of the box, for root query parameters in MVC.

A look at the simple Enum:

  1. public enum UserPreferenceType
  2. {
  3.     [EnumMember(Value = "landing-page-sort-order")]
  4.     LandingPageSortOrder = 1,
  5.  
  6.     [EnumMember(Value = "landing-page-pagination")]
  7.     LandingPagePagination = 2,
  8. }

The following is an example MVC controller signature where we want to accept an optional query value for the user preference type, and have it deserialize from the “kebab-case” values above into the Enum.

  1. [HttpGet("{userKey}")]
  2. public List<UserPreferenceDto> GetByUser(int userKey, UserPreferenceType? preferenceTypeKey = null)

The following HTTP request will yield the preferenceTypeKey always being NULL using the “kebab-case”. I could pass in: “LandingPageSortOrder” instead and it would work just fine. But that is not the nicely fine-tuned contract I was hoping to expose.

  1. http://test.com/v1/preference/23?preferenceTypeKey=landing-page-sort-order

The Solution

Now, let’s create a custom model binder to handle this…

  1. public class EnumMemberValueBinder : IModelBinder
  2. {
  3.     public Task BindModelAsync(ModelBindingContext bindingContext)
  4.     {
  5.         if (bindingContext == null)
  6.         {
  7.             throw new ArgumentNullException(nameof(bindingContext));
  8.         }
  9.  
  10.  
  11.         // validate this can only be used for Enum types
  12.         if (!bindingContext.ModelMetadata.IsEnum)
  13.         {
  14.             throw new InvalidOperationException($"Cannot use custom model binder EnunMemberValueBinder for non Enum type: { bindingContext.ModelMetadata.ModelType.Name } on field { bindingContext.FieldName }");
  15.         }
  16.  
  17.         // try to determine the correct value.
  18.         var request = bindingContext?.ActionContext?.HttpContext?.Request;
  19.         if (request != null &amp;&amp; request.Query.ContainsKey(bindingContext.FieldName))
  20.         {
  21.             var queryValues = request.Query[bindingContext.FieldName];
  22.             if (queryValues.Count == 1)
  23.             {
  24.                 var result = queryValues.Single().ToNullableEnum(bindingContext.ModelMetadata.ModelType) as Enum;
  25.                 if (result != null)
  26.                 {
  27.                     bindingContext.Result = ModelBindingResult.Success(result);
  28.                     return Task.CompletedTask;
  29.                 }
  30.             }
  31.         }
  32.  
  33.         // if this point was reached, then no value could be found
  34.         // determine if this is a required field type to include (indicated by a non-nullable enum type)
  35.         if (!bindingContext.ModelMetadata.IsNullableValueType)
  36.         {
  37.             bindingContext.ModelState.TryAddModelError(bindingContext.FieldName, $"{ bindingContext.FieldName } is a required field.");
  38.             bindingContext.Result = ModelBindingResult.Failed();
  39.         }
  40.  
  41.         return Task.CompletedTask;
  42.     }
  43. }

 

This custom model binder has a number of behaviours to be aware of:

– Validates that the Model being bound to is in fact a type of Enum.

– Safely pulls the value out of the query string that matches the same name as the field name that it is bound too (meaning we can use this model binder in the same signature multiple times).

– The string is dynamically converted to a Nullable Enum matching the bound type using reflection (meaning we can use for any Enum).

– If the provided value does not match an EnumMember in the enum or is not provided on the query string, a custom model error is added, and model binding triggers a failure on the context. But notice this only happens if “IsNullableValueType”. This allows us to use nullable Enum or not and have that enforce model validation failure if not provided or incorrect. The model validation message could easily be updated to include the possible enum options as well.

Notice the plethora of information available to you inside the binding context. I would encourage you to debug and take a look at it. The ModelMetadata specifically provides a great deal of information about the binding source, the property (reflection), the HTTP context, etc.

Finally, all we have to do is update the method signature to use the custom model binder:

  1. [HttpGet("{userKey}")]
  2. public List<UserPreferenceDto> GetByUser(int userKey, [ModelBinder(typeof(EnumMemberValueBinder))] UserPreferenceType? preferenceTypeKey = null)

Or similarly, if I wanted it to be a required I could use the following signature:

  1. public List<UserPreferenceDto> GetByUser(int userKey, [ModelBinder(typeof(EnumMemberValueBinder))] UserPreferenceType preferenceTypeKey)