dateo. Coding Blog

Coding, Tech and Developers Blog

ASP.NET
.NET
Swagger

How to version your ASP.NET API

Dennis Frühauff on October 4th, 2023

In this article, we want to take a look at the introduction of proper versioning into your ASP.NET Core WebApi project. You will see how to set up both your application as well as your Swagger documentation, which you might have.


If you followed along closely, you might remember our article on best practices regarding ASP.NET controllers (APIs). In that article, I made the point that it will always be a good idea to version your API. I have spent years of my career both using and designing APIs of different quality. Let's take a look at why versioning is worth implementing and how you would actually do that in ASP.NET Core 7.


As usual, you can find the complete code for this article on GitHub. Let's dive right in.


Why versioning is a good idea

There are a couple of reasons that come to mind that support implementing versioning in your next or current API project:


  • Flexibility: As requirements change, your API might have to change. Versioning lets you safely do this.
  • Backward compatibility: Versioning allows you to make changes to your API, even breaking changes, without forcing your users to instantly adopt them. Remember Hyrum's law?
  • Client-specific tailoring: You can also think of versioning not as a temporal advancement of your software, but rather as different alternatives of your API for different users and requirements.
  • Deprecation: With versioning, you can mark specific endpoints as deprecated, thereby encouraging users to move to a newer version and smoothing their experience.
  • Client trust: Seeing a version strategy lets me trust an API more because I see that the authors are willing to adapt, if necessary.

You might agree that versioning public APIs is a good idea in general. Hell, that is why NuGet packages have versions, for that matter. So let's implement it in our application in .NET 7.


Implementing versioning in ASP.NET Core 7

It is actually very unfortunate that the official MS versioning documentation on GitHub is not at all up to date and almost useless on the latest framework versions of .NET. And since my head seems to forget the setup code every few weeks, I thought I'd create an article on this topic as a reference implementation.


The first thing we'll have to do is create a new ASP.NET WebApi project in .NET 7.
Then, we can add the following two NuGet packages to our application:


Asp.Versioning.Mvc
Asp.Versioning.Mvc.ApiExplorer

*Note: There are also Microsoft.AspNetCore.Mvc.Versioning packages around - these are the old ones and, sort of, deprecated.


The basic setup

Now, let's implement a simple controller that has a version attribute to it:


[ApiController]
[ApiVersion(1.0)]
[Route("api/v{version:apiVersion}/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly ProductsService productsService;

    public ProductsController(ProductsService productsService)
    {
        this.productsService = productsService;
    }

    [HttpGet("{productId}")]
    [ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> GetProductById([FromRoute] int productId)
    {
        var product = await this.productsService.FindProductByIdAsync(productId);
        
        return product is null 
            ? this.NoContent() 
            : this.Ok(product);
    }
}

In this snippet, we are creating a class for our products (every example should have products or customers, right?) and assigning an actual version to it: [ApiVersion(1.0)].
We are also setting up the conventions for the specific route with the Route attribute.
In this specific example, we will later be able to call the API with one of the following routes


http://localhost:5182/api/v1.0/Products/1
http://localhost:5182/api/v1/Products/1

and the response will be


{
  "name": "Hat",
  "description": "The only available hat"
}

(If haven't shown you the ProductsService, you'll get the point.)


Before we can actually start the application, we need to add the following lines to our Program.cs to make sure that the necessary services are actually registered:


builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1.0);
    options.ReportApiVersions = true;
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
});

Versioning and Swagger

So that looks good already, but if you run the application and take a look at the Swagger page, you'll notice that it looks something like this:
Screenshot 1
If people (developers, users) want to try out the API via Swagger, they'll have to provide the version parameter - that's inconvenient. Let's automate that away.


The only thing we'll need to do is configure the API explorer in our Program.cs. The complete setup code now looks like this:


builder.Services.AddApiVersioning(options =>
    {
        options.DefaultApiVersion = new ApiVersion(1.0);
        options.ReportApiVersions = true;
        options.ApiVersionReader = new UrlSegmentApiVersionReader();
    })
    .AddApiExplorer(options =>
    {
        options.SubstituteApiVersionInUrl = true;
        options.GroupNameFormat = "'v'VVV";
        options.AssumeDefaultVersionWhenUnspecified = true;
    });

With this in place, you'll notice that the version parameter is gone from the Swagger page. Instead, the proper version is already part of the example URL to try out - this is much better.


Adding a version

Fortunately, all of the existing code already supports having more than one version of endpoints. It will basically discover all of the versions that we specified somewhere in our code and add them to its list. Let's add another method to the controller and assume that it will be part of our version 2.0:


[ApiController]
[ApiVersion(1.0)]
[ApiVersion(2.0)]
[Route("api/v{version:apiVersion}/[controller]")]
public class ProductsController : ControllerBase
{
    private readonly ProductsService productsService;

    public ProductsController(ProductsService productsService)
    {
        this.productsService = productsService;
    }

    [HttpGet("{productId}")]
    [ProducesResponseType(typeof(ProductDto), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> GetProductById([FromRoute] int productId)
    {
        var product = await this.productsService.FindProductByIdAsync(productId);

        return product is null
            ? this.NoContent()
            : this.Ok(ProductDto.FromProduct(product));
    }

    [HttpGet]
    [MapToApiVersion(2.0)]
    [ProducesResponseType(typeof(IList<ProductDto>), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> SearchByProductName([FromQuery] string productName)
    {
        var products = await this.productsService.FindProductsByNameAsync(productName);

        return products.Any() == false
            ? this.NoContent()
            : this.Ok(products.Select(ProductDto.FromProduct));
    }
}

With the two lines at the top, we are telling the API explorer that this controller will contain endpoints for two different versions. If we did not specify that, our 2.0 endpoint would not be accessible. Specifying the version is also possible on the method level, which might be more explicit and understandable.


Our first endpoint, since it is not explicitly mapped to a specific version, will be available in both 1.0 and 2.0, i.e., products can be queried by ID using one of:


http://localhost:5182/api/v1.0/Products/1
http://localhost:5182/api/v1/Products/1
http://localhost:5182/api/v2.0/Products/1
http://localhost:5182/api/v2/Products/1

The second endpoint, however, is explicitly mapped to version 2.0, and it will only be available there. Consequently, products can only be searched for by using one of:


http://localhost:5182/api/v2/Products?productName=hat
http://localhost:5182/api/v2.0/Products?productName=hat

So that seems promising so far. But if you start up the application now, you will only see the endpoint for version 1.0 and there seems to be no way of switching between the versions - how do we fix that?


Switching between versions in Swagger UI

The Swagger page already has that Select a definition dropdown in place, we just need to fill it with life. Unfortunately, that involves a few more lines of code than one would expect.


First, we need a configuration class that creates Swagger documentation for all of our versions:


public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
    private readonly IApiVersionDescriptionProvider _provider;

    public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider) => _provider = provider;

    public void Configure(SwaggerGenOptions options)
    {
        foreach (var description in _provider.ApiVersionDescriptions)
        {
            options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
        }
    }

    private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
    {
        var info = new OpenApiInfo()
        {
            Title = "Sandbox.ApiVersioning",
            Version = description.ApiVersion.ToString(),
        };

        if (description.IsDeprecated)
        {
            info.Description += " Deprecated.";
        }

        return info;
    }
}

Next, we need a class that will properly format all of our different API versions for us automatically. In Swagger, this can be done by implementing an IOperationFilter


public class SwaggerDefaultValues : IOperationFilter
{
    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        var apiDescription = context.ApiDescription;
        operation.Deprecated |= apiDescription.IsDeprecated();

        foreach (var responseType in context.ApiDescription.SupportedResponseTypes)
        {
            var responseKey = responseType.IsDefaultResponse ? "default" : responseType.StatusCode.ToString();
            var response = operation.Responses[responseKey];

            foreach (var contentType in response.Content.Keys)
            {
                if (responseType.ApiResponseFormats.All(x => x.MediaType != contentType))
                {
                    response.Content.Remove(contentType);
                }
            }
        }

        if (operation.Parameters == null)
        {
            return;
        }

        foreach (var parameter in operation.Parameters)
        {
            var description = apiDescription.ParameterDescriptions.First(p => p.Name == parameter.Name);

            parameter.Description ??= description.ModelMetadata.Description;

            if (parameter.Schema.Default is null 
                && description.DefaultValue is not null 
                && description.DefaultValue is not DBNull)
            {
                var json = JsonSerializer.Serialize(description.DefaultValue, description.ModelMetadata.ModelType);
                parameter.Schema.Default = OpenApiAnyFactory.CreateFromJson(json);
            }

            parameter.Required |= description.IsRequired;
        }
    }
}

The full setup code will now become this:


builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
builder.Services.AddSwaggerGen(options =>
{
    options.OperationFilter<SwaggerDefaultValues>();
});
builder.Services.AddTransient<ProductsService>();
builder.Services.AddApiVersioning(options =>
    {
        options.DefaultApiVersion = new ApiVersion(1.0);
        options.ReportApiVersions = true;
        options.ApiVersionReader = new UrlSegmentApiVersionReader();
    })
    .AddApiExplorer(options =>
    {
        options.SubstituteApiVersionInUrl = true;
        options.GroupNameFormat = "'v'VVV";
        options.AssumeDefaultVersionWhenUnspecified = true;
    });

And, lastly, in order to make switching between the versions in the Swagger UI possible, we change app.UseSwaggerUI() at the bottom of Program.cs to


app.UseSwaggerUI(options =>
    {
        var descriptions = app.DescribeApiVersions();
      
        foreach (var description in descriptions)
        {
            var url = $"/swagger/{description.GroupName}/swagger.json";
            var name = description.GroupName;
            options.SwaggerEndpoint(url, name);
        }
    });

If you start the application now, you'll notice that you can easily switch between the different versions using the drop-down at the top.


Deprecation and status information

The ApiVersion attribute lets you specify two additional things on your API


[ApiVersion(1.0, Deprecated = true)]

Specifying deprecation on a version will encourage your users to move away from this version while not making it useless right away. On the Swagger page, you'll notice that the corresponding endpoints are highlighted in gray. API users will be informed of the status of your API versions in the HTTP headers of your responses:


 api-deprecated-versions: 1.0  
 api-supported-versions: 2.0,2.1-workinprogress  
 content-type: application/json; charset=utf-8  
 date: Sat,23 Sep 2023 16:33:37 GMT  
 server: Kestrel  transfer-encoding: chunked 

This is automatically done for us because we set options.ReportApiVersions = true; in our Program.cs.


The other thing you can do is apply a status tag to any of your versions:


[ApiVersion(2.1, status: "workinprogress")]

This tag will automatically be added as an addition to the full version string, e.g.


http://localhost:5182/api/v2.1-workinprogress/Products/1

With this, you can have more telling version strings, e.g., for developer versions or specific customer and test versions.


Conclusion

I guess that is all to get you going for now. I hope this tutorial can serve as a reference for those of you willing to implement versioning on the latest .NET versions.
Feel free to play around with the code on GitHub as well.



Please share on social media, stay in touch via the contact form, and subscribe to our post newsletter!

Be the first to know when a new post was released

We don’t spam!
Read our Privacy Policy for more info.

We use cookies on our website to give you the most relevant experience by remembering your preferences and repeat visits. By clicking “Accept All”, you consent to the use of ALL the cookies. However, you may visit "Cookie Settings" to provide a controlled consent.