Rick Strahl's Web Log

Wind, waves, code and everything in between...
ASP.NET • C# • HTML5 • JavaScript • AngularJs
Contact   •   Articles   •   Products   •   Support   •   Search
Ad-free experience sponsored by:
ASPOSE - the market leader of .NET and Java APIs for file formats – natively work with DOCX, XLSX, PPT, PDF, images and more

Passing multiple simple POST Values to ASP.NET Web API


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

Posted in Web Api  AJAX  

The Voices of Reason


 

Tyrone
September 11, 2012

# re: Passing multiple simple POST Values to ASP.NET Web API

So, since the Web API is open-source, you should be able to fork the repo and submit this as a patch correct?

Chad
September 11, 2012

# re: Passing multiple simple POST Values to ASP.NET Web API

Agree with Tyrone - please submit this as a pull request.

Mike Gale
September 12, 2012

# re: Passing multiple simple POST Values to ASP.NET Web API

Thanks very much for that. I'll give it a test (after rejecting it when I thought POST was impossible).

K
September 21, 2012

# re: Passing multiple simple POST Values to ASP.NET Web API

I get the below error when posting to the action.

{"No MediaTypeFormatter is available to read an object of type 'FormDataCollection' from content with media type 'application/json'."} at this line "request.Content.ReadAsFormDataAsync().Result;"
Any suggestions??

Rick Strahl
September 21, 2012

# re: Passing multiple simple POST Values to ASP.NET Web API

This has nothing to do with this parameter binding. You can't accept a FormDataCollection when the content you're sending is JSON. FormDataCollection only works with posted form data.

K
September 24, 2012

# re: Passing multiple simple POST Values to ASP.NET Web API

I have multiple API Controllers. Using you suggestion from above, i was able to post multiple values successfully.
But ,Some other actions take a complex type as parameter and i pass JSON data using knockout and jquery ajax. This was working fine before , but once i hookup "SimplePostVariableParameterBinding", in my global configuration, this call fails with the error i posted earlier.
How i can ignore "SimplePostVariableParameterBinding" for JSON data? Please suggest.

Rick Strahl
September 25, 2012

# re: Passing multiple simple POST Values to ASP.NET Web API

Can you tell me what the issue is exactly? What does the controller look like? When you pass a complex object that object won't hit the parameter binder. However, I think there's a bug (fixed now) that tries to read the content for an additional Query String value. Is that the case? Do you have an object and query string? (fix below) If it's something else please post the controller and HTTP request.

The fix below in TryReadBody adds a check for the url encoded content explicitly and doesn't try to read the content otherwise. With a complex type the only time the binder should fire is when a query string value is provided.

Here's the updated TryReadBody method (updated in the article and the source online):
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;
}

K
September 25, 2012

# re: Passing multiple simple POST Values to ASP.NET Web API

Your solution above solved my original problem, because i was using both QueryString and ComplexType object as show in the method below. Thanks for the update.
But i have a different error now. Sorry for troubling you, but i want to do this the right way, with your help.

Action on my Test controller looks like this
public bool PutSomething(int id, ComplexType complexType)
and my jquery ajax call looks as below

$.ajax({
url: '@Url.HttpRouteUrl("DefaultApi", new { controller = "Test"})' + '/' + self.id,
type: "PUT",
contentType: 'application/json; charset=utf-8',
data: ko.toJSON(self) // My complex data
}).done(function (data) {
// Do something.
alert('Updated Successfully!!');
});

This call throws an error with the message "The parameters dictionary contains a null entry for parameter 'id' of non-nullable type 'System.Int32' for method"

I debugged the code and found that below code does not recognize the Querystring
from the url, unless i change the url to be (?Id= is the change. I have to specifically mention the Id.)

url: '@Url.HttpRouteUrl("DefaultApi", new { controller = "Test"})' + '/?Id=' + self.id
in my ajax call?

// 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;
}
}

Eric
October 01, 2012

# re: Passing multiple simple POST Values to ASP.NET Web API

To k:

This function only for contentType: 'application/x-www-form-urlencoded; charset=utf-8'

here is my simple javascript code:
  $.ajax({
                    url: serviceRoot + '/api/product/' ,
                    cache: false,
                    type: 'PUT',
                    contentType: 'application/x-www-form-urlencoded; charset=UTF-8',
                    data: { id: data.Id, name: data.Name}
                });



And I am looking for help for an exception "Multiple actions were found that match the request........."

I have attached simple post variable binding in global.aspx and wrote a test controller with two put methods
        [HttpPut]
        public void TestPut(int price)
        {
            
        }
 
        [HttpPut]
        public string TestMultipleSimpleValues(string id, string name)
        {
            return string.Format("id: {0}, name: {1}", id, name);
        }


There is an an exception "Multiple actions were found that match the request........." when I call web api by above javascript code.

But it will work properly when remove put method "public void TestPut(int price)" from test controller.

It seems can only have one put method in controller, but I need more.

Any idea to help?


Thanks.

Petr
October 05, 2012

# re: Passing multiple simple POST Values to ASP.NET Web API

Thanks for sharing your solution.

Will be fine if is possible to use custom binding only on params with custom attribute like this
public string PostMultipleSimpleValues([FromWWWPost]string name, [FromWWWPost]int value)

It can eliminate sideeffects. Is it possible to update code to support this?

Petr

BTW: This binding work by default before RC or RTM, but I don't know why MS change this

Jonathan
November 26, 2012

# re: Passing multiple simple POST Values to ASP.NET Web API

Thank you for sharing your code.

I just tried to implement this in VB, but when Inserting "HookupParameterBinding" in the configuration I get the following error:

Argument not specified for parameter 'descriptor' of 
'Public Shared Function HookupParameterBinding(descriptor As HttpParameterDescriptor) 
As HttpParameterBinding'.

Jason
May 09, 2014

# re: Passing multiple simple POST Values to ASP.NET Web API

Sorry for reviving an old post, but one thing that might help other people avoid the problem I ran into while implementing this code:

When adding the new ParameterBindingRule to the Application_Start in Global.asax, it's important that the binding occur BEFORE any call to "MapHttpAttributeRoutes". For instance, in my test code, this version will result in the ParamterBindingRule never being called:

protected void Application_Start()
{
    GlobalConfiguration.Configure(WebApiConfig.Register);
 
    // Attach simple post variable binding
    GlobalConfiguration.Configuration.ParameterBindingRules.Insert(0, SimplePostVariableParameterBinding.HookupParameterBinding);
 
    // ...EXTRA CONFIGURATION CODE REMOVED...
}


In the code above, MapHttpAttributeRoutes is being called in the WebApiConfig.Register method. If I simply put the new ParameterBindingRule above that line, the new ParameterBindingRule executes successfully.

protected void Application_Start()
{
    // Attach simple post variable binding
    GlobalConfiguration.Configuration.ParameterBindingRules.Insert(0, SimplePostVariableParameterBinding.HookupParameterBinding);
 
    GlobalConfiguration.Configure(WebApiConfig.Register);
 
    // ...EXTRA CONFIGURATION CODE REMOVED...
}


This could be related to some sort of configuration issue, but it seems to have fixed the issue I was having where even with this code, I was still getting an error message stating that I couldn't pass multiple parameters to the service.

Grant Erickson
June 20, 2014

# re: Passing multiple simple POST Values to ASP.NET Web API

I may have missed something, but I was having issues with optional parameters. I added the following code to take care of it.
        public override Task ExecuteBindingAsync(ModelMetadataProvider metadataProvider,
                                                    HttpActionContext actionContext,
                                                    CancellationToken cancellationToken)
...
 
            object value = StringToType(stringValue);
 
            // Handle optional values.
            if (value == null && this.Descriptor.IsOptional) 
            {
                value = this.Descriptor.DefaultValue;
            }
 
            // Set the binding result here
            SetValue(actionContext, value);

Eric Johnson
November 04, 2014

# re: Passing multiple simple POST Values to ASP.NET Web API

Thanks for the helpful code.

I'm not sure if this is the best way to go about it, but to also bind route parameters I added this code after the body and query string checks:
if (stringValue == null)
{
    var routeData = (HttpRouteData)actionContext.Request.Properties["MS_HttpRouteData"];
    object routeParameterValue;
    if (routeData.Values.TryGetValue(Descriptor.ParameterName, out routeParameterValue))
    {
        stringValue = (string)routeParameterValue;
    }
}

Also, per MS documentation (http://www.asp.net/web-api/overview/formats-and-model-binding/parameter-binding-in-aspnet-web-api), I added this:
public override bool WillReadBody
{
    get
    {
        return true;
    }
}

Royi Namir
November 21, 2014

# re: Passing multiple simple POST Values to ASP.NET Web API

NOw with nullable types supports


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(int?), 
                                                typeof(decimal), 
                                                typeof(decimal?), 
                                                typeof(double), 
                                                typeof(double?), 
                                                typeof(long), 
                                                typeof(long?), 
                                                typeof(bool),
                                                typeof(bool?),
                                                typeof(DateTime),
                                                typeof(DateTime?),
                                                typeof(byte[])
                                            };
 
            if (supportedTypes.Count(typ => typ == descriptor.ParameterType) > 0)
                return new SimplePostVariableParameterBinding(descriptor);
        }
 
        return null;
    }
 
 
    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 (int?)) value = string.IsNullOrWhiteSpace(stringValue) ? (int?) null : int.Parse(stringValue, CultureInfo.CurrentCulture);
        else if (Descriptor.ParameterType == typeof (long)) value = long.Parse(stringValue, CultureInfo.CurrentCulture);
        else if (Descriptor.ParameterType == typeof (long?)) value = string.IsNullOrWhiteSpace(stringValue) ? (long?) null : long.Parse(stringValue, CultureInfo.CurrentCulture);
        else if (Descriptor.ParameterType == typeof (decimal)) value = decimal.Parse(stringValue, CultureInfo.CurrentCulture);
        else if (Descriptor.ParameterType == typeof (decimal?)) value = string.IsNullOrWhiteSpace(stringValue) ? (decimal?) null : decimal.Parse(stringValue, CultureInfo.CurrentCulture);
        else if (Descriptor.ParameterType == typeof (double)) value = double.Parse(stringValue, CultureInfo.CurrentCulture);
        else if (Descriptor.ParameterType == typeof (double?)) value = string.IsNullOrWhiteSpace(stringValue) ? (double?) null : double.Parse(stringValue, CultureInfo.CurrentCulture);
        else if (Descriptor.ParameterType == typeof (DateTime)) value = DateTime.Parse(stringValue, CultureInfo.CurrentCulture);
        else if (Descriptor.ParameterType == typeof (DateTime?)) value = string.IsNullOrWhiteSpace(stringValue) ? (DateTime?) null : DateTime.Parse(stringValue, CultureInfo.CurrentCulture);
        else if (Descriptor.ParameterType == typeof (bool))
        {
            value = false;
            if (stringValue == "true" || stringValue == "on" || stringValue == "1") value = true;
        }
        else if (Descriptor.ParameterType == typeof (bool?))
        {
            value = false;
            if (string.IsNullOrWhiteSpace(stringValue)) value = (bool?) null;
            else
                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
    {
    }
}

Royi Namir
December 03, 2014

# re: Passing multiple simple POST Values to ASP.NET Web API

Now with both x-form-encoded supoort AND JSON send support. ( for POSTS)


https://github.com/RoyiNamir/SimplePostVariableParameterBindingExtended

Sagar Mummidivarapu
April 22, 2015

# re: Passing multiple simple POST Values to ASP.NET Web API

I used your code but still my post method receives null values.
I found that the below code supports only form passed values but what if I send Json request like this: { itemId: "10626217", lookupType: "0" } (multipleBodyparameters is always null)

// 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;
}

Dan H
May 10, 2015

# re: Passing multiple simple POST Values to ASP.NET Web API

Thanks for sharing your solution Rick. I am migrating a project previously done with asmx and was able to adapt your solution for my purposes and it worked for the majority of cases. However, I have a number of (former) asmx methods that accept one or more complex types along with some simple parameter types, and that's where things seem to break down. I just don't see any way to get this to work with web api.

Consider a method like this:

public ValidationResult SaveEvent(EventInfo e, List<UserInfo> inviteList, long currentUserID, string token)
{
     ValidateToken(currentUserID, token);
 
     return ValidationManager.ValidateAndSaveEvent(e, inviteList, currentUserID);
}


It seems that my only option for this is to create a single (view) model that encompasses the first two complex-type parameters. And if I did that, I might as well have that model also include the last two simple parameters, otherwise I'd have to pass the last two parameters via querystring while passing the model parameter in the form body.

Needless to say this requires much more work than it seems like it should, especially since everything was working fine when it was asmx. Any suggestions?

Rick Strahl
May 10, 2015

# re: Passing multiple simple POST Values to ASP.NET Web API

@Dan - the difference is that ASP.NET AJAX with ASMX was basically RPC based so it only knew how to POST data and automatically wrapped the messages for you. If you look at the actual data going over the wire you'll find that the actual client code sent a complex object as well. The framework just knew about the assumptions and knows how to take this complex object and parse that into the parameters when the ASMX method is called.

Web API can't make these same assumptions as you can pass ANYTHING to it. Therefore there are more restrictions on the actual interface you can pass. I'd argue that passing complex parameters in a REST call is not really clean as it doesn't describe what's actually happening underneath. If you have a model that describes the structure on the other hand you are EXACTLY describing what's happening.

As outlined in the post there are two options to do what you're doing:

* Create a ViewModel that includes each parameter as a property
* Use dynamic types which translate into JObject values

Additionally if you really want to be able to do this using exactly that syntax you could create a custom MessageHandler that could do what you want. Maybe with a custom attribute that identifies when this should happen. Wouldn't be hard to do either. But it's something that has to be explicitly designated.

Zijian
August 12, 2015

# re: Passing multiple simple POST Values to ASP.NET Web API

Web API 2.2 apparently has fixed the problem of posting multiple simple type parameters in query.

zheng
November 16, 2015

# re: Passing multiple simple POST Values to ASP.NET Web API

I add SimplePostVariableParameterBinding to my project ,but the asp.net web api help page still generated the api url with query string .How to fix this?

simon
February 08, 2017

# re: Passing multiple simple POST Values to ASP.NET Web API

How do Passing multiple simple POST Values IN ASP.NET Core? Thanks!

 

West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2017