ASP.NET Web API and Simple Value Parameters from POSTed data
- by Rick Strahl
In testing out various features of Web API I've found a few oddities in the way that the serialization is handled. These are probably not super common but they may throw you for a loop. Here's what I found. Simple Parameters from Xml or JSON Content Web API makes it very easy to create action methods that accept parameters that are automatically parsed from XML or JSON request bodies. For example, you can send a JavaScript JSON object to the server and Web API happily deserializes it for you. This works just fine:public string ReturnAlbumInfo(Album album)
{
return album.AlbumName + " (" + album.YearReleased.ToString() + ")";
}
However, if you have methods that accept simple parameter types like strings, dates, number etc., those methods don't receive their parameters from XML or JSON body by default and you may end up with failures. Take the following two very simple methods:public string ReturnString(string message)
{
return message;
}
public HttpResponseMessage ReturnDateTime(DateTime time)
{
return Request.CreateResponse<DateTime>(HttpStatusCode.OK, time);
}
The first one accepts a string and if called with a JSON string from the client like this:var client = new HttpClient();
var result = client.PostAsJsonAsync<string>(http://rasxps/AspNetWebApi/albums/rpc/ReturnString, "Hello World").Result;
which results in a trace like this:
POST http://rasxps/AspNetWebApi/albums/rpc/ReturnString HTTP/1.1Content-Type: application/json; charset=utf-8Host: rasxpsContent-Length: 13Expect: 100-continueConnection: Keep-Alive
"Hello World"
produces… wait for it: null.
Sending a date in the same fashion:var client = new HttpClient();
var result = client.PostAsJsonAsync<DateTime>(http://rasxps/AspNetWebApi/albums/rpc/ReturnDateTime, new DateTime(2012, 1, 1)).Result;
results in this trace:
POST http://rasxps/AspNetWebApi/albums/rpc/ReturnDateTime HTTP/1.1Content-Type: application/json; charset=utf-8Host: rasxpsContent-Length: 30Expect: 100-continueConnection: Keep-Alive
"\/Date(1325412000000-1000)\/"
(yes still the ugly MS AJAX date, yuk! This will supposedly change by RTM with Json.net used for client serialization)
produces an error response:
The parameters dictionary contains a null entry for parameter 'time' of non-nullable type 'System.DateTime' for method 'System.Net.Http.HttpResponseMessage ReturnDateTime(System.DateTime)' in 'AspNetWebApi.Controllers.AlbumApiController'. An optional parameter must be a reference type, a nullable type, or be declared as an optional parameter.
Basically any simple parameters are not parsed properly resulting in null being sent to the method. For the string the call doesn't fail, but for the non-nullable date it produces an error because the method can't handle a null value.
This behavior is a bit unexpected to say the least, but there's a simple solution to make this work using an explicit [FromBody] attribute:public string ReturnString([FromBody] string message)
andpublic HttpResponseMessage ReturnDateTime([FromBody] DateTime time)
which explicitly instructs Web API to read the value from the body.
UrlEncoded Form Variable Parsing
Another similar issue I ran into is with POST Form Variable binding. Web API can retrieve parameters from the QueryString and Route Values but it doesn't explicitly map parameters from POST values either.
Taking our same ReturnString function from earlier and posting a message POST variable like this:var formVars = new Dictionary<string,string>();
formVars.Add("message", "Some Value");
var content = new FormUrlEncodedContent(formVars);
var client = new HttpClient();
var result = client.PostAsync(http://rasxps/AspNetWebApi/albums/rpc/ReturnString, content).Result;
which produces this trace:
POST http://rasxps/AspNetWebApi/albums/rpc/ReturnString HTTP/1.1Content-Type: application/x-www-form-urlencodedHost: rasxpsContent-Length: 18Expect: 100-continue
message=Some+Value
When calling ReturnString:public string ReturnString(string message)
{
return message;
}
unfortunately it does not map the message value to the message parameter. This sort of mapping unfortunately is not available in Web API.
Web API does support binding to form variables but only as part of model binding, which binds object properties to the POST variables. Sending the same message as in the previous example you can use the following code to pick up POST variable data:public string ReturnMessageModel(MessageModel model)
{
return model.Message;
}
public class MessageModel
{
public string Message { get; set; }}
Note that the model is bound and the message form variable is mapped to the Message property as would other variables to properties if there were more. This works but it's not very dynamic.
There's no real easy way to retrieve form variables (or query string values for that matter) in Web API's Request object as far as I can discern. Well only if you consider this easy:public string ReturnString()
{
var formData = Request.Content.ReadAsAsync<FormDataCollection>().Result;
return formData.Get("message");
}
Oddly FormDataCollection does not allow for indexers to work so you have to use the .Get() method which is rather odd.
If you're running under IIS/Cassini you can always resort to the old and trusty HttpContext access for request data:public string ReturnString()
{
return HttpContext.Current.Request.Form["message"]; }
which works fine and is easier. It's kind of a bummer that HttpRequestMessage doesn't expose some sort of raw Request object that has access to dynamic data - given that it's meant to serve as a generic REST/HTTP API that seems like a crucial missing piece. I don't see any way to read query string values either.
To me personally HttpContext works, since I don't see myself using self-hosted code much.© Rick Strahl, West Wind Technologies, 2005-2012Posted in Web Api
Tweet
!function(d,s,id){var js,fjs=d.getElementsByTagName(s)[0];if(!d.getElementById(id)){js=d.createElement(s);js.id=id;js.src="//platform.twitter.com/widgets.js";fjs.parentNode.insertBefore(js,fjs);}}(document,"script","twitter-wjs");
(function() {
var po = document.createElement('script'); po.type = 'text/javascript'; po.async = true;
po.src = 'https://apis.google.com/js/plusone.js';
var s = document.getElementsByTagName('script')[0]; s.parentNode.insertBefore(po, s);
})();