What’s the Difference Between a Value Provider and Model Binder?

asp.net, asp.net mvc, code 0 comments suggest edit

ASP.NET MVC 3 introduced the ability to bind an incoming JSON request to an action method parameter, which is something I wrote about before.

For example, suppose you have the following class defined (keeping it really simple here):

public class ComicBook
{
  public string Title { get; set; }
  public int IssueNumber { get; set; }
}

And you have an action method that accepts an instance of ComicBook:

[HttpPost]
public ActionResult Update(ComicBook comicBook)
{
  // Do something with ComicBook and return an action result
}

You can easily post a comic book to that action method using JSON.

Under the hood, ASP.NET MVC uses the DefaultModelBinder in combination with the JsonValueProviderFactory to bind that value.

A question on an internal mailing list recently asked the question (and I’m paraphrasing here), “Why not cut out the middle man (the value provider) and simply deserialize the incoming JSON request directly to the model (ComicBook in this example)?”

Great question! Let me provide a bit of background to set the stage for the answer.

Posting Content to an Action

There are a couple of different content types you can use when posting data to an action method.

application/x-www-form-urlencoded

You may not realize it, but when you submit a typical HTML form, the content type of that submission is application/x-www-form-url-encoded.

As you can see in the screenshot below from Fiddler, the contents of the form is posted as a set of name value pairs separated by ampersand characters. The name and value within each pair are separated by an equals sign.

By the time you typically interact with this data (outside of model binding), it’s in the form of a dictionary like interface via the Request.Form name value collection.

The following screenshot shows what such a request looks like using Fiddler.

form-encoded-post

When content is posted in this format, the DefaultModelBinder calls into the FormValueProvider asking for a value for each property of the model. The FormValueProvider is a very thin abstraction over the Request.Form collection.

application/json

Another content type you can use to post data is application/json. As you might guess, this is simply JSON encoded data.

Here’s an example of a bit of JavaScript I used to post the same content as before but using JSON. Note that this particular snippet requires jQuery and a browser that natively supports the JSON.stringify method.

<script type="text/javascript">
    $(function() {
        var comicBook = { Title: "Groo", IssueNumber: 101 }
        var comicBookJSON = JSON.stringify(comicBook);
        $.ajax({
            url: '/home/update',
            type: 'POST',
            dataType: 'json',
            data: comicBookJSON,
            contentType: 'application/json; charset=utf-8',
        });
    });
</script>

When this code executes, the following request is created.

json-encoded-post

Notice that the content is encoded as JSON rather than form url encoded.

JSON is a serialization format so it’s in theory possible that we could straight deserialize that post to a ComicBook instance. Why don’t we do that? Wouldn’t it be more efficient?

To understand why, let’s suppose we did use serialization and walk through a common scenario. Suppose someone submits the form and they enter a string instead of a number for the field IssueNumber. You’d probably expect to see the following.

form-validation-error

Notice that the model binding was able to determine that the Title was submitted correctly, but that the IssueNumber was not.

If our model binder deserialized JSON into a ComicBook it would not be able to make that determination because serialization is an all or nothing affair. When serialization fails, all you know is that the format didn’t match the type. You don’t have access to the granular details we need to provide property level validation. So all you’d be able to show your users is an error message stating something went wrong, good luck figuring out what.

The Solution

Instead, what we really want is a way bind each property of the model one at a time so we can determine which of the fields are valid and which ones are in error. Fortunately, the DefaultModelBinder already knows how to do that when working with the dictionary-like IValueProvider interface.

So all we need to do is figure out how to expose the posted JSON encoded content via the IValueProvider interface. As I wrote before, Jonathan Carter had the bit of insight that provided the solution to this problem. He realized that you could have the JSON value provider deserialize the incoming JSON post to a dictionary. Once you have a dictionary, it’s pretty easy to implement IValueProvider and the DefaultModelBinder already knows how to bind those values to a type while providing property level validation. Score!

Value Provider Aggregation

The answer I provided only tells part of the story of why this is implemented as a value provider. There’s another aspect that was illustrated by my co-worker Levi. Sadly, for someone so gifted intellectually, he has no blog, so I’ll paraphrase his words here (with a bit of verbatim copying).

As I mentioned earlier, value providers provide an abstraction over where values actually come from. Value providers are responsible for aggregating the values that are part of the current request, e.g. from Form collection, the query string, JSON, etc.  They basically say “I don’t know what a ‘FirstName’ is for or what you can do with it, but if you ask me for a ‘FirstName’ I can give you what I have.”

Model binders are responsible for querying the value providers and building up objects based on those results.  They basically say “I don’t know where directly to find a ‘FirstName’, ‘LastName’, or ‘Age’, but if the value provider is willing to give them to me then I can create a Person object from them.”

Since model binders aren’t locked to individual sources (with some necessary exceptions, e.g. HttpPostedFile), they can build objects from an aggregate of sources. If your Person type looks like this:

public class Person
{
  int Id { get; set; }
  [NonNegative]
  int Age { get; set; }
  string FirstName { get; set; }
  string LastName { get; set; }
}

And a client makes a JSON POST request to an action method (say with the url /person/edit/1234 with the following content:

{ 
  "Age": 30, 
  "FirstName": "John", 
  "LastName": "Doe" 
} 

The DefaultModelBinder will pull the Id value from the RouteData and the Age, FirstName, and LastName values from the JSON when building up the Person object. Afterwards, it’ll perform validation without having to know that the various values came from different sources.

Even better, if you wrote a custom Person model binder and made it agnostic as to the current IValueProvider, you’d get the correct behavior on incoming JSON requests without having to change your model binder code one tiny iota.  Neither of these is possible if the model binder is hard-coded to a single provider.

TL;DR Summary

The goal of this post was to provide a bit of detail around an interesting aspect of how ASP.NET MVC turns strings sent to a web server into strongly typed objects passed into your action methods.

Going back to the original question, the answer is simply, we use a value provider for JSON to enable property level validation of the incoming post and also so that model binding can build up an object by aggregating multiple sources of data without having to know anything about those sources.

Found a typo or error? Suggest an edit! If accepted, your contribution is listed automatically here.

Comments

avatar

17 responses

  1. Avatar for Jim Davies
    Jim Davies June 30th, 2011

    Many thanks Phil - very clear and enlightening.
    Typo near the end? TL;DR Summary
    Best,
    Jim

  2. Avatar for Chris V.
    Chris V. June 30th, 2011

    This is a great post. I love getting perspective on implementation details like this.
    On a related note, one thing that's always bothered me about the default model binder code (though I admit, not enough for me to look at the source code and figure out a way around it), is that if you have an integer property to be bound, and the user submits a non-integer value, the error message returned seems to come from deep within the framework somewhere. This error message does not seem configurable via code because the error is thrown before the model-based validation ever actually occurs. Maybe you could shed some light on that one?

  3. Avatar for Gurdeep Singh
    Gurdeep Singh June 30th, 2011

    Very well explained. Thanks for the write up! Still wondering what if the browser do not support json.stringify natively? Resort back to plugins? Though, I myself use very little json for post backs and that too without the need of any stringifiers.

  4. Avatar for haacked
    haacked June 30th, 2011

    @Chris You are right. That error comes when we attempt type coercion. We're going to fix the fact that you can't configure that message in ASP.NET MVC 4.

  5. Avatar for haacked
    haacked June 30th, 2011

    @Jim TL;DR stands for Too Long, Didn't Read
    @Gurdeep You should probably use the json2.js library as described here. I was just being lazy in my blog post and demonstrating the principle, not the code I would use in production.

  6. Avatar for Anonymious
    Anonymious June 30th, 2011

    Not know whether you are aware about this bug present in http://bit.ly/lkAIyb, which will bypass ASP.NET Request Validation if you are using Json Value Provider.

  7. Avatar for av
    av July 2nd, 2011

    In FUBUMVC you wouldn't even have to worry about it.....
    Haha..just had to be the first one who FUBU'ed the post

  8. Avatar for Petar Repac
    Petar Repac July 2nd, 2011

    This is one of those aha!!! posts!
    Thank you for clear explanation.

  9. Avatar for Roatin Marth
    Roatin Marth July 3rd, 2011

    One problem with shoe-horning JSON request bodies into IValueProvider is that incoming enum values (given as ints) do not get bound to the target property. JSON requests get a DictionaryValueProvider&lt;object&gt;. The model binder asks the value provider to convert it's value to the enum type via valueProviderResult.ConvertTo(enumType)).
    The value provider gets an EnumConverter for the TypeConverter, but alas, when given an int object the EnumConverter doesn't know what to do with it. No other ValueProvider has this behaviour since they all store their raw values as string objects.

  10. Avatar for bill
    bill July 3rd, 2011

    So, with this ability, whats the value prop of WCF over MVC with json objects?

  11. Avatar for Mazhar
    Mazhar July 6th, 2011

    Aha!!! (same as Peter!)

  12. Avatar for Rais Hussain
    Rais Hussain July 6th, 2011

    @haacked Nice post and clear explanation of internals of ASP.NET MVC haacks!!!
    Thanks Phil.

  13. Avatar for ZyZ
    ZyZ July 6th, 2011

    @chris: You can configure those error messages, however it is more complicated than it should be
    1) When application starts register resource that will handle binding errors
    DefaultModelBinder.ResourceClassKey = "ResourceNameHere";
    2) Create "ResourceNameHere" resource in App_GlobalResources
    3) Resource should provide values for 2 keys "PropertyValueInvalid"
    and "PropertyValueRequired"

  14. Avatar for Aaron
    Aaron November 25th, 2011

    Just a bit of feedback on the Json Value Provider introduction. I need to use a different JSON parser ( Newtonsoft is now using the new SerializationBinder.BindToName which offers pretty clean polymorphic JSON data models ) than the one provided by MS and after a few hours of digging it turned out that the built-in JsonValueProviderFactory is what was internally not providing the HTTP POSTed JSON data to the alternate ModelBinder (Request.InputStream was empty) that I created to use my own JSON parser.
    Once I removed JsonValueProviderFactory from the ValueProviderFactories.Factories I was getting nice clean object bindings with my own DefaultModelBinder. I understand the seatbelt it is supposed to provide but in addition to the security issue that was also found (weblogs.asp.net/...) it may be worth an internal thread to discuss taking it out or making it in an opt-in.

  15. Avatar for Asif Ashraf
    Asif Ashraf January 30th, 2012

    Cool, fixed my issue. thanks

  16. Avatar for Mohit Dharmadhikari
    Mohit Dharmadhikari April 26th, 2015

    Excellent!! Just amazed with detailed explanation.

  17. Avatar for aledpardo
    aledpardo September 19th, 2015

    Phil, you rocks! Thanks for everything!