Content Negotiation in ASP.NET Core


aspcore-1024x355As you might know, in ASP.NET Core, the MVC6 stack now includes the Web API functionality. Having a single stack has advantages and I’m happy they’ve converged the two stacks.

While working with early builds, I noticed the patterns for doing content negotiation weren’t working as expected so I defaulted to the MVC approach to REST APIs. In the RC1 build, it seems to be working as expected. Let’s talk about it.

Out of the Box

ASP.NET Core gives you options. You could write APIs like I started to in earlier builds:

[Route("api/[controller]")]
public class PeopleController : Controller
{

  ...

  // GET: api/values
  [HttpGet]
  public JsonResult Get()
  {
    return Json(_people.Get());
  }
  ...

Using JsonResult and returning data with the Json() helper method works great and can get you started, but there are some limitations. This approach means that you have to only return JSON and that returning result codes that aren’t 200 (OK) is a problem. If something goes wrong, a 500 error is returned which isn’t the right thing in many cases.

Removing the JSON Ceremony

Instead of using a JsonResult, we might change this to allow for the data to be returned without the ceremony of telling the controller about JSON:

[Route("api/[controller]")]
public class PeopleController : Controller
{

  ...

  // GET: api/values
  [HttpGet]
  public IEnumerable<Person> Get()
  {
    return _people.Get();
  }
  ...

You’ll notice that we’re returning IEnumerable<Person> instead so the action seems to indicate the content that is being returned. This solves the JSON-only approach, but still gives us problems with result codes. So this is getting closer to supporting both content negotiation and good result codes.

Using Result Codes

Instead of using JsonResult or raw CLR types, let’s return IActionResult so we can return good result codes:

// GET: api/people
[HttpGet]
public IActionResult Get()
{
  return Ok(_people.Get());
}


Notice that we’re returning IActionResult then we can simply wrap the data we’re returning with a helper method called Ok that returns 200 with the result. This becomes more apparent when we look at the POST method that can return BadRequest and Created:

// POST api/people
[HttpPost]
public IActionResult Post([FromBody]Person value)
{
  if (string.IsNullOrWhiteSpace(value.Name))
  {
    return HttpBadRequest("Name cannot be empty");
  }

  value.Id = _people.Get().Max(p => p.Id) + 1;
  _people.Add(value);
  return Created($"/api/people/{value.Id}", value);
}


You can see that if the name is empty, we’re returning a BadRequest (400) or we’re returning a Created (201) if we succeed. This is the best of both worlds. No JSON Ceremony and good result codes. This is what I think it should look like. But does content negotiation still work?

Testing Content Negotiation

Out of the box, ASP.NET Core only has Json (and possibly plain text) as content types. If you need to support XML, you have to add it back to the input and output formatters. That’s pretty easy. First you need a new Nuget package in your project.json:

"dependencies": {
  "Microsoft.AspNet.Mvc": "6.0.0-rc1-final",
  "Microsoft.AspNet.Server.Kestrel": "1.0.0-rc1-final",
  "Microsoft.AspNet.StaticFiles": "1.0.0-rc1-final",
  "Microsoft.Extensions.Configuration.FileProviderExtensions" : "1.0.0-rc1-final",
  "Microsoft.Extensions.Configuration.Json": "1.0.0-rc1-final",
  "Microsoft.Extensions.Logging": "1.0.0-rc1-final",
  "Microsoft.Extensions.Logging.Console": "1.0.0-rc1-final",
  "Microsoft.Extensions.Logging.Debug": "1.0.0-rc1-final",
  "Microsoft.AspNet.Mvc.Formatters.Xml": "6.0.0-rc1-final"
},

With this added, we can configure the formatters in the configuration of the MVC in ConfigureServices:

public void ConfigureServices(IServiceCollection services)
{
  ...

  // Add framework services.
  services.AddMvc(config =>
  {
    // Add XML Content Negotiation
    config.RespectBrowserAcceptHeader = true;
    config.InputFormatters.Add(new XmlSerializerInputFormatter());
    config.OutputFormatters.Add(new XmlSerializerOutputFormatter());
  });
}

Note that the call to RespectBrowserAcceptHeader is false by default, so if you want content negotiation, you have to enable this. Now that if you run this in the browser (which sets the text/xml as an Accept header) you’ll see XML in the result:

3-15-2016 10-23-30 PM

Now that content negotiation is working, you’ll still want to fix the JSON rendering (where it preserves the .NET case) by adding Camel-casing.

Fixing JSON Rendering

You can add camelCase support to JSON by configuring the JSON options:

services.AddMvc(config =>
{
  // Add XML Content Negotiation
  config.RespectBrowserAcceptHeader = true;
  config.InputFormatters.Add(new XmlSerializerInputFormatter());
  config.OutputFormatters.Add(new XmlSerializerOutputFormatter());
})
  .AddJsonOptions(opts =>
  {
    // Force Camel Case to JSON
    opts.SerializerSettings.ContractResolver = new CamelCasePropertyNamesContractResolver();
  });
By calling AddJsonOptions, you can set the contract resolver to use camelCase by default. With these small changes, you can build your API services very similar to the way we did in MVC5/Web API. I like the approach and except for the attributes for routing, I love the approach.

Playing with the Code

I’ve uploaded the example to github if you want to play with it:

https://github.com/shawnwildermuth/WebAPICore 

I’ll be updating my http://shawnw.me/learnaspnetcore course with this new approach when I update it for the RC2 once that is released soon.




Application Name WilderBlog Environment Name Production
Application Ver 1.1.0.0 Runtime Framework .NETCoreApp,Version=v1.1
App Path D:\home\site\wwwroot Runtime Version .NET Core 4.6.25009.03
Operating System Microsoft Windows 6.2.9200 Runtime Arch X86