Passing multiple simple POST Values to ASP.NET Web API

Posted by Rick Strahl on West-Wind See other posts from West-Wind or by Rick Strahl
Published on Tue, 11 Sep 2012 11:27:00 GMT Indexed on 2012/09/11 9:38 UTC
Read the original article Hit count: 774

Filed under:
|

A few weeks backs I posted a blog post  about what does and doesn't work with ASP.NET Web API when it comes to POSTing data to a Web API controller. One of the features that doesn't work out of the box - somewhat unexpectedly -  is the ability to map POST form variables to simple parameters of a Web API method.

For example imagine you have this form and you want to post this data to a Web API end point like this via AJAX:

    <form>
        Name: <input type="name" name="name" value="Rick" />
        Value: <input type="value" name="value" value="12" />
        Entered: <input type="entered" name="entered" value="12/01/2011" />
        <input type="button" id="btnSend" value="Send" />
    </form>

    <script type="text/javascript">
    $("#btnSend").click( function() {                
        $.post("samples/PostMultipleSimpleValues?action=kazam",
               $("form").serialize(),
               function (result) {
                   alert(result);                   
               });
    });
    </script>

or you might do this more explicitly by creating a simple client map and specifying the POST values directly by hand:

$.post("samples/PostMultipleSimpleValues?action=kazam",
        { name: "Rick", value: 1, entered: "12/01/2012" },
        $("form").serialize(),
        function (result) {
            alert(result);                   
        });

On the wire this generates a simple POST request with Url Encoded values in the content:

POST /AspNetWebApi/samples/PostMultipleSimpleValues?action=kazam HTTP/1.1
Host: localhost
User-Agent: Mozilla/5.0 (Windows NT 6.2; WOW64; rv:15.0) Gecko/20100101 Firefox/15.0.1
Accept: application/json
Connection: keep-alive
Content-Type: application/x-www-form-urlencoded; charset=UTF-8
X-Requested-With: XMLHttpRequest
Referer: http://localhost/AspNetWebApi/FormPostTest.html
Content-Length: 41
Pragma: no-cache
Cache-Control: no-cache
name=Rick&value=12&entered=12%2F10%2F2011

Seems simple enough, right? We are basically posting 3 form variables and 1 query string value to the server.

Unfortunately Web API can't handle request out of the box. If I create a method like this:

[HttpPost]
public string PostMultipleSimpleValues(string name, int value, DateTime entered, string action = null)
{
    return string.Format("Name: {0}, Value: {1}, Date: {2}, Action: {3}", name, value, entered, action);
}
You'll find that you get an HTTP 404 error and
{
  "Message": "No HTTP resource was found that matches the request URI…"
}

Yes, it's possible to pass multiple POST parameters of course, but Web API expects you to use Model Binding for this - mapping the post parameters to a strongly typed .NET object, not to single parameters. Alternately you can also accept a FormDataCollection parameter on your API method to get a name value collection of all POSTed values. If you're using JSON only, using the dynamic JObject/JValue objects might also work.

ModelBinding is fine in many use cases, but can quickly become overkill if you only need to pass a couple of simple parameters to many methods. Especially in applications with many, many AJAX callbacks the 'parameter mapping type' per method signature can lead to serious class pollution in a project very quickly. Simple POST variables are also commonly used in AJAX applications to pass data to the server, even in many complex public APIs. So this is not an uncommon use case, and - maybe more so a behavior that I would have expected Web API to support natively. The question "Why aren't my POST parameters mapping to Web API method parameters" is already a frequent one…

So this is something that I think is fairly important, but unfortunately missing in the base Web API installation.

Creating a Custom Parameter Binder

Luckily Web API is greatly extensible and there's a way to create a custom Parameter Binding to provide this functionality! Although this solution took me a long while to find and then only with the help of some folks Microsoft (thanks Hong Mei!!!), it's not difficult to hook up in your own projects. It requires one small class and a GlobalConfiguration hookup.

Web API parameter bindings allow you to intercept processing of individual parameters - they deal with mapping parameters to the signature as well as converting the parameters to the actual values that are returned.

Here's the implementation of the SimplePostVariableParameterBinding class:

public class SimplePostVariableParameterBinding : HttpParameterBinding
{
    private const string MultipleBodyParameters = "MultipleBodyParameters";

    public SimplePostVariableParameterBinding(HttpParameterDescriptor descriptor)
        : base(descriptor)
    {
    }

    /// <summary>
    /// Check for simple binding parameters in POST data. Bind POST
    /// data as well as query string data
    /// </summary>
    public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
                                                HttpActionContext actionContext,
                                                CancellationToken cancellationToken)
    {
        // Body can only be read once, so read and cache it
        NameValueCollection col = TryReadBody(actionContext.Request);

        string stringValue = null;

        if (col != null)
            stringValue = col[Descriptor.ParameterName];

        // try reading query string if we have no POST/PUT match
        if (stringValue == null)
        {
            var query = actionContext.Request.GetQueryNameValuePairs();
            if (query != null)
            {
                var matches = query.Where(kv => kv.Key.ToLower() == Descriptor.ParameterName.ToLower());
                if (matches.Count() > 0)
                    stringValue = matches.First().Value;
            }
        }

        object value = StringToType(stringValue);

        // Set the binding result here
        SetValue(actionContext, value);

        // now, we can return a completed task with no result
        TaskCompletionSource<AsyncVoid> tcs = new TaskCompletionSource<AsyncVoid>();
        tcs.SetResult(default(AsyncVoid));
        return tcs.Task;
    }


    private object StringToType(string stringValue)
    {
        object value = null;

        if (stringValue == null)
            value = null;
        else if (Descriptor.ParameterType == typeof(string))
            value = stringValue;
        else if (Descriptor.ParameterType == typeof(int))
            value = int.Parse(stringValue, CultureInfo.CurrentCulture);
        else if (Descriptor.ParameterType == typeof(Int32))
            value = Int32.Parse(stringValue, CultureInfo.CurrentCulture);
        else if (Descriptor.ParameterType == typeof(Int64))
            value = Int64.Parse(stringValue, CultureInfo.CurrentCulture);
        else if (Descriptor.ParameterType == typeof(decimal))
            value = decimal.Parse(stringValue, CultureInfo.CurrentCulture);
        else if (Descriptor.ParameterType == typeof(double))
            value = double.Parse(stringValue, CultureInfo.CurrentCulture);
        else if (Descriptor.ParameterType == typeof(DateTime))
            value = DateTime.Parse(stringValue, CultureInfo.CurrentCulture);
        else if (Descriptor.ParameterType == typeof(bool))
        {
            value = false;
            if (stringValue == "true" || stringValue == "on" || stringValue == "1")
                value = true;
        }
        else
            value = stringValue;

        return value;
    }

    /// <summary>
    /// Read and cache the request body
    /// </summary>
    /// <param name="request"></param>
    /// <returns></returns>
    private NameValueCollection TryReadBody(HttpRequestMessage request)
    {
        object result = null;

        // try to read out of cache first
        if (!request.Properties.TryGetValue(MultipleBodyParameters, out result))
        {
            // parsing the string like firstname=Hongmei&lastname=Ge            
            result = request.Content.ReadAsFormDataAsync().Result;
            request.Properties.Add(MultipleBodyParameters, result);
        }

        return result as NameValueCollection;
    }

    private struct AsyncVoid
    {
    }
}

 

The ExecuteBindingAsync method is fired for each parameter that is mapped and sent for conversion. This custom binding is fired only if the incoming parameter is a simple type (that gets defined later when I hook up the binding), so this binding never fires on complex types or if the first type is not a simple type.

For the first parameter of a request the Binding first reads the request body into a NameValueCollection and caches that in the request.Properties collection. The request body can only be read once, so the first parameter request reads it and then caches it. Subsequent parameters then use the cached POST value collection. Once the form collection is available the value of the parameter is read, and the value is translated into the target type requested by the Descriptor. SetValue writes out the value to be mapped.

Once you have the ParameterBinding in place, the binding has to be assigned. This is done along with all other Web API configuration tasks at application startup in global.asax's Application_Start:

GlobalConfiguration.Configuration.ParameterBindingRules
    .Insert(0,
    (HttpParameterDescriptor descriptor) =>
    {
        var supportedMethods = descriptor.ActionDescriptor.SupportedHttpMethods;

        // Only apply this binder on POST and PUT operations
        if (supportedMethods.Contains(HttpMethod.Post) || 
            supportedMethods.Contains(HttpMethod.Put))
        {
            var supportedTypes = new Type[] { typeof(string), 
                                                typeof(int), 
                                                typeof(decimal), 
                                                typeof(double), 
                                                typeof(bool), 
                                                typeof(DateTime) 
                                            };

            if (supportedTypes.Where(typ => typ == descriptor.ParameterType).Count() > 0)
                return new SimplePostVariableParameterBinding(descriptor);
        }

        // let the default bindings do their work
        return null;

    });

 

The ParameterBindingRules.Insert method takes a delegate that checks which type of requests it should handle. The logic here checks whether the request is POST or PUT and whether the parameter type is a simple type that is supported. Web API calls this delegate once for each method signature it tries to map and the delegate returns null to indicate it's not handling this parameter, or it returns a new parameter binding instance - in this case the SimplePostVariableParameterBinding.

Once the parameter binding and this hook up code is in place, you can now pass simple POST values to methods with simple parameters. The examples I showed above should now work in addition to the standard bindings.

Summary

Clearly this is not easy to discover. I spent quite a bit of time digging through the Web API source trying to figure this out on my own without much luck. It took Hong Mei at Micrsoft to provide a base example as I asked around so I can't take credit for this solution :-). But once you know where to look, Web API is brilliantly extensible to make it relatively easy to customize the parameter behavior.

I'm very stoked that this got resolved  - in the last two months I've had two customers with projects that decided not to use Web API in AJAX heavy SPA applications because this POST variable mapping wasn't available. This might actually change their mind to still switch back and take advantage of the many great features in Web API. I too frequently use plain POST variables for communicating with server AJAX handlers and while I could have worked around this (with untyped JObject or the Form collection mostly), having proper POST to parameter mapping makes things much easier.

I said this in my last post on POST data and say it again here: I think POST to method parameter mapping should have been shipped in the box with Web API, because without knowing about this limitation the expectation is that simple POST variables map to parameters just like query string values do. I hope Microsoft considers including this type of functionality natively in the next version of Web API natively or at least as a built-in HttpParameterBinding that can be just added. This is especially true, since this binding doesn't affect existing bindings.

Resources

© Rick Strahl, West Wind Technologies, 2005-2012
Posted in Web Api  AJAX  

© West-Wind or respective owner

Related posts about Web Api

Related posts about AJAX