Skip to content

How to Version Your Service

Chris Martinez edited this page Dec 29, 2022 · 11 revisions

REST services are implemented in ASP.NET as an endpoint. To version your service, you simply need to decorate your endpoints with the appropriate API version information. The method of decoration will vary depending on whether you are using controllers or Minimal APIs as well as whether you want to use attributes or conventions.

How It Works

The way that you create and define routes remains unchanged. The key difference is that routes may now overlap depending on whether you are using convention-based routing, attribute-based routing, or both. In the case of attribute routing, multiple controllers will define the same route. The default services in each flavor of ASP.NET assumes a one-to-one mapping between routes and endpoints and, therefore, considers duplicate routes to be ambiguous. The API versioning services replace the default implementations and allow endpoints to also be disambiguated by API version. Although multiple routes may match a request, they are expected to be distinguishable by API version. If the routes cannot be disambiguated, this is likely a developer mistake and the behavior is the same as the default implementation.

Supported Routing Methods

The following table outlines the various supported routing methods:

Routing Method Supported
Attribute-based routing Yes
Convention-based routing Yes
Attribute and convention-based routing (mixed) Yes*

* Due to limitations in the routing infrastructure in ASP.NET Web API, API versioning is not guaranteed to work for controllers that define both attribute and convention-based routes for the same route

Naming and Collation

While it might seem more intuitive that similar route templates are collated together, that is simply not the case. Consider that order/{id} and order/{id:int} are different, but semantically identical. API Versioning makes no attempt understand this difference. Although it is possible to have an API with a single endpoint, most APIs consist of a collection of endpoints; for example the Orders API. What if we saw the route template order/{id}/items? Is this part of the Orders API or some other API? For this reason, API Versioning collates on the logical name of an API and not individual route templates.

When you use controllers to define your endpoints, the controller provides the grouping construct for a set of endpoints and the name derives from the defining type without the Controller suffix. For example, HelloWorldController is interpreted as HelloWorld. Unfortunately, this can cause an issue for service API versioning if you want to split the implementation across different types. If the defining type is in a different .NET namespace, then there is no issue; however, if they are in the same namespace there would be a name collision. For example:

namespace My.Services.V1
{
    [ApiVersion( 1.0 )]
    [Route( "[controller]" )]
    public class HelloWorldController : ControllerBase
    {
        [HttpGet]
        public string Get() => "Hello world v1.0!";
    }
}

namespace My.Services.V2
{
    [ApiVersion( 2.0 )]
    [Route( "[controller]" )]
    public class HelloWorldController : ControllerBase
    {
        [HttpGet]
        public string Get() => "Hello world v2.0!";
    }
}

Controllers separated by .NET namespace

namespace My.Services.Controllers
{
    [ApiVersion( 1.0 )]
    [Route( "[controller]" )]
    public class HelloWorldController : ControllerBase
    {
        [HttpGet]
        public string Get() => "Hello world v1.0!";
    }

    [ApiVersion( 2.0 )]
    [Route( "[controller]" )]
    public class HelloWorld2Controller : ControllerBase
    {
        [HttpGet]
        public string Get() => "Hello world v2.0!";
    }
}

Controllers with different names in the same .NET namespace

To address name collisions and provide control over how collation happens, API Versioning provides the following service:

public interface IControllerNameConvention
{
    string NormalizeName( string controllerName );
    string GroupName( string controllerName );
}

NormalizeName controls how or whether a controller name is normalized. GroupName provides the name used to group and collate on, which may not necessarily be the same as the normalized name. ControllerNameConvention provides three implementations out-of-the-box.

Default

ControllerNameConvention.Default provides the default configuration which extends the original convention to have the form: [Name][#]Controller. This means that if you already have a HelloWorldController, you can now have a HelloWorld2Controller and HelloWorld3Controller. Each type name removes the Controller suffix as well as any trailing numbers. All of these controllers would end up named and grouped HelloWorld.

Original

ControllerNameConvention.Original provides an alternate configuration that retains the original naming convention. Consider that you have a type named S3Controller. In this scenario, you do not want the 3 to be stripped away. If you have multiple versions of a such a controller, you would need your own implementation that understands this behavior or separate the types into different .NET namespaces.

Grouped

ControllerNameConvention.Grouped is a hybrid configuration the combines the Default and Original conventions. For the purposes of the name, the original convention is used. For the purposes of grouping, the default convention is used. A controller type of S3Controller would have the name S3, but the group name S. The group name is only used for collation and is never displayed anywhere, so this behavior is acceptable.

Attribute

If you do not want to rely on a convention, you can explicitly provide a name using the ControllerNameAttribute. The name provided will be used verbatim for the [controller] token, the controller name, and for grouping. This attribute is particularly useful with OData because the name of the controller must also exactly match the name of the associated entity set.

[ApiVersion( 2.0 )]
[ControllerName( "HelloWorld" )]
[Route( "[controller]" )]
public class HelloWorld2Controller : ControllerBase
{
    [HttpGet]
    public string Get() => "Hello world v2.0!";
}

Minimal APIs

Minimal APIs do not use controllers nor any of these conventions or attributes. The intrinsic grouping capabilities define collation without having to infer anything. It is, however, possible to add a logical API name to the group if you want to:

var builder = WebApplication.CreateBuilder( args );

builder.Services.AddProblemDetails();
builder.Services.AddApiVersioning();

var app = builder.Build();
var people = app.NewVersionedApi( "People" ); // ← provides optional, logical name

people.MapGet( "/people", () => new[] { new Person() } ).HasApiVersion( 1.0 );

app.Run();

Supported API Versioning Methods

Several API versioning methods are supported out-of-the-box:

Multiple methods of API versioning can be supported simultaneously. Use the ApiVersionReader.Combine method to compose two or more IApiVersionReader instances together. You can also implement your own method of extracting the requested API version using a custom IApiVersionReader.

Clone this wiki locally