If you have used previous versions of the ASP.NET MVC framework, you might have probably come across the model binding process and even created a few custom model binders. Similarly those who have used ASP.NET WebAPI, know that the binding process is different from that of MVC.
This article is published from the DNC Magazine for Developers and Architects. Download or Subscribe to this Free magazine [PDF] to access all previous, current and upcoming editions.
With the new .NET Core, things have changed with both the MVC and WebApi merged into the one ASP.Net Core framework.
So how does model binding now work in the new Core framework?
Interestingly, it behaves closer to the previous WebAPI framework.
This means there are scenarios that will take you by surprise if you were accustomed to the previous MVC framework.
For e.g. Submitting a JSON within the request body is particularly different now when compared to the previous MVC framework and requires you to understand these changes.
In this article, I will describe how Model Binding in ASP.NET Core works, how does it compare against the previous versions of the framework and how you can customize it with your custom binders.
Model Binding in ASP.NET Core MVC
A look back at the old ASP.Net model binding process
In the previous version of MVC, the model binding process was based on model binders and value providers. 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.
Figure 1, a simplified view of the old MVC model binding
If you wanted to extend the default MVC behavior, you would either create and register a custom model binder, or a value provider.
Along came WebApi which shared with MVC its design philosophy and many of its ideas, but used a different pipeline.
The model binding process was one of the key differences between MVC and WebApi.
It was critical whether you were binding from the Url or the Body, as there were two separate model binding processes:
- Binding from the Url used a similar model binding process than the MVC one, based on ModelBinders and ValueProviders, adding type converters into the mix. (which convert strings into a given types)
- Binding from the body followed a different process which involved the added MediaTypeFormatters. These basically deserialized the request body into a type, and the specific formatter to be used was determined from the Content-Type header. Formatters like JsonMediaTypeFormatter and XmlMediaTypeFormatter were added, deserializing the body using Newtonsoft.Json, XmlSerializer or DataContractSerializer.
By default, simple types used the Url binding while complex types used the MediaTypeFormatters.
The framework also provided hooks for the user to be specific about which process should be used and to customize it, with the final set of rules described in its documentation.
Figure 2, a simplified view of the old WebApi parameter binding
The new ASP.NET Core Model Binding process
In ASP.Net Core, both WebApi and MVC have been merged into a single framework.
However, as you read in the previous section, they had different approaches to model binding. So, you might be wondering how does model binding work in ASP.Net Core? How did it merge both approaches?
The answer is that the new framework followed the direction started by WebApi, but has integrated the formatters within a unified binding process, based on model binders.
The formatters are still there, now abstracted behind the IInputFormatter interface, but these formatters are now used by specific model binders like the BodyModelBinder.
Figure 3, a simplified view of the model binding in ASP.Net Core
All of this means that there are different ways of binding your parameters and models. Depending on which model binder is selected, the framework might end up:
- using value providers and even other model binders for nested properties
- using an input formatter to deserialize the entire model from the body
Why should I care?
Knowing the model binding process followed behind the scenes is going to be critical for you to customize it.
People who are used to the binding process in the previous ASP.NET MVC framework, might get particularly surprised by the changes!
- 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.
- In particular, the Headers, Body and Files binding sources must be specifically enabled by the user and will use specific model binders.
- 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.
- Binding from the body anything other than URL-encoded form data, will always use a formatter instead of the value providers, with the entire model being deserialized from the body. No separated binding for each individual property will be attempted!
- A JSON formatter based on JSON.Net is added by default. All the JSON.Net hooks and options to customize how data is serialized/deserialized are available, like custom JsonConverters.
- Formatters for XML can be added, both XmlSerializer and DataContractSerializer are available.
The number of different model binders with its providers that are added by default is not a small list. You can customize that list adding/removing providers as you see fit through the MvcOptions.ModelBinderProviders.
The default list of binder providers might give you a better idea of how the binding process works. Bear in mind that providers are evaluated in order, the first one returning a non-null binder wins:
options.ModelBinderProviders.Add(new BinderTypeModelBinderProvider());
options.ModelBinderProviders.Add(new ServicesModelBinderProvider());
options.ModelBinderProviders.Add(new BodyModelBinderProvider(options.InputFormatters, _readerFactory, _loggerFactory));
options.ModelBinderProviders.Add(new HeaderModelBinderProvider());
options.ModelBinderProviders.Add(new SimpleTypeModelBinderProvider());
options.ModelBinderProviders.Add(new CancellationTokenModelBinderProvider());
options.ModelBinderProviders.Add(new ByteArrayModelBinderProvider());
options.ModelBinderProviders.Add(new FormFileModelBinderProvider());
options.ModelBinderProviders.Add(new FormCollectionModelBinderProvider());
options.ModelBinderProviders.Add(new KeyValuePairModelBinderProvider());
options.ModelBinderProviders.Add(new DictionaryModelBinderProvider());
options.ModelBinderProviders.Add(new ArrayModelBinderProvider());
options.ModelBinderProviders.Add(new CollectionModelBinderProvider());
options.ModelBinderProviders.Add(new ComplexTypeModelBinderProvider());
If you look carefully at the list of the binder provider names, you can infer that some binders will be selected for specific model types, while others will be selected for specific binding sources!
For example:
- BodyModelBinder, will be selected when [FromBody] is used, and will just use the request body through the InputFormatters.
- ComplexTypeModelBinder, will be selected when binding a complex type and no specific source like [FromBody] is used.
When you create a custom model binder, you need to think carefully:
- Whether you will bind from sources that have associated value providers (like query string or form data) or associated input formatters (like the body)
- Whether you intend your binder to be used with nested properties. For example, if you create a model binder for properties of type decimal, it will be used when binding a parent model from the Form data, but it won’t be used when binding a parent model from a JSON body. In the second case, the InputFormatter deserializes the entire model in one go.
The latter might take most people coming from MVC 5 by surprise and is the scenario we will go through in the following section.
Custom Model Binders
The scenario
Let’s go through a typical example used to demonstrate custom model binders in previous versions of the framework. This is the scenario where dateTime fields are provided as separated date and time properties, but you still want to bind them to a single dateTime field in your model.
Important note: I like this as an example for model binders because it is easy enough to understand and has already been used as an example in the past. However, I wouldn’t use this on a real system where I would just stick with timestamps or ISO Strings and use JavaScript to combine values from multiple editors.
Let’s say we have an appointment model like this:
public class Appointment
{
public string Id { get; set; }
public string Name { get; set; }
public DateTime AppointmentDate { get; set; }
}
When the following form is posted, we want our custom model binder to bind the separated date and time posted values into our Appointment.AppointmentDate property:
Figure 4, Form with date split in 2 fields
Following the same idea, if a JSON string like the following is posted, we also want the separated date and time values to be bound as the Appointment.AppointmentDate property:
{
"name":"Dummy Appointment",
"appointmentDate":{
"date":"10/03/2017",
"time":"11:34"
}
}
As discussed in the previous sections, we will need to handle both scenarios differently.
- In the case of the form data, we can create a custom model binder.
- However, in the second case where a JSON is posted, we will need to customize the way the JsonInputFormatter will deserialize that property.
If you have any trouble following along, you can download the code from Github.
Create a new Model Binder used with forms
In this scenario, we have a view where an HTML form is rendered and submitted to our controller. This form will contain separated inputs for the date and time of the AppointmentDate property.
The following code block shows the relevant parts of the view:
<div class="form-group">
<label asp-for="Name" class="col-md-2 control-label"></label>
<div class="col-md-10">
<input asp-for="Name" class="form-control" />
<span asp-validation-for="Name" class="text-danger"></span>
</div>
</div>
<div class="form-group">
<label asp-for="AppointmentDate" class="col-md-2 control-label"></label>
<div class="col-md-5">
<input asp-for="AppointmentDate" name="AppointmentDate.Date" class="form-control" value="@Model?.AppointmentDate.ToString("d")" />
</div>
<div class="col-md-5">
<input asp-for="AppointmentDate" name="AppointmentDate.Time" class="form-control" value="@Model?.AppointmentDate.ToString("t")" />
</div>
<span asp-validation-for="AppointmentDate" class="col-md-12 text-danger"></span>
</div>
Of course, this is accompanied by controller actions that render the view and handle the form being submitted:
public IActionResult CreateForm()
{
return View();
}
[HttpPost]
[ValidateAntiForgeryToken]
public async Task< IActionResult > CreateForm([Bind("Name,AppointmentDate")] Appointment appointment)
{
if (ModelState.IsValid)
{
appointment.Id = Guid.NewGuid().ToString();
_context.Add(appointment);
await _context.SaveChangesAsync();
return RedirectToAction("Index");
}
return View(appointment);
}
So far, this is all quite standard in any MVC application.
First attempt at our custom binder
Now we need to create a new custom model binder for our AppointmentDate property. Since this is intended for posted forms, we can use the value providers to retrieve the AppointDate.Date and AppointmentDate.Time form values.
- For the sake of simplicity, in the code that follows, I will force both date and time values to be present in the request data. While it suits the purposes of the article, in the real world, you most likely want them to be optional and proceed with the binding as long as one of the two values is found.
- A similar remark must be made about the handling of different time zones, formats and cultures. I am sticking with the invariant culture and UTC for the purposes of the article, but double check your use case and needs, in case you need to handle different ones.
The following shows a very rough version of this idea, without getting deep into different date/time formats and error handling, however it is enough for the purposes of the article and demonstrating how to use custom binders:
public class SplitDateTimeModelBinder: IModelBinder
{
public Task BindModelAsync(ModelBindingContext bindingContext)
{
// Make sure both Date and Time values are found in the Value Providers
// NOTE: You might not want to enforce both parts
var datePartName = $"{bindingContext.ModelName}.Date";
var timePartName = $"{bindingContext.ModelName}.Time";
var datePartValues = bindingContext.ValueProvider.GetValue(datePartName);
var timePartValues = bindingContext.ValueProvider.GetValue(timePartName);
if (datePartValues.Length == 0 || timePartValues.Length == 0) return Task.CompletedTask;
// Parse Date and Time
// NOTE: You might want a stronger/smarter handling of time zones, formats and cultures
DateTime.TryParseExact(
datePartValues.FirstValue,
"d",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var parsedDateValue);
DateTime.TryParseExact(
timePartValues.FirstValue,
"t",
CultureInfo.InvariantCulture,
DateTimeStyles.AdjustToUniversal,
out var parsedTimeValue);
// Combine into single DateTime which is the end result
var result = new DateTime(parsedDateValue.Year,
parsedDateValue.Month,
parsedDateValue.Day,
parsedTimeValue.Hour,
parsedTimeValue.Minute,
parsedTimeValue.Second);
bindingContext.ModelState.SetModelValue(bindingContext.ModelName, result, $"{datePartValues.FirstValue} {timePartValues.FirstValue}");
bindingContext.Result = ModelBindingResult.Success(result);
return Task.CompletedTask;
}
}
Now we need to tell the MVC framework when to use this model binder.
As we probably don’t want to use it for every DateTime, we can just add the [ModelBinder] attribute to our model:
public class Appointment
{
public string Id { get; set; }
public string Name { get; set; }
[ModelBinder(BinderType = typeof(SplitDateTimeModelBinder))]
public DateTime AppointmentDate { get; set; }
}
A better binder with fallback for regular date time values
The approach described above requires us to explicitly tell whether a DateTime field in a model will be posted as separated date and time values, so we can add the [ModelBinder] attribute.
Depending on your requirements, you might be more interested in registering a new model binder provider that will create an instance of our binder for any DateTime property, regardless of whether date and time are submitted as a single or separated fields.
Before using this approach, let’s rewrite the binder so we can use the default SimpleTypeModelBinder as a fallback in case the separated Date and Time values are not found.
This isn’t complicated, we just need to inject a fallback IModelBinder and use it when either Date or Time are missing.
The changed parts of the binder are displayed next:
public class SplitDateTimeModelBinder: IModelBinder
{
private readonly IModelBinder fallbackBinder;
public SplitDateTimeModelBinder(IModelBinder fallbackBinder)
{
this.fallbackBinder = fallbackBinder;
}
public Task BindModelAsync(ModelBindingContext bindingContext)
{
// Make sure both Date and Time values are found in the Value Providers
var datePartName = $"{bindingContext.ModelName}.Date";
var timePartName = $"{bindingContext.ModelName}.Time";
var datePartValues = bindingContext.ValueProvider.GetValue(datePartName);
var timePartValues = bindingContext.ValueProvider.GetValue(timePartName);
// Fallback to the default binder when a part is missing
if (datePartValues.Length == 0 || timePartValues.Length == 0) return fallbackBinder.BindModelAsync(bindingContext);
// Parse Date and Time. From this point onwards the binder is unchanged
Now let’s create a model binder provider that uses our updated binder for any DateTime field:
public class SplitDateTimeModelBinderProvider : IModelBinderProvider
{
private readonly IModelBinder binder =
new SplitDateTimeModelBinder(
new SimpleTypeModelBinder(typeof(DateTime)));
public IModelBinder GetBinder(ModelBinderProviderContext context)
{
return context.Metadata.ModelType == typeof(DateTime) ? binder : null;
}
}
Finally, let’s register the provider at the beginning of the list in Startup.ConfigureServices:
services.AddMvc(opts =>
{
opts.ModelBinderProviders.Insert(0, new SplitDateTimeModelBinderProvider());
});
That’s it, now our custom binding logic will be used when separated Date/Time values are found, while the default binding logic will be used otherwise.
If you check the code on Github, you can try the update view which uses a single form field for the AppointmentDate.
Testing our model binder with ajax requests
Let’s say we have to create a different controller endpoint we can use with Ajax requests or as a REST API:
[HttpPost]
public async Task< IActionResult > CreateJson(Appointment appointment)
{
if (ModelState.IsValid)
{
appointment.Id = Guid.NewGuid().ToString();
_context.Add(appointment);
await _context.SaveChangesAsync();
return Ok();
}
return StatusCode(400);
}
You might wonder what will happen if you just get the previous form and post it using an ajax call.
For example, let’s consider the following simple jQuery code that grabs all fields and posts the data:
$.ajax({
type: 'POST',
url: '/appointments/createjson',
data: data,
success: function (data, status, xhr) {
if (xhr.status == 200) { // go to appointments index }
}
});
// Where data can be one of these 2 options:
var data = $("#appointmentForm").serialize();
var data = {
name: $('input[name=Name]').val(),
appointmentDate: {
date: $('input[name="AppointmentDate.Date"]').val(),
time: $('input[name="AppointmentDate.Time"]').val()
}
};
The code above will send an ajax request with the Content-Type header set as application/x-www-form-urlencoded. This is critical for the model binding process, as those values will be available in the FormValueProvider and our custom binder will work as expected. Everything will behave the same as when the HTML form was submitted.
Now let’s change the code above to post a JSON and specify the Content-Type set as application/json:
var data = {
name: $('input[name=Name]').val(),
appointmentDate: {
date: $('input[name="AppointmentDate.Date"]').val(),
time: $('input[name="AppointmentDate.Time"]').val()
}
};
$.ajax({
type: 'POST',
url: '/appointments/createjson',
data: JSON.stringify(data),
contentType: 'application/json',
success: function (data, status, xhr) {
if (xhr.status == 200) window.location('/appointments');
}
});
You will notice that our model binder is not executed, in fact the model received by the controller action will be null!
The following section will shed some light on this scenario.
Updating how the posted JSON data is deserialized
When posting a JSON and specifying the Content-Type as application/json, the value providers will have no data available at all, as they only look at the Query string, route and form values!
In this case, we need to explicitly tell MVC that we want to bind our model from the request body where the posted JSON will be found.
Notice the usage of the [FromBody] attribute:
[HttpPost]
public async Task< IActionResult > CreateJson([FromBody]Appointment appointment)
{
…
}
However, by doing this we will be using the BodyModelBinder instead of the binder we got before (the ComplexModelBinder). This new binder will internally use the JsonInputFormatter to deserialize the JSON found in the request body into an instance of the Appointment class.
As the entire model will be deserialized, our custom DateTime binder will not be used!
So how can we customize this scenario, and deserialize the separated date and time parts into a single DateTime value?
Since the JsonInputFormatter uses JSON.Net, we can write a custom JsonConverter and inform JSON.Net about it!
Let’s start by creating a new converter that will verify that we have our separated date and time values, reading them both and creating the combined DateTime.
We will also specify that it should only be used when reading JSON:
public class SplitDateTimeJsonConverter : JsonConverter
{
public override bool CanWrite => false;
public override bool CanConvert(Type objectType)
{
return objectType == typeof(DateTime);
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// Make sure we have an object with date and time properties
// Example: { date: "01/01/2017", time: "11:35" }
if (reader.TokenType == JsonToken.Null) return null;
if (reader.TokenType != JsonToken.StartObject) return null;
var jObject = JObject.Load(reader);
if (jObject["date"] == null || jObject["time"] == null) return null;
// Extract and parse the separated date and time values
// NOTE: You might want a stronger/smarter handling of locales, formats and cultures
DateTime.TryParseExact(
jObject["date"].Value<string>(),
"d",
CultureInfo.InvariantCulture,
DateTimeStyles.None,
out var parsedDateValue);
DateTime.TryParseExact(
jObject["time"].Value<string>(),
"t",
CultureInfo.InvariantCulture,
DateTimeStyles.AssumeUniversal,
out var parsedTimeValue);
// Combine into single DateTime as the end result
return new DateTime(parsedDateValue.Year,
parsedDateValue.Month,
parsedDateValue.Day,
parsedTimeValue.Hour,
parsedTimeValue.Minute,
parsedTimeValue.Second);
}
public override void WriteJson(JsonWriter writer, object value, JsonSerializer serializer)
{
throw new NotImplementedException();
}
}
Then we can explicitly register it for our AppointmentDate property using the [JsonConverter] attribute.
public class Appointment
{
public string Id { get; set; }
public string Name { get; set; }
[JsonConverter(typeof(SplitDateTimeJsonConverter))]
public DateTime AppointmentDate { get; set; }
}
However, as we saw, when we created the custom model binder, you might prefer a single converter that we can register for all DateTime properties. Again, we will need to rewrite it so we have a fall back handler when no separated date/time values are provided. It is very similar to what we did with the custom binder, using a DateTimeConverterBase as fall back:
public class SplitDateTimeJsonConverter : JsonConverter
{
private readonly DateTimeConverterBase fallbackConverter;
public SplitDateTimeJsonConverter(DateTimeConverterBase fallbackConverter)
{
this.fallbackConverter = fallbackConverter;
}
public override object ReadJson(JsonReader reader, Type objectType, object existingValue, JsonSerializer serializer)
{
// Make sure we have an object with date and time properties
// Example: { date: "01/01/2017", time: "11:35" }
if (reader.TokenType == JsonToken.Null) return null;
if (reader.TokenType != JsonToken.StartObject)
return fallbackConverter.ReadJson(reader, objectType, existingValue, serializer);
var jObject = JObject.Load(reader);
if (jObject["date"] == null || jObject["time"] == null)
return fallbackConverter.ReadJson(reader, objectType, existingValue, serializer);
// No changes from this point onwards
}
Now instead of explicitly adding [JsonConverter] attributes to some DateTime fields, we can just register our converter in Startup.ConfigureServices:
services.AddMvc(opts =>
{
opts.ModelBinderProviders.Insert(0,
new SplitDateTimeModelBinderProvider());
}).AddJsonOptions(opts =>
{
opts.SerializerSettings.Converters.Add(
new SplitDateTimeJsonConverter(
new IsoDateTimeConverter()));
});
We are using the default IsoDateTimeConverter as fall back, you might need to set its format/culture properties or use a different fallback converter as per JSON.Net date handling.
Conclusion
The model binding process has changed significantly in ASP.Net Core, especially if you compare it against the binding process used in the previous versions of ASP.Net MVC. Those of you who are used to ASP.Net WebAPI, will find the new binding process closer to what you already know.
Now you need to be aware of the different binding sources, which ones of those are available by default (like the query string or form data binding sources) and which ones must be explicitly activated (like the body or header binding sources).
When customizing the model binding process, being aware of the different model binders and which binding sources these are used with, will be critical. Just creating and registering a new model binder will be enough as long as you plan using binding sources available as value providers (like query string or form data). However other binding sources like the body or the header will require different strategies and extra effort.
As an example, we have seen how to customize DateTime bindings when submitting forms and when posting JSON data. This required a new custom model binder and a new JsonConverter.
Download the entire source code of this article (Github).
This article was technically reviewed by Damir Arh and Suprotim Agarwal.
This article has been editorially reviewed by Suprotim Agarwal.
C# and .NET have been around for a very long time, but their constant growth means there’s always more to learn.
We at DotNetCurry are very excited to announce The Absolutely Awesome Book on C# and .NET. This is a 500 pages concise technical eBook available in PDF, ePub (iPad), and Mobi (Kindle).
Organized around concepts, this Book aims to provide a concise, yet solid foundation in C# and .NET, covering C# 6.0, C# 7.0 and .NET Core, with chapters on the latest .NET Core 3.0, .NET Standard and C# 8.0 (final release) too. Use these concepts to deepen your existing knowledge of C# and .NET, to have a solid grasp of the latest in C# and .NET OR to crack your next .NET Interview.
Click here to Explore the Table of Contents or Download Sample Chapters!
Was this article worth reading? Share it with fellow developers too. Thanks!
Daniel Jimenez Garciais a passionate software developer with 10+ years of experience who likes to share his knowledge and has been publishing articles since 2016. He started his career as a Microsoft developer focused mainly on .NET, C# and SQL Server. In the latter half of his career he worked on a broader set of technologies and platforms with a special interest for .NET Core, Node.js, Vue, Python, Docker and Kubernetes. You can
check out his repos.