Web API – Binding Complex types from the URI by default for HTTP GET requests

The model binding implementation in ASP.NET Web Api shares some similarities to the model binding you find in ASP.NET MVC and at the same time has some striking differences. For a more detailed discussion on web api parameter binding, check out Mike Stall’s excellent post on the topic. The topic of today’s post is how we define our own convention for parameter binding complex types when the request is a HTTP GET request.

Body Check

The IActionValueBinder interface is the contract for getting the binding information for a web api controller action

// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information.

namespace System.Web.Http.Controllers
{
    public interface IActionValueBinder
    {
        HttpActionBinding GetBinding(HttpActionDescriptor actionDescriptor);
    }
}

and DefaultActionValueBinder is the default implementation of IActionValueBinder within a web api application. The route it takes to determine where model binding should attempt to bind values for a parameter action is as follows:

  1. Does the parameter type have a ParameterBindingAttribute? If so, use this for binding
  2. Look in the ParameterBindingRulesCollection on the HttpConfigurationfor the application. Is there a binding specified for the parameter in the collection? If so, use this for binding
  3. Look at the parameter type. Is it a simple type i.e. a primitive, string, DateTime, Decimal, Guid, DateTimeOffset or TimeSpan (or an applicable Nullable version of these)? If so, look at the URI for binding
  4. Does the parameter type have a string converter defined for it? If so, look at the URI for binding
  5. Default to looking in the request body for binding

 

The fallback option of looking in the request body seems like an odd action to take for a HTTP GET request, with some much better well informed suggesting that a body has no semantic meaning for a HTTP GET request

Yes. In other words, any HTTP request message is allowed to containa message body, and thus must parse messages with that in mind.Server semantics for GET, however, are restricted such that a body,if any, has no semantic meaning to the request. The requirementson parsing are separate from the requirements on method semantics.So, yes, you can send a body with GET, and no, it is never usefulto do so.This is part of the layered design of HTTP/1.1 that will becomeclear again once the spec is partitioned (work in progress).....Roy

Purism aside, I’d like model binding to default to looking at the URI for GET requests when an binding attribute or rule is not specified for the parameter. It’s actually straightforward to do by leveraging the DefaultActionValueBinder and overriding the GetParameterBinding method

public class CustomActionValueBinder : DefaultActionValueBinder
{
    protected override HttpParameterBinding GetParameterBinding(HttpParameterDescriptor parameter)
    {
        // Attribute has the highest precedence
        // Presence of a model binder attribute overrides.
        var bindingAttribute = parameter.ParameterBinderAttribute;
        if (bindingAttribute != null)
        {
            return bindingAttribute.GetBinding(parameter);
        }

        // No attribute, so lookup in global map.
        var parameterBindingRules = parameter.Configuration.ParameterBindingRules;
        if (parameterBindingRules != null)
        {
            var binding = parameterBindingRules.LookupBinding(parameter);
            if (binding != null)
            {
                return binding;
            }
        }

        // if we're dealing with a method that supports HTTP Gets, Assume the model is in the URI
        // otherwise fallback to the defaults.
        return parameter.ActionDescriptor.SupportedHttpMethods.Contains(HttpMethod.Get)
                    ? parameter.BindWithAttribute(new FromUriAttribute())
                    : base.GetParameterBinding(parameter);
    }
}

To hook this up for the application, we need to replace the default IActionValueBinder registered. This can be done in Application_Start in global.asax

public class WebApiApplication : System.Web.HttpApplication
{
    protected void Application_Start()
    {
        // replace the default IActionValueBinder
        GlobalConfiguration.Configuration.Services.Replace(typeof(IActionValueBinder), new CustomActionValueBinder());
    }
}

Alternatively, if you’re using an Inversion of Control Container for wiring up dependencies then you simply need to register the IActionValueBinder with your container to allow your IDependencyResolver to resolve and use it.

Comments

comments powered by Disqus