In part 2, we’ll explore how ASP.NET implements content negotiation. I’ll show how to customize the behavior and get full control.
This post is part of a series:
- What is Content Negotiation
- ASP.NET Core Support for Content Negotation (this post)
- Codecs and Transcoders (coming soon)
- Using Codecs in Clients (coming soon)
- Using Codecs for Data Persistence (coming soon)
Supported, but Challenging
HTTP Content Negotiation is supported in ASP.NET Core. However, the documentation and examples are a bit sparse. Articles mostly cover the simplistic use case of picking JSON vs XML.
This article (part 2) aims to highlight key building blocks and illustrate a more complex example.
In Part 3, I’ll propose some additional concepts and plumbing to make content negotiation a first-class experience in ASP.NET Core HTTP APIs.
ASP.NET Building Blocks for Content Negotiation
The following pieces make content negotiation work in ASP.NET Core (aka ANC). Links are provided for reference, but see the next section for a tour of how these compose together.
- HTTP headers: Accept and Content-Type
- MediaType class
- InputFormatter implementations such as StringInputFormatter, SystemTextJsonInputFormatter, XmlDataContractSerializerInputFormatter, NewtonsoftJsonInputFormatter.
- OutputFormatter implementations such as StringOutputFormatter, SystemTextJsonOutputFormatter, XmlDataContractSerializerOutputFormatter, NewtonsoftJsonOutputFormatter.
- Formatter registration (configuration) with ASP.NET via MvcOptions
- OutputFormatterSelector implementations such as DefaultOutputFormatterSelector
- Produces and Consumes controller attributes
- ObjectResult and subclasses
Composing the Building Blocks
We’ll start with the Microsoft docs and examples. Then we’ll go beyond the basics in the following sections.
As you read the Microsoft docs, note that ANC’s built-in support for content negotiation is limited to 3 media types (application/json, application/xml, and text/plain). Use of arbitrary media types requires custom formatters.
Beyond Basics – Leveraging MediaTypes
As noted in Part 1, content negotiation helps solve multiple challenges:
- Versioning, Evolution, and Backwards-Compatibility
- Client / Server deployment decoupling
- Clean and semantically meaningful endpoints
In pursuit of these, a few assumptions and goals for the scenario:
- Use Domain specific media types
- Use Versions in the media types
- Support multiple versions of a media type in a single endpoint (path)
- Use a single resource class, but render (format) as multiple representations
I’ve created an ASP.NET Core Content Negotiation Example to demonstrate the above goals and approaches.
The Readme.md includes samples of the JSON representations. Note that the representations differ. Same data, but different representation structure. Also note that the custom representation differs from the structure and naming of the related resource class.
Code is heavily commented with the intent to highlight the purpose and nuances.
You can exercise the API via the annotated curl commands in the readme. I encourage experimenting with the example by running it (execute
dotnet run from the solution or project directory) and then using the curl commands. On windows, I suggest WSL or git bash.
“Custom” vs “Serialized” formatters
The example shows two flavors of formatters. These are primarily to illustrate an alternative approach to generating and parsing JSON. They also show content negotiation based on MediaType parameters (domain and version).
- Full control over the json structure
- Better performance
The serialized variations use the more common “binding” approach. This is attractive for ease of use and reduced code, but it has sharp edges when the representation needs to deviate from the structure of your resource class. Over time this will become more of a problem.
We’ll discuss this further in a subsequent post.
Startup and Configuration
The Startup.cs class shows some important points.
- How to register formatters
- The order of formatter registration matters
- The impact of the RespectBrowserAcceptHeader setting
- Default is false.
- I suggest setting this to true.
- Because… if set to false, and a client passes “*/*” as one of multiple media types, they will NOT see the expected behavior. Instead, the server will skip content negotiation. This is unexpected and will likely cause much consternation.
- The impact of the ReturnHttpNotAcceptable setting.
- Default is false.
- I suggest setting this to true.
- Because… this makes the API strict, and therefor predictable in negotiating media types.
Produces Attribute on Controller Endpoints
The example controller shows multiple variations of the Produces attribute on the endpoints. This attribute is useful for a couple purposes:
- Supported Media Types
- .NET Type of Result
It is possible to set either or both when declaring the attribute.
For the purposes of content negotiation, we care most about setting the supported media types. Setting the media types will constrain the list during content negotiation processing (see DefaultOutputFormatterSelector for this logic). Generally, an endpoint SHOULD return an HTTP 406 if the request asks for an unsupported media type.
Setting the .NET Type has minimal impact on content negotiation, but it is a useful hint to the system and documentation for developers. This is especially useful when controllers use the IActionResult in the method signature, which is strongly encouraged as a best practice.
This attribute is also highly useful for swagger documentation. The metadata supplied helps in generating the OAS (Open API Spec).
Consumes Attribute on Controller Endpoints
The example controller shows multiple variations of the Consumes attribute on the endpoints. This attribute is primarily used to define the supported MediaTypes that can be sent in the body of the request. ANC uses this during model binding to locate a formatter for decoding the body into an object and passing it to the controller method as a parameter. Often this parameter is marked with the [FromBody] attribute.
The Consumes attribute is not strictly part of “response content negotiation”, but plays a very similar and useful role in shaping the behavior of an endpoint.
A request that specifies an unsupported Content-Type will result in an HTTP 415 – Unsupported MediaType response. As noted above, it is useful and important for an API to be predictable and accurate about data handling.
Architecture and Design Patterns
SRP is a key goal of the design. Controllers do NOT decide what format (representation) to return. The inputs and outputs are POCO resources and/or method parameters. Controllers DO leverage ANC model binding and formatters. This cleanly separates concerns, makes controller code testable, and provides extension points for content negotiation.
Formatters encapsulate the logic to transcode (encode / decode) between the representation (bits on the wire) and the resource POCO.
Backwards Compatible (Versionable)
A single endpoint is able to return multiple representations. Over time, we can support new versions on the same endpoint.
The client decides which representation to return. This is critical to maintaining compatibility. Content negotiation makes this possible, as long as the client clearly specifies what it supports.
Clients can upgrade on their own timing, before / during / after the server upgrade.
Servers can upgrade on their own timing, independent of clients.
Servers can even roll-back a deployment if the clients are built to support both old and new representations.
Clean, Meaningful Endpoint Paths
Endpoints are simple and align to the ReST principles. The path does not include details about representation. Instead, we rely on a well-vetted and well-understood feature that is core to the HTTP protocol and supported by network infrastructure (proxies, firewalls, etc).
The example and content above aimed to show how ASP.NET Core supports content negotiation. As noted, it is supported “out of the box” and customizable.
Customization is a bit challenging. It requires a custom formatter for each media type. These formatters are strongly tied to ASP.NET’s server-side plumbing, which makes them hard to use elsewhere (like in a client SDK).
In Part 3, we’ll separate some concerns to make customization simpler, testable, and reusable.
Further Reading – MS Example
Microsoft supplies various example (test) web sites that exercise these capabilities. A useful example site is this BasicWebSite. I include it here as further reading for deeper exploration of what ANC provides out of the box.
I suggest reviewing:
- A set of controllers specific to content negotiation (see other controllers in this same folder)
- Use of the Consumes attribute in these ActionConstraints controllers
- Custom formatters such as the VCardFormatters
The BasicWebSite illustrates the myriad ways of using formatters. I found it interesting that Formatters can also be registered in the controller by setting them on the ObjectResult directly. I do not recommend this approach, but I do find the capability interesting.
MediaType Quality (“q”) Parameter
This is the “quality” parameter and is used in HTTP requests to indicate which of the multiple Accept header’s media types are preferred. Values are between 0 and 1 as decimal values specified to 3 decimal places. A value of 1 is most preferred.
The OutputFormatterSelector shows use of the “q” parameter on media types.