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:
-
The client has many avenues of sending data to your backend:
- Form data
- Http Request body
- URI (route values and query parameters)
-
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!
The 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...
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!
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
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).
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.
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.
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 fullPerson
. If you only bindFirstName
and leave the rest unset, they will be null.
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!