Intro
In this blog, I intend to provide a simplified how-things-work and how-to-change-the-behavior. It is not intended as a deep dive into content negotiation.
Since Web API 1 controller code can return an object of an arbitrary type and the framework will send it as JSON or XML to the client. The process of picking the output format is called “content negotiation”. The basic rules can be described simply as:
1. The framework will attempt to return the format that the client asked for using the Accept header.
2. In absence of a specific format requested or inferred, the default format is JSON.
3. If the format the client asked for is not available the framework will return the default format JSON. (Example: accept header was application/DoesNotExistFormat)
MVC 6 combined Web API and MVC into a single framework. Although content negotiation was revamped, the basic rules have not changed. There are some enhancements to the API, factoring of the code and a few behavior improvements for edge cases have been added.
A common question about content negotiation is: Why is my API returning XML by default. Nine out of ten times the reason is that the developer is using a browser to test the API, using Chrome or FireFox. The Chrome browser is asking for XML in the accept header and the server follows the above rules.
Here is a simple GET request from Chrome to localhost:
GET / HTTP/1.1
Host: localhost:5001
Proxy-Connection: keep-alive
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/39.0.2171.65 Safari/537.36
Accept-Encoding: gzip, deflate, sdch
Accept-Language: en-US,en;q=0.8,he;q=0.6
Note that the accept header asks for several formats including application/xml, but does not ask for application/json. Hence the server will just return the only format it can match which is XML.
In contrast if we made the same request with IE it will simply not ask for XML:
GET / HTTP/1.1
Accept: text/html, application/xhtml+xml, */*
Accept-Language: en-US,en;q=0.7,he;q=0.3
User-Agent: Mozilla/5.0 (Windows NT 6.3; WOW64; Trident/7.0; Touch; rv:11.0) like Gecko
Accept-Encoding: gzip, deflate
Host: localhost:5001
DNT: 1
Proxy-Connection: Keep-Alive
Since the browser did not ask for any format that the framework recognizes, it will fallback to the default rule and return JSON data.
Note: q factors are assigned to specific accept headers, this is something content negotiation will support. This blog doesn’t cover these scenarios.
What do I do about it
There are a few ways to look at it, the simplest one is to use the right tool for the right job. The browser’s job is to render HTML, so why use it to test your APIs? There are much better tools for that, like Fiddler, browser dev tools, or test your javascript call to the API rather than hitting it directly from the browser.
For me this is where I stop, the API is behaving as expected and will support any client following the HTTP spec.
Thanks for your advice, but I still want to just return JSON!
Many developers just care about returning JSON, while others still want to be able to content negotiate the result for some actions.
Here are some ways to implement the behavior.
Return a JSON result explicitly
- Pros – The code is simple to read, and there is no magic involved. It’s easy to test, and you can mix in other action results.
- Cons – The code is explicit, and there is no notion of what is going to get returned for any introspection APIs such as “ApiDescriptionProvider”. ApiDescriptionProvider is the replacement to ApiExplorer.
- publicIActionResult GetMeData()
- {
- var data = GetDataFromSource();
- if (data == null)
- {
- return HttpNotFound();
- }
- return Json(data);
- }
Remove the XML output formatter from the system.
- Pros – A single change in options.
- Cons – This is a global approach that removes XML from content negotiation. If one of the clients requires XML it is not longer available by default.
note: In MVC6 XmlOutputSerializer was broken into two distinct serializers. The one below is registered by default.
- services.Configure<MvcOptions>(options =>
- options
- .OutputFormatters
- .RemoveAll(
- formatter => formatter.Instance isXmlDataContractSerializerOutputFormatter)
- );
Use the [Produces(“application/json”)] attribute – New in MVC 6
- Pros – Keeps code in the action simple, and can be applied locally or globally in various ways.
- Cons – Can’t mix a string with Produces.
By applying the attribute to an action
- [Produces("application/json")]
- publicList<Data> GetMeData()
- {
- return GetDataFromSource();
- }
By adding the filter globally to startup.cs
- services.Configure<MvcOptions>(options =>
- options.Filters.Add(newProducesAttribute("application/json"))
- );
By applying it to a base class
- [Produces("application/json")]
- publicclassJsonController : Controller { }
- publicclassHomeController : JsonController
- {
- publicList<Data> GetMeData()
- {
- return GetDataFromSource();
- }
- }
What else changed in content negotiation in MVC 6
1. If the controller returns a string (regardless of declared return type), expect to get a text/plain content type.
- publicobject GetData()
- {
- return"The Data";
- }
- publicstring GetString()
- {
- return"The Data";
- }
2. If the controller return null or the return type is void, expect a status code of 204 (NoContent) rather than 200. And the body will be empty.
- publicTask DoSomethingAsync()
- {
- // Do something.
- }
- publicvoid DoSomething()
- {
- // Do something.
- }
- publicstring GetString()
- {
- returnnull;
- }
- publicList<Data> GetData()
- {
- returnnull;
- }
Both the behaviors above are controlled by the HttpNoContentOutputFormatter and the TextPlainFormatter. Both these behaviors can be overridden by removing the formatters from the Options.OutputFormatter collection, similarly to how the XML formatter was removed in the example above.