Versioning ASP.NET Core HTTP APIs using the Accept header

For some reason, I have spent a couple of days thinking about versioning HTTP based APIs. Actually, the reason is that I have a client who is using quite a lot of HTTP based APIs, but in a way that I find less than perfect. I’m not blaming them in any way, as it is the result of growing an application over many years, using many different forms of Microsoft tech, and continuously focusing on delivering features to users instead of building a maintainable system. Currently their application uses ASP.NET WebForms, ASP.NET Core MVC, asmx webservices, WCF webservices, Silverlight, Angular etc, which is what happens in quite a lot of cases over time.

One of the things that have been bothering me is their use of the HTTP services. Sure, it is a great step up from asmx, and even WCF services, as they have been moving from Silverlight to Angular and JavaScript. Unfortunately, as different developers have worked on different applications in the solution, they have created their own API endpoints to suit their needs. Often endpoints are more or less duplicated just to get the returned entity representation to take a slightly different shape.

A very basic example would be a user endpoint. Sometimes when you retrieve a user, you might only need a very limited subset of information about that user. And in other cases you might want a lot more information. And being developers, we obviously don’t think we should retrieve a bunch of fields regarding a user when we don’t need it. Not to mention that the user retrieving the data might not be allowed to view all the fields. So to solve that, there are a bunch of different “get user” endpoints, each returning a specifically designed representation of the requested user, each used from a specific application, or even a specific part of an application. And this means a lot of duplicated code, as well as a very hard to use API.

API versioning

Even if we don’t need to have several different representation of an entity in our application, there is another situation that is very similar, and that comes up all the time. Versioning… How do we version an API? For example, in version 1, the user entity contained a set of very well defined set of values, including an age field. However, when working through the API for version 2, it was decided that passing a date of birth would be much better, as some apps wanted more control than just an age. So how do we solve this? Well, there are a couple of ways…

In this particular case, one could just add another field to the representation and be done with it. It wouldn’t cause any problems with existing clients, but would offer the new functionality consumers of version 2. ANd this is a great way to extend the API from v1 to v2. However, in some cases this doesn’t work. What if in v1 we return a date of birth, but then realize that that might be a privacy issue and want to replace it with just an age. That would be a breaking change, causing problems for every single application depending on the API.

There are 4 common ways to handle versioning of APIs

1. Modify the path – https://example.com/v1/users/1

Adding the version to the path is simple and easy to do, and easy to consume. The problem with this solution however, is that the path should be stable. This means that the representation of an entity should always be available at the same path. By changing the path, we are kind of saying that it is 2 different entities, whereas it is really just 2 different representations of the same entity.

2. Add a query string parameter – https://example.com/users/1?version=1

Using a query string to pass the version is also very nice and easy. However, for me, query string parameters has to do with querying. And by that I mean that, for me, query string parameters should be used to filter, or query, the information that is being requested, not to tell it what representation that should be used for the returned data.

3. Add a custom header – version: 1

The third option is to have the client consuming the API pass the version to the server using a custom header. This takes moves the concern from the path to another part of the transport, which is nice. However, it does make it harder to send the request. You can’t easily just type it into the address bar of you browser and get the response. Instead you need a specific tool to do it. On the other hand, for me, that isn’t a huge problem. APIs are supposed to be consumed by applications (code), not by users using a browser, so it shouldn’t be a big problem. But why would you do it this way, when there is a built in way in the HTTP specification as you will see in the fourth way?

4. Use the Accept header – Accept: application/vnd.myapp.v1.user+json

HTTP already defines a header to this problem. It is the Accept header. It is used to tell the server what types of response that the client can accept. Often this header is used to request JSON by passing the value application/json, or html by passing the value text/html. However, the spec defines the value as a string. So you can literally pass along any form of string value that you want. But there are some nice conventions that you might want to follow. First of all, it is generally a 2 part value, split by a /. The first part gives a high-level type like “text” or “image” for example, and the second part defines it more specifically like “text/html” or “image/png”. But as I said, any string is valid, so the xxx/yyy is just a convention, it isn’t in any way enforced. For formats that are consumed by applications, like JSON for example, the first part is often “application”. And for custom formats used by specific APIs, the convention is to prefix the 2nd part with “vnd.” to indicate that it is vendor specific. After that, you put the definition of what you are requesting. In this case I chose “myapp.v1.user” to indicate that the representation that I want should be a “user” from the “myapp” application, and that I want version 1. But once again, you can format this in any way you want.

However, using the accept header to indicate a custom representation causes a problem. Normally, this header is used to define what serialization format should be used, such as JSON or XML. Replacing this with a custom string means that we need to find another way to convey that information. The common way to do this, is to append it to the string, separating it with a + sign. In this case I added “+json” to indicate that I want my “application/vnd.myapp.v1.user” representation sent to me using JSON. I could have added “+xml” to get it in XML format if my backend supported this.

API versioning thoughts

All of the above mentioned ways work for versioning APIs. They each have benefits and downsides, and I don’t think any of them are “the right way”. It all depends on you requirements, as so much does in our line of business. But my personal preference is option 4, using the Accept header. Mostly because it uses a predefined feature in HTTP, and following standards makes things a lot easier in most cases. I’m sure that the people who designed HTTP are MUCH smarter than me, and have thought of WAY more scenarios than I have. So I’ll trust those guys, and try to follow their recommendation. However, if it is a requirement that you should be able to call the API from a browser, well, then I guess I would have to choose option 1 or 2. Option 3 is probably my least favorite, but on the other hand it has some benefits. By using the Accept header to pass the format to be used, JSON or XML for example, I can used the built in content negotiation in the ASP.NET Core framework to solve the formatting, and just look at the custom header to figure out the representation to use. So they all have their place in one form or another.

Using the Accept header versioning in ASP.NET Core MVC routing

ASP.NET Core MVC routes requests to actions using either convention based routing with templates, or using attributes. Personally I prefer the attributes when building APIs, as it gives me better control over the paths being used, and also makes it easier to understand the path by looking at the controller and the attributes.

However, the built in stuff doesn’t take any headers into account. The Accept header is only taken into account when formatting the returned value. So to make use of that header while routing, we need to write some code, but luckily it is not very complicated code.

What we need is an implementation of IActionConstraint. This is an interface that is used to programmatically add a constraint to an action, basically allowing us to use logic to tell the routing engine whether or not an action can be used to handle the current request. Using this, we can add an attribute to an action, and define what accept header value should be passed for this specific action to be executed.

The IActionConstraint interface has 2 members that needs to be implemented

bool Accept(ActionConstraintContext context)
int Order { get; }

The Accept method gets a “context” with information regarding the current request, and returns a boolean defining whether or not this request can be used to handle this request. And the Order property defines in what order the action’s constraints should be executed. The lower the number, the earlier in the list it will be executed.

There are a couple of ways to go about building the IActionConstraint that we want. We could create a class that inherits from Attribute, and implements IActionConstraint. Or, we could go and create a class that inherits from ActionMethodSelectorAttribute, and implement the IsValidForRequest method. And being me, I’ll go for the simplest solution, and just create a class called AcceptHeaderAttribute and inherit from ActionMethodSelectorAttribute.

public class AcceptHeaderAttribute : ActionMethodSelectorAttribute {
    public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action) {  }
}

Next, I need to know what Accept header value it should check for. So I add a constructor that accepts a string defining this

public class AcceptHeaderAttribute : ActionMethodSelectorAttribute {
    private readonly string _acceptType;
    public AcceptHeaderAttribute(string acceptType)
    {
        this._acceptType = acceptType;
    }
   
}

Now that I know what Accept header value should be passed, I can implement the IsValidForRequest method

public override bool IsValidForRequest(RouteContext routeContext, ActionDescriptor action)
{
     var accept = routeContext.HttpContext.Request.Headers.ContainsKey("Accept") ? routeContext.HttpContext.Request.Headers["Accept"].ToString() : null;
     if (accept == null) return false;

    var acceptWithoutFormat = accept;
     if (accept.Contains("+"))
     {
         acceptWithoutFormat = accept.Substring(0, accept.IndexOf("+"));
     }

    return acceptWithoutFormat == _acceptType;
}

I start out by checking to see whether or not there is an Accept header at all in the request by looking at the RouteContext’s HttpContext. If there isn’t an Accept header I return false, as without the header this action is not an option for execution.

Next I remove any potential formatting information from the header by removing anything after a +-sign, including the potential +-sign.

And finally, I just return whether or not the remaining string corresponds to the configured value.

That’s all there is to it! And using it is just as simple. Just add it to an action like this

[HttpGet("{id:int}")]
[AcceptHeader("application/vnd.mydemo.v1.user")]
public async Task<IActionResult> GetUserById(int id)
{
     var user = await _users.WhereIdIs(id);
     if (user != null)
         return Ok(user.ToModel());
     return NotFound();
}


[HttpGet("{id:int}")]
[AcceptHeader("application/vnd.mydemo.v2:user")]
public async Task<IActionResult> GetUserByV2(int id)
{
     var user = await _users.WhereIdIs(id);
     if (user != null)
         return Ok(user.ToModelV2());
     return NotFound();
}

Another nice feature is that you can easily handle the case where an Accept header is not passed at all, by adding

[HttpGet("{id:int}")]
public IActionResult GetUserByIdDefault(int id)
{
     return StatusCode(406, "Invalid Accept header");
}

As you can see, it has the same HttpGet attribute as the other actions, but no AcceptHeader attribute. This means that when the router looks at the actions, it will select the one without the AcceptHeader attribute if it’s missing. This means that the client gets an HTTP 406 back if the header is missing. You could obviously also have the “bare” action be v1, and default to that for any request missing the header or with an invalid header if you wanted to. On the other hand, that might be a little confusing to someone who wanted v2 but forgot to pass the Accept header, or passed an incorrect value. But if you have an API that currently doesn’t use this kind of versioning, it might be a great way to move forward with it without breaking existing clients.

Customizing the ASP.NET Core MVC content negotiation to handle custom Accept headers

There is a small problem with the current solution. If you want to support content negotiation, and multiple response formats, ASP.NET Core MVC can easily to add other formats than the default JSON formatter by adding other output formatters. This is done when configuring the MVC services like this

services.AddMvc(config => {
     config.OutputFormatters.Add(new XmlSerializerOutputFormatter());
});

However, the content negotiation built into ASP.NET Core MVC uses the Accept header to do it’s work. So when we stop using “application/json” and “text/xml”, it breaks down. So to sort this out, we either need to create our own custom IOutputFormatter implementations, or modify the way that the formatter is selected. I’m going to go for the latter in a slightly hacky, but working way.

I don’t feel like doing too much work, and I believe that what is already in the framework works very well, so all I want to do is hack in a little change using a custom OutputFormatterSelector that makes the new Accept header format work with the existing implementation.

For this, I’ll go ahead and add a new class called AcceptHeaderOutputFormatterSelector, and have it inherit OutputFormatterSelector.

public class AcceptHeaderOutputFormatterSelector : OutputFormatterSelector {
}

And since I like the default implementation from the framework, I’ll add a constructor that sets up an instance of that OutputFormatterSelector internally in my class

public AcceptHeaderOutputFormatterSelector(IOptions<MvcOptions> options, ILoggerFactory loggerFactory, string defaultContentType = "application/json")
{
     _fallbackSelector = new DefaultOutputFormatterSelector(options, loggerFactory);
     _formatters = new List<IOutputFormatter>(options.Value.OutputFormatters);
     _defaultContentType = defaultContentType;
}

As you can see, it accepts some MvcOptions, an ILoggerFactory and a string containing the default format to use, which I default to application/json. And then I use those values to create a new instance of DefaultOutputFormatterSelector, which is the implementation used by the framework by default. The values are then stored globally so that I can use them later in my class.

The OutputFormatterSelector class is abstract, and requires you to implement the SelectFormatter method. It looks like this

public IOutputFormatter SelectFormatter(OutputFormatterCanWriteContext context, IList<IOutputFormatter> formatters, MediaTypeCollection mediaTypes);

This is the method that is called by the framework to figure out what output formatter to use when serializing the response. It gets a “context” with all the essential information, a list of formatters to use, which should override the list of formatters passed into the constructor, and a collection of media types.

So how do I implement this. Well, I want a version that, if the Accept header contains a custom content type with a ”+[format]” definition, replaces the custom content type with a standard one, and then let’s the default implementation of the selector figure out the rest.

To do this, I implement the method like this

public override IOutputFormatter SelectFormatter(OutputFormatterCanWriteContext context, IList<IOutputFormatter> formatters, MediaTypeCollection mediaTypes)
{
     if (!HasVendorSpecificAcceptHeader(context.HttpContext.Request))
         return _fallbackSelector.SelectFormatter(context, formatters, mediaTypes);

    if (formatters.Count == 0)
     {
         formatters = _formatters;
     }

    context.ContentType = GetContentTypeFromAcceptHeader(context.HttpContext.Request);

    var formatter = formatters.FirstOrDefault(x => x.CanWriteResult(context));

    context.ContentType = context.HttpContext.Request.Headers["Accept"].First();

    return formatter;
}

First, I verify that the request has a vendor specific content type. This basically just means that I check if the Accept header value starts with “application/vnd”.

private bool HasVendorSpecificAcceptHeader(HttpRequest request)
{
     return request.Headers["Accept"].First().IndexOf("application/vnd.") == 0;
}

If it doesn’t have a vendor specific value, it just uses the default implementation to do the work. If it does on the other hand I carry on by checking to see if the passed in list of formatters is empty, in which case I use the default once that I received in the constructor instead.

Once I have my list of formatters, I get the “proper” content type by calling a helper method that looks like this

private string GetContentTypeFromAcceptHeader(HttpRequest request)
{
     var acceptHeaderValue = request.Headers["Accept"].First();
     if (acceptHeaderValue.IndexOf("+") > 0)
     {
         var contentType = acceptHeaderValue.Substring(acceptHeaderValue.IndexOf("+") + 1);
         if (_contentTypeMap.ContainsKey(contentType))
             return _contentTypeMap[contentType];
     }
     return _defaultContentType;
}

It pulls out the Accept header and looks to see if it contains a +-sign. If it doesn’t, it returns the default content type from the constructor. But if it does, it takes the part of the string after the +-sign, eg json or xml, and passes that to a dictionary that will map those values to proper types like application/json or text/xml. If there is no conversion available in the map, it just fallbacks to the default type.

Once I have the “proper” type that the framework knows about, I set the context’s ContentType property to this value, before iterating through all the formatters to see which formatter can write a result for this value. Once I have my formatter, I reset the context’s ContentType to the value of the Accept header to make sure that the response to the client has the Content-Type header set to the same type that has been requested. If I don’t do this, the client will request one type, but get a generic application/json Content-Type back.

Once I have my AcceptHeaderOutputFormatterSelector implemented, I can add the XmlSerializerOutputFormatter and replace the default format selector in the ConfigureServices method in the Startup class like this

services.AddMvc(config => {
     config.RespectBrowserAcceptHeader = true;
     config.OutputFormatters.Add(new XmlSerializerOutputFormatter());
});
services.AddSingleton<OutputFormatterSelector, AcceptHeaderOutputFormatterSelector>();

Note: ASP.NET Core MVC defaults to JSON and ignores the Accept header by default. So to enable content negotiation based on the Accept header, you need to set the RespectBrowserAcceptHeader property on the MvcOptions.

That’s “all” there is to it to get this to work… Yes, it it not real production quality code, but it works and could be extended to production quality with some testing and so on. But for now, all I wanted, and needed, was a proof of concept that it works, and it does!

If you want to see some code, I have uploaded some sample code to my GitHub account, which you can find here: https://github.com/ChrisKlug/AspNetCoreMvcAcceptHeaderRouting

Hope this helps in some way, or at least gave you some ideas of how to solve some problem that you have, or might run into in the future!

zerokoll

Chris

Developer-Badass-as-a-Service at your service