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.1
Content-Type: application/json; charset=utf-8
Host: rasxps
Content-Length: 13
Expect: 100-continue
Connection: 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.1
Content-Type: application/json; charset=utf-8
Host: rasxps
Content-Length: 30
Expect: 100-continue
Connection: 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 just producing null, 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)
and
public HttpResponseMessage ReturnDateTime([FromBody] DateTime time)
which explicitly instructs Web API to read the value from the body.
UrlEncoded Form Variable Parsing
updated 3/23/2012 with additional information about FormDataCollection
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.1
Content-Type: application/x-www-form-urlencoded
Host: rasxps
Content-Length: 18
Expect: 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 model properties to the POST variables. Sending the same message to the following method that access a model object works:
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 POST form variables if there were more. This works but it's not very dynamic as you have to a have a static model parameter.
You can get at the form variables manually however. You can specify that the parameter is a FormDataCollection instance:
public string ReturnFormVariableString(FormDataCollection formData)
{
return formData.Get("message");
}
Oddly FormDataCollection does not allow for indexers to work so you have to use the .Get() or .GetValues() methods (for multi-select values) which is pretty weird for a collection type.
Note that although this isn't recommended: If you're running Web API under ASP.NET you still have access to the HttpContext.Current.Request object. This will work ONLY in ASP.NET and not in self-hosted scenarios and will significantly complicate test scenarios, but if you need to get at some data that Web API doesn't expose the Request is there for you in a pinch. There shouldn't be much that Web API doesn't expose though, so try to avoid use of it even if sometimes it just seems easier to go that route.
Access the QueryString
Querystring values are automatically mapped to parameters on the controller method fired. In most cases that's sufficient, but if for some reason you have too many querystring values to push into parameters or you otherwise need to parse your querystring explicitly here's how you can do that from within a controller method:
var queryVals = Request.RequestUri.ParseQueryString();
var message = queryVals["message"];
Note that ParseQueryString is an Extension Method to Uri in System.Net.Http so make sure to add that namespace.
Summary
As you can see Web API makes most things related to parameter mapping pretty easy, but there are some behaviors can be a little 'different' and require a little more work than we might be used from directly accessing HttpContext.Current in plain ASP.NET applications. While HttpContext is still available in Web API when running under IIS, in general it's not a good idea to use it. First and most importantly it only works when running under IIS which means it doesn't work for self-hosting and is problematic if you are unit testing your Web API code since no HttpContext is available under test. Inside of the controller most Http features are available from the Request property although it might require a little digging the first few times you need access to form variables and querystrings.
In a way it's funny that accessing complex values in Web API actually requires less effort than accessing simple values. I'm not fond of the decision to make the behavior between simple and complex types different when mapping parameters - when I first started playing with Web API it actually led me to a bunch of false assumptions about what wasn't working. For example I first ran into this with date values and assumed this was a problem with Web API's date parsing when in fact it was simply the parameter mapping that didn't work. I hope some revision happens here make this easier still.
Also realize that Web API doesn't have any sort of global context object. Controllers get easy access to the Request object, but lower level components like formatters, filters and message handlers don't have easy access to these objects. These APIs require special methods that can intercept and capture the Request which is a topic for another post.
One of the reasons that these inconsistencies exist is that there are a number of different binding approaches used by Web API: QueryString mapping, Post data model binding, xml and json parsing of single values etc. and the order of the paths through Web API end up causing some of these issues. While I think they are pretty annoying and can result in WTF moments, I also think that the scenarios where it happens (single value assignments) are pretty rare. I suspect most applications will opt to pass objects around or map POST/PUT data to a server side View model rather than posting individual values. But nevertheless it's important to understand that there are some… cough… unexpected behaviors.
Other Posts you might also like