ASP.NET Web API parameter binding part 1 – Understanding binding from URI

Today, let’s kick off a series intended to look at different aspects of HTTP parameter binding in ASP.NET Web API. Why? Aside from the awesome series by Mike Stall, there isn’t really that much material on the web on this particular subject. And developers coming from MVC background, often get surprised by differences in the model binding mechanism between MVC and Web API.

In this first post, let’s have a brief overview of parameter binding in Web API and then specifically look at binding model from URI.

More after the jump.

Model binding with MVC

In MVC, the model binding process is done against both the body and the URI. In other words, the framework will look for model properties in both places, and try to stitch everything together. This is possible because MVC would buffer the body of the request and that would allow it to pick pieces of key value pairs (effectively, each value in the request is part of the name value collection) and try to compose models out of them.

Consider the simple example.

In MVC you can send the following request:

This will correctly bind itself to an action with this signature:

You can also easily bind the whole model just from the URI, which is particularly useful on GET requests where you might be passing in a complex sets of conditions. Conversely, you can bind the entire model just from the body too.

HttpParameterBinding

In Web API, the core concept behind binding parameters is HttpParameterBinding, which, out of the box, can execute either as a model binder, or through the use of a formatter.

In practice, this means that when Web API pipeline runs, a default implementation of IActionValueBinder will determine whether each specific parameter will be bound using the URI (model binding) or request’s body (formatter). To be even more precise, this process will not happen on every request, because in Web API binding can be statically determined for each parameter. Therefore, it will really happen just once – through a call to ApiControllerActionSelector, which will cache the resulting ActionDescriptor inside a private ActionSelectorCacheItem class. The subsequent requests will then be fed from that cache.

We’ll look at what happens when the formatter type of binding is selected in the next post. For now let’s focus on what happens when Web API decides to use ModelBinder and bind from URI.

Web API will look for any ValueProviderFactories registered against the Services property of HttpConfiguration.

A factory returns IValueProvider, which can be used to compose an object from the HTTP request using the most basic building block – a string key/value present in the HTTP request. The main role of ValueProviders is to abstract away the logic of retrieving the information from the HTTP request from ModelBinders and just feed them with individual bits used to compose more complex objects.

By default, Web API ships with two registered factories that take part in the URI parameter bindings:

  • QueryStringValueProviderFactory
  • RouteDataValueProviderFactory

And their names & roles are rather self explanatory.

On a side note, it’s worth mentioning, that if the split into model binders / formatters is not enough for you, you can also introduce your own versions of HttpParameterBinding – and we’ll do that in the later posts in this series.

Understanding Web API parameter binding

So what happens if you just stick some parameters into your action? How does IActionValueBinder determine whether to use a formatter or a model binder?

The generic rules are:

  • – simple, string-convertible parameters (value types, strings, Guids, DateTimes and so on) are by default read from URI
  • – complex types are by default read from the body
  • – collections of simple parameters are by default read from the body too
  • – you cannot compose a single model based on input from both URI and request body, it has to be one or the other

While some of these default conventions may seem limiting and unintuitive, there are just enough customization hooks to allow you as a developer to override them.

Binding from URI

In order to bind a model (an action parameter), that would normally default to a formatter, from the URI you need to decorate it with either [ModelBinder] or [FromUri] attribute.

FromUriAttribute simply inherits from ModelBinderAttribute, providing you a shortcut directive to instruct Web API to grab specific parameters from the URI using the ValueProviders defined in the IUriValueProviderFactory. The attribute itself is sealed and cannot be extended any further, but you add as many custom IUriValueProviderFactories as you wish.

Mike Stall has a great post on creating a custom value provider for Web API.

Let’s use a ProductFilter as an example:

Just through the use of [FromUri] you are able to bind the following types entirely from the URI:

1) complex types, such as our ProductFilter

This will bind correctly to the type we declared, leaving other properties as null (since we explicitly declared them as nullable).

2) collections, such as *List*

3) key value pairs

4) apparently it also works with dictionaries, but I honestly don’t know how :)

5) anything else, as long as you create a custom ModelBinder or a custom ValueProvider that would do the appropriate type coercion from the string

[ModelBinder], allows you to plug in your own logic of determining how a set of data fed from ValueProviders should be converted into a CLR type of your choice.

A good example might be:

In this example you may want to bind sizes to an IEnumerable or Array of strings. There is nothing in the box in Web API for this particular scenario, so you’d need a custom [ModelBinder].

In the above example it would be:

And can now be used:

Notice we used the default ValueProviders and just split the string extracted by the provider in the ModelBinder itself. You could potentially split the string inside a custom value provider too.

Defaulting to [FromUri] for GET and HEAD requests

Web API will always try to eagerly bind non-primitive and non-string covnertible types from the body of the request. While in many cases that’s all right, semantically, it doesn’t make much sense for GET and HEAD requests, since in accordance to the HTTP specification, these are body-less requests.

The decision whether a parameter should be bound from URI or body happens inside the DefaultActionValueBinder (the default implementation of the aforementioned IActionValueBinder), which, as almost every infrastructure piece in Web API, is extensible and overridable.

Going back to our previous example, we’d like the GET request to be written without a necessity for [FromUri].

To achieve this we can introduce a small custom action value binder, extending the default one:

With this trivial change, we tell Web API that if a given action is GET/HEAD type of action, always bind as if [FromUri] was present, otherwise, we let the base logic flow.

Using MVC style binding with Web API

If you feel that MVC style binding is something you’d need in your Web API, you can achieve it by incorporating a terrific MvcActionValueBinder by Mike Stall into your application. It is also now part of the WebApiContrib project, so can be instantly pulled from Nuget.

To enable it, simply install WebApiContrib and register it against your Web API configuration:

And:

The binding is also applicable via an attribute so you can add it to your controller accordingly:

without having to register against the global HttpConfiguration.

What’s next

In the next post of this series we’ll look at binding from the body, and after that implement customized HttpParameterBinding.

Be Sociable, Share!

  • Pingback: The Morning Brew - Chris Alcock » The Morning Brew #1338()

  • Pingback: Dew Drop – April 18, 2013 (#1,530) | Alvin Ashcraft's Morning Dew()

  • Pingback: Dew Drop – April 19, 2013 (#1,531) | Alvin Ashcraft's Morning Dew()

  • Pingback: Scott Banwart's Blog › Distributed Weekly 203()

  • Roel Ang

    Great post!

    I have this mind boggling issue about IModelBinder you presented here. It works in another app but in this particular app its doesn’t. I tried to debug it and put a breakpoint but it doesn’t step into the class. Looks like something is preventing it from working/invoking.

    Hope theres something you may be able to share.

    Thanks
    Roel

  • couten

    What happened to part 2 and 3?

    • http://www.strathweb.com/ Filip W

      oh, I have forgotten about that :)

  • JasonMing

    “4) apparently it also works with dictionaries, but I honestly don’t know how ”

    I tried, it can’t bind to dictionaries by FromUri attribute, seems caused by routing mechanism, the binder alway cannot find the correct parameters from query string which names like the parameter.
    To bind to a dictionary, it seems no way unless you wrote a custom ParameterBindingAttribute.

  • Md.Ibrahim

    I have a question regarding this article posted here: http://stackoverflow.com/questions/24844830/web-api-modelbinding-from-uri
    Could you please take a look at it?

  • http://tuncaliku.wordpress.com/ Tunç Ali Kütükçüoğlu

    Hello Filip, does Web Api handle parameter binding for 2 and multi dimensional arrays out of the box? I directed the same question to stackoverflow:
    http://stackoverflow.com/questions/24957896/does-web-api-support-two-and-multi-dimensional-arrays-for-serialization-and-para

  • Pingback: Web API 2 Exploring Parameter Binding | Software Engineering()

  • Pingback: Optional Parameters into a POST ask in .NET? - Abramek()

  • http://About.me/Nilesh_Moradiya Nilesh Mordiya

    We can create one action filter for input
    like i had done for array input.

    ///

    /// Transform array input from url to action parameter

    ///

    public class ArrayInputAttribute : ActionFilterAttribute

    {

    ///

    ///

    ///

    public char Separator { get; set; }

    ///

    /// Data type

    ///

    public TypeCode Type { get; set; }

    private readonly string _parameterName;

    ///

    /// Constructor

    ///

    /// Parameter name

    public ArrayInputAttribute(string parameterName)

    {

    _parameterName = parameterName;

    Type = TypeCode.String;

    }

    ///

    /// Called before any action execute

    ///

    ///

    public override void OnActionExecuting(HttpActionContext actionContext)

    {

    if (actionContext.ActionArguments.ContainsKey(_parameterName))

    {

    string parametersAttemptedValue = string.Empty;

    if (actionContext.ControllerContext.RouteData.Values.ContainsKey(_parameterName))

    parametersAttemptedValue = (string)actionContext.ControllerContext.RouteData.Values[_parameterName];

    else if (actionContext.ControllerContext.Request.RequestUri.ParseQueryString()[_parameterName] != null)

    parametersAttemptedValue = actionContext.ControllerContext.Request.RequestUri.ParseQueryString()[_parameterName];

    switch (Type)

    {

    case TypeCode.Empty:

    throw new NotImplementedException();

    case TypeCode.Object:

    throw new NotImplementedException();

    case TypeCode.DBNull:

    throw new NotImplementedException();

    case TypeCode.Boolean:

    throw new NotImplementedException();

    case TypeCode.Char:

    throw new NotImplementedException();

    case TypeCode.SByte:

    throw new NotImplementedException();

    case TypeCode.Byte:

    throw new NotImplementedException();

    case TypeCode.Int16:

    actionContext.ActionArguments[_parameterName] = string.IsNullOrWhiteSpace(parametersAttemptedValue) ? new int[] { } : parametersAttemptedValue.Split(Separator).Select(int.Parse).ToArray();

    break;

    case TypeCode.UInt16:

    throw new NotImplementedException();

    case TypeCode.Int32:

    actionContext.ActionArguments[_parameterName] = string.IsNullOrWhiteSpace(parametersAttemptedValue) ? new int[] { } : parametersAttemptedValue.Split(Separator).Select(int.Parse).ToArray();

    break;

    case TypeCode.UInt32:

    throw new NotImplementedException();

    case TypeCode.Int64:

    actionContext.ActionArguments[_parameterName] = string.IsNullOrWhiteSpace(parametersAttemptedValue) ? new int[] { } : parametersAttemptedValue.Split(Separator).Select(int.Parse).ToArray();

    break;

    case TypeCode.UInt64:

    throw new NotImplementedException();

    case TypeCode.Single:

    throw new NotImplementedException();

    case TypeCode.Double:

    throw new NotImplementedException();

    case TypeCode.Decimal:

    throw new NotImplementedException();

    case TypeCode.DateTime:

    throw new NotImplementedException();

    case TypeCode.String:

    actionContext.ActionArguments[_parameterName] = string.IsNullOrWhiteSpace(parametersAttemptedValue) ? new string[] { } : parametersAttemptedValue.Split(Separator).ToArray();

    break;

    default:

    throw new ArgumentOutOfRangeException();

    }

    }

    }

    }