Updated hybrid model binding in ASP.NET Core 1.0 RC2
Why being greedy by default was the wrong approach.
In my previous introductory post on hybrid model binding, I demonstrated how a binder could be used with both body and URI content. After receiving feedback (thanks @buhakmeh), it became apparent being greedy by default was not the best option and would most-likely hinder adoption of the binder by the community.
Imagine posting this content:
curl -X POST -H "Accept: application/json" -H "Content-Type: application/json" -d '{
"name": "Bill Boga"
}' "https://localhost/people?isAdmin=true"
note the isAdmin
in the query string
to this action:
[HttpPost]
[Route("people")]
public IActionResult Create(Person model)
{ }
where Person
is also your domain model:
using Newtonsoft.Json;
public class Person
{
[JsonIgnore]
public bool IsAdmin {get; set; }
public string Name { get; set; }
}
But this would still happen if we just included the
isAdmin
property in our body, right?
No, since the property is decorated with JsonIgnore
, the value does not get deserialized when binding from the body. By having additional binding outside the body-binder, we can get unexpected results. This concept is especially important with team development since all developers may not be familiar with the consequences on hybrid binding.
HybridModelBinder
has been revamped and no longer directly exposes the binder and provider collections. And, each IValueProviderFactory
is matched with an Attribute
. There is no special requirement for the attribute although sane defaults were pulled from the Microsoft.AspNetCore.Mvc
namespace.
public HybridModelBinder AddMappedValueProviderFactory<TAttribute>(IValueProviderFactory factory)
where TAttribute : Attribute
Here is the new DefaultHybridModelBinder
:
public class DefaultHybridModelBinder : HybridModelBinder
{
public DefaultHybridModelBinder(IHttpRequestStreamReaderFactory readerFactory)
{
AddModelBinder(new BodyModelBinder(readerFactory))
.AddMappedValueProviderFactory<FromFormAttribute>(new FormValueProviderFactory())
.AddMappedValueProviderFactory<FromRouteAttribute>(new RouteValueProviderFactory())
.AddMappedValueProviderFactory<FromQueryAttribute>(new QueryStringValueProviderFactory());
}
}
So, if we want to bind using query string values, we need to update our model:
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
public class Person
{
[JsonIgnore]
[FromQuery]
public bool IsAdmin {get; set; }
public string Name { get; set; }
}
We still have the ability to create ordered binding. In this next example, a matching route-value is bound first and potentially overwritten by a query string value:
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;
public class Person
{
[JsonIgnore]
[FromRoute]
[FromQuery]
public bool IsAdmin {get; set; }
public string Name { get; set; }
}
No worries. There is a matching greedy provider and binder for those who want to stick with the original behavior:
public DefaultGreedyHybridModelBinder(IHttpRequestStreamReaderFactory readerFactory)
:base(isGreedy: true)
{
AddModelBinder(new BodyModelBinder(readerFactory))
.AddMappedValueProviderFactory(new FormValueProviderFactory())
.AddMappedValueProviderFactory(new RouteValueProviderFactory())
.AddMappedValueProviderFactory(new QueryStringValueProviderFactory());
}
By specifying our binder is greedy, we no longer have to provide provider/attribute mapping. In fact, calling the previous example's methods (non-generic) on a non-greedy binder will cause a MethodAccess
exception.
PM> Install-Package HybridModelBinding -Version 0.2.0
0.2.0
is the current version as of this writing.
I still plan to write at-least a couple more posts in the upcoming weeks going into more detail how these new internal processes work and look forward to more feedback from the community.