Custom Model Binding ASP.NET Core 2.1

I was recently assigned a task by the throne: From a .NET backend, intercept incoming data from the client, filter out all the strings and perform various functions (Normalize names of people and places, trim whitespaces etc.) on the data before persisting it to a database. It was a fairly narrow domain, given I only had to deal with a single model class and just one action method. So I started preparing for my quest.


I put together my gear: Visual Studio, Google Chrome and Postman, mounted my horse and headed West, on a quest to fulfill the King's wishes. My first stop was Redmond, the land of Microsoft Official Docs (which, by the way, I strongly suggest you read before proceeding). An informant there gave me a couple of helpful pointers which led to the following discoveries:


  1. The client has many avenues of sending data to your backend:
    • Form data
    • Http Request body
    • URI (route values and query parameters)

  2. Whenever a Request hits the backend, before the values are available for use in the action method, model binding has to occur. Now the default model binder does an excellent job at this but sometimes you may need functionality not provided by default.

    Which is why the Redmondonian druids graciously provided any willing tinkerer the ability to forge their own model binders and bind their data to the models themselves!


I gave the informant 2 gold coins for this valuable information and set out to plan the next phase of my quest. So now we have the required tools, a recipe and the blessings of our ancestors. Next, we need a plan!

Part A: Understanding the Problem

The client will send me an object that looks like this:

{
  "firstName": "HERMES",
  "secondName": "Dandelion  ", // with trailing spaces
  "phoneNumber": 0722222222
}

..which I will receive in this action method:

[Route("api/Person")]
public class PersonController : Controller {
  [Route("p")]
  [HttpPost]
  public IActionResult PostPerson(Person somePerson) {
      // var result = personService.Add(somePerson);
      return Ok();
  }
}

Person looks like this..

  public class Person {
      public string FirstName { get; set; }
      public string SecondName { get; set; }
      public string PhoneNumber { get; set; }
  }

It is important to note that the default model binder will work just fine in this case. But for this post, we want a custom model binder to:

  • proper case the names
  • remove the trailing space in the second name

Now that we understand the solution, let's brew up a solution...

⬆ back to top

Part B: Coming up with a solution

Now, according to my informant at Redmond, in order to surmount this task I had to do two things:

  • Create my custom model binder
  • Inform the framework of the existence of my custom model binder

Creating our model binder

To create custom model binders, we implement interface IModelBinder which exposes a single method BindModelAsync().

  public class PersonBinder : IModelBinder {
    public Task BindModelAsync(ModelBindingContext bindingContext) {
        // don't worry we'll implement this shortly
    }
  }

Once we do that, we'll have to inform the framework on when to use our model binder

Informing the framework

There are at least two ways to tell the framework where to look for our custom binders:

1. Using decorators:
  • Decorating a property

    ...with a ModelBinderAttribute specifying the type of our custom model binder. Using our example we can have the binder be called to bind just the first name:

    public class Person {
      [ModelBinder(typeof(PersonModelBinder))]
      public string FirstName { get; set; }
      public string SecondName { get; set; }
      public string PhoneNumber { get; set; }
    }

  • Decorating an Action's parameter:

    ...with the same attribute

    public IActionResult PostPerson([ModelBinder(typeof(PersonModelBinder))]Person content) {
        Console.Write(content);
        return Ok();
    }
  • Decorating the whole Class or Struct
    [ModelBinder(typeof(PersonModelBinder))]
    public class Person {
      public string FirstName { get; set; }
      public string SecondName { get; set; }
      public string PhoneNumber { get; set; }
    }

    In this case the model binder is used for each occurrence of the target type. Unluckily, we can’t follow this path for system types (like string), as we can’t apply any attribute to it.

2. Adding our binder to the pipeline

The pipeline is... There's a good read about the pipeline here.

With this approach we can set some rules that determine when our binder gets called. We'll be going the pipeline way.


When I asked my informant how to insert my custom binder into the pipeline he dismissed my proposal saying "one does not simply add one's binder to the pipeline" gesticulating it in a way that I suppose he thought I should understand. I did not.

He proceded to tell me that in order to ensure my binder gets called, I need to create a provider for it. A provider is basically a method that returns an instance of my binder. And this provider is what gets plugged into the pipeline. A provider is created by implementing IModelBinderProvider that exposes a single method GetBinder() in which I should create a new instance of my binder class somehow.
This is also how the built-in framework binders are implemented.

Implementing IModelBinderProvider

public class PersonModelBinderProvider : IModelBinderProvider {
  public IModelBinder GetBinder(ModelBinderProviderContext context) {
    // will return our binder
  }
}

To add it to the pipeline. Open Startup.cs and inside the ConfigureServices() method add this:

services.AddMvc(options =>
{
    // add custom binder to beginning of collection
    options.ModelBinderProviders.Insert(0, new PersonModelBinderProvider());
});

As you can see, we're inserting our binder provider in the beginning of this collection. Why? When evaluating model binders, the collection of providers is examined in order. The first provider that returns a binder is used. We don't know if another provider (such as the default one) is capable of providing a binder for our model (it is) and if so, what position it occupies in this collection, so armed with audacity we boldly insert it at the very beginning.
Now that we have a workable solution, let's implement it!

⬆ back to top

Part C: Implementing the solution

We're going to add one more file. This file contains a definition for a custom attribute which we will need in our solution.

StrFormatAttribute.cs

public enum ActionType {
  ProperCase = 0,
  RemoveExtraSpaces,
}
[AttributeUsage(AttributeTargets.All)]
public class StrFormatAttributeAttribute : Attribute {

  public StrFormatAttributeAttribute([CallerMemberName] string propertyName = null) {}

  public ActionType ActionType { get; set; }
}

Implementing the provider

Now we shall implement our provider first. Enter PersonModelBinderProvider.cs The method there GetBinder() is supposed to return anything that inherits from IModelBinder. Our custom model binder fits this description, so lets simply return an instance of it and see what happens:

  public IModelBinder GetBinder(ModelBinderProviderContext context) { 
    return new PersonModelBinder();
  }

In order to test how this works out, our binder has to be implemented as well. Since we haven't done that yet, we'll take a shortcut: go over to PersonModelBinder.cs and return Task.CompletedTask in BindModelAsync():

  public Task BindModelAsync(ModelBindingContext bindingContext) { 
    return Task.CompletedTask;
  }

Set a breaking point on GetBinder() and run the application. Go over to postman and create this request:

POST /api/Person/p HTTP/1.1
Host: localhost:49506
Content-Type: application/x-www-form-urlencoded

secondName=Dandelion    &firstName=HERMES&phoneNumber=0722222222
(note the four spaces after the word Dandelion) postman request


observations

Our provider gets called once. Why? Remember this collection: options.ModelBinderProviders.Insert(0, new PersonModelBinderProvider());? Whenever a model needs to be bound, the framework goes through this collection of binder providers looking for one to provide a binder to that model. If one does provide a binder, it gets called and the framework stops looking [for another provider for that given model].
Great! Now we know a little bit more about these things, let's experiment some more! What happens when we don't provide a binder in our provider?

public IModelBinder GetBinder(ModelBinderProviderContext context) { 
  return null;
}
observations

We will notice that the provider gets called 4 times. Why? This is because our controller action method essentially expects four models to be bound:

  • The complex property of type Person
  • The FirstName property in Person
  • The SecondName property in Person
  • The PhonNumber property in Person

Yup, blew my mind too. So if you have another class say Alien which looks like this:

public class Alien {
  public string Race {get; set;}
  public int LimbsCount {get; set;}
}

and expect it in our controller..

public IActionResult PostPerson(Person somePerson, Alien alienX) {
  return Ok();
}

Our provider gets called 7 times! So what happens after all these calls? Since we're returning null in each of these calls, it means that our provider won't provide a binder and the framework moves on to the next provider in the collection. (Eventually it will find the default provider which ought to provide a default binder for our models).

⬆ back to top


Ok, this seems like the best place to place a few misplaced notes:
As a rule, once a provider provides a binder, its job is done and it doesn't get called again. We have, however, demonstrated two exceptions to that rule:
  • When the framework 'traverses' a complex property; The binder will be called for each of the sub-properties of the complex one, whether or not a binder is provided for any of them.
  • If the action method expects more than one parameter. Even if a binder is provided for the first parameter, complex property or not, the provider is still called for the remaining parameters of the action.

Ok enough experiments. Back to our problem. In our case, we're looking to bind specific properties and not whole classes. So here's how we go about it. Going to Person.cs, let's make a few changes:

public class Person {
  [DataMember(Name = "firstName")]
  [StrFormatAttribute(ActionType = ActionType.ProperCase)]
  public string FirstName { get; set; }

  [DataMember(Name = "secondName")]
  [StrFormatAttribute(ActionType = ActionType.RemoveExtraSpaces)]
  public string SecondName { get; set; }

  [DataMember(Name = "PhoneNumber")]
  public string PhoneNumber { get; set; }
}

We've added two attributes to FirstName and SecondName one to PhoneNumber
The DataMember attribute is for data serialization to help the framework associate data from the client with our model class. It's always a good idea to include serialization attributes to you Data Transfer Objects.
The other one is the custom attribute we created earlier that will help us determine which operations to perform on which models.
Also by now I hope you've figured out that the term 'model' does not necessarily refer to a class. It can be used to refer to a property as well. Depending on where you learned .NET from this can be quite the ambiguous term.

Ok let's implement our provider, for real now:

public IModelBinder GetBinder(ModelBinderProviderContext context) {
  if (context == null) {
      throw new ArgumentNullException(nameof(context));
  }

  if (context.Metadata.IsComplexType) {
      return null;
  }

  var propertyInfosOfParent = context.Metadata.ContainerType.GetProperties();
  var propHasCustomAttribute = propertyInfosOfParent
        .Where(prop => prop.GetCustomAttributes(typeof(StrFormatAttributeAttribute), false).Length == 1)
        .Any(prop => prop.Name == context.Metadata.PropertyName);

  if (propHasCustomAttribute) {
    return new PersonModelBinder();
  }

  return null;
}
Phrase by phrase:

We don't know where this context comes from and whether it will always be available, so we check it for null:

if (context == null) {
  throw new ArgumentNullException(nameof(context));
}

Earlier we noticed that our provider gets called for the complex type Person. We are not interested in the whole Person, just its properties that are marked with our custom attribute. So we return null on this call, and hence our provider will get called three more times, one for each property of Person.

if (context.Metadata.IsComplexType) {
  return null;
}

At this point we are fairly certain that the Metadata describes some property in Person. So we get its parent container (Person) and extracting its properties. I know I know, Inception right? Actually it's Reflection.

  var propertyInfosOfParent = context.Metadata.ContainerType.GetProperties();

Then, we use Reflection again to check if the property currently being examined has our custom attribute:

var propHasCustomAttribute = propertyInfosOfParent
  .Where(prop => prop.GetCustomAttributes(typeof(StrFormatAttributeAttribute), false).Length == 1)
  .Any(prop => prop.Name == context.Metadata.PropertyName);

If it does, we return a binder for it:

if (propHasCustomAttribute) {
  return new PersonModelBinder();
}

Else, and for all other unimaginable cases, we just return null:

  return null;

Phew! At this point we've made sure that our binder gets called for only the properties we want to manually bind. Let's now go ahead and bind them.

⬆ back to top

Side note

Ok so in case you aren't caught up, our provider was providing a binder for multiple properties of Person. This means that our binder gets called for each of these properties. (in our case, it's twice, for FirstName and SecondName). So if we stop execution anywhere in this method and examine injected ModelBindingContext, we will see that it contains information about the current Property.

This context also contains a value provider that grants us access to the values that the client sent us. This means all values that the framework could get from the request (from the body, forms, request params etc) All you have to do is get them using their keys. Now this is where things get a little bit...tricky.

Implementing the binder

Now let's head over to PersonModelBinder.cs, and see how we implement BindModelAsync():

public Task BindModelAsync(ModelBindingContext bindingContext) {
  if (bindingContext == null)  
      throw new ArgumentNullException(nameof(bindingContext)); 
              
  var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
  if (valueProviderResult == ValueProviderResult.None)
      return Task.CompletedTask;

  var model = valueProviderResult.FirstValue;

  var propertyInfosOfParent = bindingContext.ModelMetadata.ContainerType.GetProperties()
      .Where(p => p.Name == bindingContext.ModelName)
      .Where(p => p.GetCustomAttributes(typeof(StrFormatAttributeAttribute), true).Length == 1)
      .ToList();

  var currentProperty = propertyInfosOfParent.FirstOrDefault();
  var customAttribute = (StrFormatAttributeAttribute)currentProperty?.GetCustomAttributes(typeof(StrFormatAttributeAttribute), false).FirstOrDefault();
  
  switch (customAttribute?.ActionType) {
    case ActionType.RemoveExtraSpaces:
      model = Regex.Replace(model, @"\s+", "");
      break;
    case ActionType.ToProper:
      CultureInfo cultureInfo = Thread.CurrentThread.CurrentCulture;
      TextInfo textInfo = cultureInfo.TextInfo;
      model = textInfo.ToTitleCase(model.ToLower());
      break;
  }

  bindingContext.Result = ModelBindingResult.Success(model);
  return Task.CompletedTask;
}

See there is a lot of logic that goes into determining which piece of data should be mapped to which model whenever data hits the server. By hijacking this process we are putting ourselves up for this task of sifting through the data available and trying to determine what goes where. This is hard to pull off because it needs to be perfect and it needs to scale.
That's why we should try and bind as few models as we can ourselves
That's why we went to all the trouble of creating custom attributes and marking exactly which properties we absolutely needed to bind ourselves and left the rest for the framework to take care of.

Since we are the ones who built the client app (which we are simulating using Postman), we can easily get the keys associated with the data sent by the client. We included these keys in Data Serialization attributes to further guide the framework on what to put where. We also gave our properties identical names to the fields sent from the client. This allows us to use the (framework-populated) key bindingContext.ModleName to retrieve our data from the value provider. So our plan is to get this value, check what operation we need to perform on it (using our custom attribute), perform it, and return the modified model.


Phrase by phrase:

The first line is a customary null check. We then proceed to get the current value from the value provider and null-check it as well. If there is no value to bind we return Task.CompletedTask. Now since we hijacked the model binding process, the framework has no way of knowing when we finish our binding, or what determines a successful binding. This is why it provides the bindingContext.Result property.

If we return Task.CompletedTask without setting this property, the framework assumes binding failed. If binding succeeded, we will set it to ModelBindingResult.Success(model) where model is our bound model.

  if (bindingContext == null) throw new ArgumentNullException(nameof(bindingContext));

  var valueProviderResult = bindingContext.ValueProvider.GetValue(bindingContext.ModelName);
  if (valueProviderResult == ValueProviderResult.None)
      return Task.CompletedTask;
  var model = valueProviderResult.FirstValue;

For some reason the property currently being examined itself is not contained in the metadata. We have to get it using reflection as we are here:

  var propertyInfosOfParent = bindingContext.ModelMetadata.ContainerType.GetProperties()
  .Where(p => p.Name == bindingContext.ModelName)
  .ToList();
  var currentProperty = propertyInfosOfParent.FirstOrDefault();

Then we retrieve our custom attribute StrFormatAttribute since it contains really important information (what operation to be performed on the model)
Once we do that, we determine what operation needs to be done, and do it:

var customAttribute = (StrFormatAttributeAttribute)currentProperty?.GetCustomAttributes(typeof(StrFormatAttributeAttribute), false).FirstOrDefault();
switch (customAttribute?.ActionType) {
  case ActionType.RemoveExtraSpaces:
    model = Regex.Replace(model, @"\s+", "");
    break;
  case ActionType.ToProper:
    CultureInfo cultureInfo = Thread.CurrentThread.CurrentCulture;
    TextInfo textInfo = cultureInfo.TextInfo;
    model = textInfo.ToTitleCase(model.ToLower());
    break;
}

Then you have to inform the framework whether or not your custom binding succeeded.

  bindingContext.Result = ModelBindingResult.Success(model);
  return Task.CompletedTask;

Now as you noticed in Person, PhoneNumber isn't adorned with our custom StrFormatAttribute. And since we're skipping over everything that doesn't wear this badge of honour, what happens to it?
See here, the framework graciously takes care of that for us, no problem.
framework binds the unbound
Yup, if you don't commit to binding a model, the framework will do it for you, unless:

  • You provide a binder for a complex property i.e. If you provide a binder for Person the framework expects you to bind and returned a full Person. If you only bind FirstName and leave the rest unset, they will be null.

⬆ back to top

And Finally..

after 3 days and 3 nights, having battled and triumphed over the vicious beasts in these lands called bugs. Having consulted tomes written by many master druids that came before me, tomes like StackOverflow and DotNetCurry. Having forged and reforged custom model binders in an effort to perfect the craft, at last, I had completed my quest.

I mounted my horse and rode home as fast as I could. I was happy that, finally, the King would recognize my efforts, acknowledge the importance of my work to the prosperity of the kingdom and even consider me when the Knighting ceremony comes along.

This was not to be. The king had completely forgotten that he had assigned me this task! I was reprimanded for wasting the kingdom's resources and was sent off to clean the stables for a week! *sigh*
Kings, and pawns.

The end

These are some notes I found while researching. They sounded important, so I put them here:
  * Custom model binders:
      -Shouldn't attempt to set status codes or return results (for example, 404 Not Found). If model binding fails, an action filter or logic within the action method itself should handle the failure.
      -Typically shouldn't be used to convert a string into a custom type, a TypeConverter is usually a better option.
  
  
  * In previous versions of mvc, When binding a model:
      -The framework would iterate through the collection of ModelBinderProviders until one of them returned a non-null IModelBinder instance.
      -The matched binder would have access to the request and the value providers, which basically extracted and formatted data from the request.
      -By default, a DefaultModelBinder class was used along with value providers that extracted data from the query string, form data, route values and even from the parsed JSON body.
  
  * In Core:
      -Not every binding source is checked by default. 
      Some model binders require you to specifically enable a binding source. 
      For example, when binding from the Body, you need to add [FromBody] to your model/action parameter, otherwise the BodyModelBinder won’t be used. 
      Same for binding from Header or from files
      -There are value providers for route values, query string values and form values. 
      Binders based on value providers can get data from any of these, but won’t be able to get data from other sources like the body for example. 
      Although form data is posted in the body as a URL-encoded string, it is a special case parsed and interpreted as a value provider by the framework.

Happy Custom Model Binding!

References

⬆ back to top