MVC Style parameter binding for WebAPI

I described earlier how WebAPI binds parameters. The entire parameter binding behavior is determined by the IActionValueBinder interface and can be swapped out. The default implementation is DefaultActionValueBinder.

Here’s another IActionValueBinder that provides MVC parameter binding semantics. This lets you do things that you can’t do in WebAPI’s default binder, specifically:

  1. ModelBinds everything, including the body. Assumes the body is FormUrl encoded
  2. This means you can do MVC scenarios where a complex type is bound with one field from the query string and one field from the form data in the body.
  3. Allows multiple parameters to be bound from the body.

 

Brief description of IActionValueBinder

Here’s what IActionValueBinder looks like:

     public interface IActionValueBinder
    {
        HttpActionBinding GetBinding(HttpActionDescriptor actionDescriptor);
    }

This is called to bind the parameters. It returns a  HttpActionBinding object, which is a 1:1 with an ActionDescriptor. It can be cached across requests. The interesting method on that binding object is:

     public virtual Task ExecuteBindingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)

This will execute the bindings for all the parameters, and signal the task when completed. This will invoke model binding, formatters, or any other parameter binding technique. The parameters are added to the actionContext’s parameter dictionary.

You can hook IActionValueBinder to provide your own binding object, which can have full control over binding the parameters. This is a bigger hammer than adding formatters or custom model binders.

You can hook up an IActionValueBinder either through the service resolver of the HttpControllerConfiguration attribute on a controller.

Example usage:

Here’s a an example usage. Suppose you have this code on the server. This is using the HttpControllerConfiguration attribute, and so all of the actions on that controller will use the binder. However, since it’s per-controller, that means it can still peacefully coexist with other controllers on the server.

     public class Customer
    {
        public string name { get; set; }
        public int age { get; set; }
    }

    [HttpControllerConfiguration(ActionValueBinder=typeof(MvcActionValueBinder))]
    public class MvcController : ApiController
    {
        [HttpGet]
        public void Combined(Customer item)
        {
        }
    }

And then here’s the client code to call that same action 3 times, showing the fields coming from different places.

         static void TestMvcController()
        {
            HttpConfiguration config = new HttpConfiguration();
            config.Routes.MapHttpRoute("Default", "{controller}/{action}", new { controller = "Home" });

            HttpServer server = new HttpServer(config);
            HttpClient client = new HttpClient(server);

            // Call the same action. Action has parameter with 2 fields. 

            // Get one field from URI, the other field from body
            {
                HttpRequestMessage request = new HttpRequestMessage
                {
                    Method = HttpMethod.Get,
                    RequestUri = new Uri("https://localhost:8080/Mvc/Combined?age=10"),
                    Content = FormUrlContent("name=Fred")
                };

                var response = client.SendAsync(request).Result;
            }

            // Get both fields from the body
            {
                HttpRequestMessage request = new HttpRequestMessage
                {
                    Method = HttpMethod.Get,
                    RequestUri = new Uri("https://localhost:8080/Mvc/Combined"),
                    Content = FormUrlContent("name=Fred&age=11")
                };

                var response = client.SendAsync(request).Result;
            }

            // Get both fields from the URI
            {
                var response = client.GetAsync("https://localhost:8080/Mvc/Combined?name=Bob&age=20").Result;
            }
        }
         static HttpContent FormUrlContent(string content)
        {
            return new StringContent(content, Encoding.UTF8, "application/x-www-form-urlencoded");
        }

 

The MvcActionValueBinder:

Here’s the actual code for the binder. Under 100 lines.  (Disclaimer: this requires the latest sources. I verified against this change. I had to fix an issue that allowed ValueProviderFactory.GetValueProvider to return null).

Notice that it reads the body once per request, creates a per-request ValueProvider around the form data, and stashes that in request-local-storage so that all of the parameters share the same value provider. This sharing is essential because the body can only be read once.

 // Example of MVC-style action value binder.
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Globalization;
using System.Linq;
using System.Net.Http;
using System.Net.Http.Formatting;
using System.Threading;
using System.Threading.Tasks;
using System.Web.Http;
using System.Web.Http.Controllers;
using System.Web.Http.ModelBinding;
using System.Web.Http.ValueProviders;
using System.Web.Http.ValueProviders.Providers;

namespace Basic
{    
    // Binder with MVC semantics. Treat the body as KeyValue pairs and model bind it. 
    public class MvcActionValueBinder : DefaultActionValueBinder
    {
        // Per-request storage, uses the Request.Properties bag. We need a unique key into the bag. 
        private const string Key = "5DC187FB-BFA0-462A-AB93-9E8036871EC8";

        public override HttpActionBinding GetBinding(HttpActionDescriptor actionDescriptor)
        {
            MvcActionBinding actionBinding = new MvcActionBinding();
                                    
            HttpParameterDescriptor[] parameters = actionDescriptor.GetParameters().ToArray();
            HttpParameterBinding[] binders = Array.ConvertAll(parameters, p => DetermineBinding(actionBinding, p));

            actionBinding.ParameterBindings = binders;
                        
            return actionBinding;            
        }

        private HttpParameterBinding DetermineBinding(MvcActionBinding actionBinding, HttpParameterDescriptor parameter)
        {
            HttpConfiguration config = parameter.Configuration;

            var attr = new ModelBinderAttribute(); // use default settings
            
            ModelBinderProvider provider = attr.GetModelBinderProvider(config);
            IModelBinder binder = provider.GetBinder(config, parameter.ParameterType);

            // Alternatively, we could put this ValueProviderFactory in the global config.
            List<ValueProviderFactory> vpfs = new List<ValueProviderFactory>(attr.GetValueProviderFactories(config));
            vpfs.Add(new BodyValueProviderFactory());

            return new ModelBinderParameterBinding(parameter, binder, vpfs);
        }   

        // Derive from ActionBinding so that we have a chance to read the body once and then share that with all the parameters.
        private class MvcActionBinding : HttpActionBinding
        {                
            // Read the body upfront , add as a ValueProvider
            public override Task ExecuteBindingAsync(HttpActionContext actionContext, CancellationToken cancellationToken)
            {
                HttpRequestMessage request = actionContext.ControllerContext.Request;
                HttpContent content = request.Content;
                if (content != null)
                {
                    FormDataCollection fd = content.ReadAsAsync<FormDataCollection>().Result;
                    if (fd != null)
                    {
                        NameValueCollection nvc = fd.ReadAsNameValueCollection();

                        IValueProvider vp = new NameValueCollectionValueProvider(nvc, CultureInfo.InvariantCulture);

                        request.Properties.Add(Key, vp);
                    }
                }
                        
                return base.ExecuteBindingAsync(actionContext, cancellationToken);
            }
        }

        // Get a value provider over the body. This can be shared by all parameters. 
        // This gets the values computed in MvcActionBinding.
        private class BodyValueProviderFactory : ValueProviderFactory
        {
            public override IValueProvider GetValueProvider(HttpActionContext actionContext)
            {
                object vp;
                actionContext.Request.Properties.TryGetValue(Key, out vp);
                return (IValueProvider)vp; // can be null                
            }
        }
    }
}

--