A few weeks backs I wrote about what does and doesn't work With POST values in ASP.NET Web API when it comes to sending 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" },
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 this 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.
Model Binding 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 at Microsoft (thanks Hong Mei!!!), it's not difficult to hook up in your own projects once you know what to implement. 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:
(updated 9/24/2012: Fixed bug with non-form data trying to read query string values)
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>
/// <param name="metadataProvider"></param>
/// <param name="actionContext"></param>
/// <param name="cancellationToken"></param>
/// <returns></returns>
public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
HttpActionContext actionContext,
CancellationToken cancellationToken)
{
string stringValue = null;
NameValueCollection col = TryReadBody(actionContext.Request);
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;
}
/// <summary>
/// Method that implements parameter binding hookup to the global configuration object's
/// ParameterBindingRules collection delegate.
///
/// This routine filters based on POST/PUT method status and simple parameter
/// types.
/// </summary>
/// <example>
/// GlobalConfiguration.Configuration.
/// .ParameterBindingRules
/// .Insert(0,SimplePostVariableParameterBinding.HookupParameterBinding);
/// </example>
/// <param name="descriptor"></param>
/// <returns></returns>
public static HttpParameterBinding HookupParameterBinding(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),
typeof(byte[])
};
if (supportedTypes.Where(typ => typ == descriptor.ParameterType).Count() > 0)
return new SimplePostVariableParameterBinding(descriptor);
}
return null;
}
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))
{
var contentType = request.Content.Headers.ContentType;
// only read if there's content and it's form data
if (contentType == null || contentType.MediaType != "application/x-www-form-urlencoded")
{
// Nope no data
result = null;
}
else
{
// parsing the string like firstname=Hongmei&lastname=ASDASD
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:
// Attach simple post variable binding
GlobalConfiguration.Configuration.
.ParameterBindingRules
.Insert(0,SimplePostVariableParameterBinding.HookupParameterBinding);
The hookup code calls the static HookupParameterBinding method of the SimplePostVariableParameterBinding class that provides the delegate that the Insert method requires. The delegate's job is to check which type of requests the ParameterBinding should handle. The logic in HookupParameterBinding checks whether the request is POST or PUT and whether the parameter type is one of the simple types that is supported. Web API calls this delegate once for each method signature it tries to map and the function returns null to indicate it's not handling this parameter, or it returns a new parameter binding instance - in this case the SimplePostVariableParameterBinding.
As a consumer of this class the one line above is all you need!
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 from Microsoft to provide a base example, 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 custom parameter binding in Web API 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 work around this (with untyped JObject or the Form collections mostly), I prefer in many cases to push simple POST variables to the server - it's simply more convenient and more logical in many situations.
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! Without knowing about this limitation and seeing that query string values match, the expectation is that simple POST variables should also map to parameters. 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. Especially given that this binding doesn't adversely affect existing bindings or even their performance.
Resources
Other Posts you might also like