Looking for an ASP.NET Core version of this post? Go here: Accepting Raw Request Body Content in ASP.NET Core API Controllers.
ASP.NET Web API is a great tool for building HTTP services with ASP.NET. It makes many things HTTP, that used to be ill defined in .NET easy and allows you to build a variety of services very easily. Like other complex abstraction frameworks it makes complex scenarios super easy, while some very simple and common operations are not quite as straight forward or as transparent as they should be.
During this year’s MVP summit it was pointed out that ASP.NET Web API makes some very simple operations that a newbie might try when getting started very non-obvious. One of those I wrote about last year, which is mapping post values to controller method parameters and turns out to require a fairly complex workaround. Another one that was brought to my attention by Scott Hanselman and is the topic of this post, is how to capture the raw content of an HTTP request.
Capturing raw request content is by no means difficult with Web API, but it’s not exactly obvious either. Yet, it’s one of those first steps that somebody kicking the tires of ASP.NET Web API is very likely to do.
“Hey let’s take a string and just post it to the server and see what happens, right?”
Yeah well… if you try that, you’re likely going to hit a wall because Web API’s behavior for simple value content mapping is not well defined nor easy to discover.
So in this post I’ll look at posting raw data – not JSON or XML, but just plain text or binary data – to an Web API controller and picking up that data in a controller method. In the process we’ll create a [NakedBody] attribute (the name is Scott’s idea, but it I like it!) that’ll handle the task of capturing raw request data to a parameter.
Complex Values are easy
ASP.NET Web API makes it pretty easy to pass complex data in the form of JSON or XML to the server. It’s easy to do and intuitive. If you want to capture an object you can simply create a controller method that has an object parameter and Web API automatically handles decoding JSON or XML into the object's structure for you.
public HttpResponseMessage PutAlbum(Album album)
You don't have to do anything special to get album to parse from say JSON or XML - Web API's Conneg logic automatically detects the content type, maps it to a MediaFormatter and converts the incoming JSON or XML (or whatever other formatters are configured) data and converts it to the type of the parameter of the controller method.
If the data happens to be POST form data (ie. urlencoded key value pairs), Web API’s Model Binding can automatically map each of the keys of the form data to the properties of the object, including nested object paths.
So that's very easy and as it should be, and it actually addresses most of the realistic use cases. This is the ‘complex stuff is easy’ part.
Capturing the raw Request Body
But things are not so easy and transparent when you want to pass simple parameters like strings, numbers, dates etc. Because Web API is based on a host of conventions, some things that should be easy, such as easily picking up the raw content of a POST or PUT operation and capturing it, aren't quite so transparent as the automatic type mapping shown above.
One thing you might expect to be able to easily do, is to pick up the entire content body and map it to a parameter like this:
public string PostRawBuffer(string raw)
Quick, what do you think is required by the client to call this method the string parameter? I’ll wait here while you ponder…
The answer is - not easily and not without some additional ‘hints’.
There are a number of issues here that actually make this one of the worst parameter signatures in Web API.
This parameter signature does not work with any of these posted values:
- Raw Content Buffer Data (entire buffer)
- A JSON string with application/json content type
- A UrlEncoded Form Variable
- A QueryString Variable
In fact, no matter what you pass here in posted body content – the string parameter is always null. The same is true if you have any other simple parameter type – numbers, bools, dates, byte etc. Plain parameter mapping (without special attributes) works only complex types like objects and arrays.
If you really think about how Web API’s parameter bindings work, this sort of makes sense with the exception of the JSON string parameter. Parameter bindings map based on media types (ie. content-type header) using the Conneg algorithm by default and try to map parameters as whole entities. If you post a a raw string or buffer Web API internally really has no idea how to map this to anything. Should it map the entire buffer? A form variable? A JSON string? So it needs some hints to do it’s thing.
Why doesn’t a JSON string work?
That explains raw strings, but not the JSON string. It’s definitely baffling that a JSON string posted with an application/json content type doesn’t work. So if you POST something like this:
POST http://dev.west-wind.com/aspnetwebapi/samples/PostJsonString HTTP/1.1
Content-type: application/json; charset=utf-8
"Posting a JSON string."
this is a valid JSON request, but it still fails to map.
[FromBody] to retrieve Content
I mentioned hints, and Web API allows you to use parameter binding attributes to provide these hints. These allow you to tell Web API where the content is coming from explicitly. There are [FromBody] and [FromUri] attributes that can force content to be mapped from POST or query string content for example.
Using [FromBody] like this:
public string PostJsonString([FromBody] string text)
now allows JSON or XML content to be mapped from the body. Same goes for other simple parameter types like numbers, dates, bools etc.
So if you now post:
POST http://rasxps/aspnetwebapi/samples/PostRawBuffer HTTP/1.1
Content-Type: application/json; charset=utf-8
"Hello World Suckers."
you now get the raw parameter mapped properly because the input string is in JSON format.
[FromBody] works for properly formatted content – ie. JSON, XML and whatever other media formatters that are configured in the Conneg pipeline. It requires that the data is formatted in JSON or XML. [FromBody] also works with a single POST form variable in urlencoded form data, but because it only works with a single parameter it’s kind of limited for that.
But [FromBody] still doesn’t work if you just want to capture the entire raw content – so if the data is not JSON or XML encoded and you just want the raw data [FromBody] is no help.
Other Posts you might also like