Updated hybrid model binding in ASP.NET Core 1.0 RC2

Why being greedy by default was the wrong approach.

 June 19, 2016


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.

Why is greedy binding a problem?

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; }
}

YOU HAVE JUST CREATED AN ADMIN!

Bill Paxton as Hudson from Aliens commenting on their dire situation. Ref. https://memegenerator.net/instance/68941874

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.

Attributes to the rescue!

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; }
}

But, I like the old behavior 😦

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.

How do I get this update?

PM> Install-Package HybridModelBinding -Version 0.2.0

0.2.0 is the current version as of this writing.

Upcoming content

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.