Christian Schou
  • Home
  • Blog
    • Programming
      • C#
      • PowerShell
      • Python
      • SQL
    • WordPress
      • Tutorials
    • Cloud
    • Home Automation
      • Home Assistant
        • Node-Red
    • Career
  • Services
  • Glossary
  • About
No Result
View All Result
Christian Schou
  • Home
  • Blog
    • Programming
      • C#
      • PowerShell
      • Python
      • SQL
    • WordPress
      • Tutorials
    • Cloud
    • Home Automation
      • Home Assistant
        • Node-Red
    • Career
  • Services
  • Glossary
  • About
No Result
View All Result
Christian Schou
No Result
View All Result
Home Programming C#
api versioning

How to use API versioning in ASP.NET Core Web API and integrate it with Swagger using .NET 6

by Christian
3. Mai 2022
in C#
2

What is API versioning? A question I see a lot. I’m mostly working on the backend and have many times exposed an API to clients they could consume to get data about their order, shipments, invoice, purchase data, etc… I’m sure you might have done the same or are about to? Anyway – when doing that, most developers agree that APIs should not be changed that much once implemented. Then we have to make a new version (here it comes: API versioning) with the new functionality in order not to break things on the other end.

If you have been consuming third-party APIs, I’m sure that you might have experienced that you would have to type something like https://<domain>/api/v2/... to get the resource you need. By implementing API versioning like in the above URL, we can easily add new versions of the API.

With that said, let’s move on and find out how you easily can implement API versioning in your own .NET Core Web API. The project is build on top of my previous article “How to add localization in ASP.NET Core Web APIs with Caching” – you can ignore the localization part in the project – it will not have any impact on the API functionality.

Inhaltsverzeichnis
  1. What are the benefits of using API versioning?
  2. Why use API Versioning?
  3. Implementing API Versioning in .NET Core Web API using .NET 6
  4. Update controllers to accept API versioning
  5. Integrate .NET Core API Versioning with Swagger
    • Fetch the version of our APIs with ApiExplorer
    • Configure SwaggerOptions to register all API versions
  6. Test Swagger with API Versioning v1 and v2
  7. Summary

What are the benefits of using API versioning?

Let’s agree that API versioning is not a thing many developers are thinking of when creating a new project – Am I right? After I first started implementing versions I do it every I create a new project to make everything easier for myself in the future.

Here is an example: let’s say that you would like to offer your customers a way to get a list of their current processing orders. Time passes everything is good, but one day you need to make a breaking change in the API that would cause all clients to stop retrieving data if you update the endpoint they are using – and if there is one thing don’t like, then it’s angry customers – they should be happy all the time. What to do?

The first thing we need to do is to make sure that none of our clients will be affected by this change unless they choose to by themselves. From my perspective, there are three ways to go around this, and I only like one of them:

  1. We could talk with all our customers and make them change their apps at the same time as we deploy the new solution to our host (Not recommended)
  2. We could add a new method (new URL) in our API that would serve the new model for the endpoint and ask the customer to ypdate their app accorindgly. (Not recommended)
  3. We could make use of API versioning and just give the customer the option to change the version parameter in the URL when they are ready for it. (Recommended)

Why use API Versioning?

The primary reason we would offer API versioning is that the same method is able to be called in the same way, but with a different version number in the URL. This gives us the option to serve the data to clients that we would like, but our customers are also able to wait and update their applications when they got time for it in their systems. Sounds good? Let’s see how you can achieve just that in your .NET Core Web API.

Implementing API Versioning in .NET Core Web API using .NET 6

For this tutorial I will be using .NET 6, please make sure you have that installed on your computer. You also have to install the following package:

Install-Package Microsoft.AspNetCore.Mvc.Versioning

The first thing you have to do is go to your program.cs file and add the following code to the services section:

builder.Services.AddApiVersioning(opt =>
                                    {
                                        opt.DefaultApiVersion = new Microsoft.AspNetCore.Mvc.ApiVersion(1,0);
                                        opt.AssumeDefaultVersionWhenUnspecified = true;
                                        opt.ReportApiVersions = true;
                                        opt.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(),
                                                                                        new HeaderApiVersionReader("x-api-version"),
                                                                                        new MediaTypeApiVersionReader("x-api-version"));
                                    });

The ReportAPIVersions flag is optional, but it can be useful. The function of that one is that it allows for the API to return versions in the response header. When a client is calling your API they will see a flag with available options for that method.

api versioning, header
Supported API versions in the header response

The first flag named DefaultApiVersion is important. It will set v1 as the default version number if a client forgets to set that themselves.

Second-line AssumeDefaultVersionWhenUnspecified (quite a mouthful 🙂 ) can be handy when migrating an API without API versioning to supporting versioning. If you do not set this, you are gonna break your client’s applications if they do not specify a version in the API call. If your client gets an error saying: “An API version is required, but was not specified.” – then you know why.

ApiVersionReader makes it possible for our clients to choose whether they would like to place the parameter in the URL or in the request header. The reasons are:

  • UrlSegmentApiVersionReader – this method allow us to get the version number from the URL, ex: https://domain.com/api/v1/metod
  • HeaderApiVersionReader – makes it possible to get the version number from a header parameter, ex: x-api-version:1.0
  • MediaTypeApiVersionReader – gives us the option to get the parameter from the header again, but this time it’s placed within the Content-Type or Accept flags, ex: Accept/Content-Type: application/json; x-api-version=1.0

Update controllers to accept API versioning

Now that our configuration is in place, we need to update our controllers to accept the API versions we would like to offer our clients. It’s possible to set a whole controller to be within one version or do it on one specific action in the controller.

Below is a configuration on the controller that allows us to make the controller work in both version 1.0 and 2.0. Underneath the [ApiController] attribute you find the Route for the controller accepting versions.

[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class WeatherForecastController : ControllerBase

If you would like to map each action to a specific version, you can use the attribute [MapToApiVersion] to control it.

using Microsoft.AspNetCore.Mvc;

namespace LocalizationAPI.Controllers.v1
{
    [ApiController]
    [Route("api/v{version:apiVersion}/[controller]")]
    [ApiVersion("1.0")]
    public class WeatherForecastController : ControllerBase
    {
        [MapToApiVersion("1.0")]
        [HttpGet]
        public string Get() => ".Net Core Web API Version 1";
    }
}

using Microsoft.AspNetCore.Mvc;

namespace LocalizationAPI.Controllers.v2
{
    [ApiController]
    [Route("api/v{version:apiVersion}/[controller]")]
    [ApiVersion("2.0")]
    public class WeatherForecastController : ControllerBase
    {
        [MapToApiVersion("2.0")]
        [HttpGet]
        public string Get() => ".Net Core Web API Version 2";
    }
}

It is possible to create a new version of the action and not the entire controller as you have seen above. A word of warning (from my own learning and experience). At some point in the future, you will get a mismatch of the controller, actions, versions, etc… When you get a big solution with lots of those, it will get hard to manage. Instead, I would always go for a more structured way.

Best practice is to place actions in different controllers in sub-folders named v1, v2, etc… inside the Controllers folder. This will give you a more clean solution that will be easier for you and other developers to maintain in the future.

Below is a screenshot of how I would place the controllers:

api versioning
API Versioning folders

Okay, this gives us two endpoints we can access

curl -X 'GET' \
  'https://localhost:7013/api/v1/WeatherForecast' \
  -H 'accept: text/plain; x-api-version=1.0'

Response Headers
 api-supported-versions: 1.0,2.0 
 content-type: text/plain; charset=utf-8; x-api-version=1 
 date: Mon,10 Jan 2022 20:35:22 GMT 
 server: Kestrel 

Response body
 .Net Core Web API Version 1
curl -X 'GET' \
  'https://localhost:7013/api/v2/WeatherForecast' \
  -H 'accept: text/plain; x-api-version=2.0'

Response Headers
 api-supported-versions: 1.0,2.0 
 content-type: text/plain; charset=utf-8; x-api-version=2 
 date: Mon,10 Jan 2022 20:35:10 GMT 
 server: Kestrel 

Response body
 .Net Core Web API Version 2

Integrate .NET Core API Versioning with Swagger

I’m a big fan of Swagger and I often expose Swagger documentation for my APIs to clients in order to show the API solution I have made and to give them an easier way to test and implement it into their own applications.

If you created a solution based on the .NET Core Web API template you would already have the Swashbuckle.AspNetCore nuget. If you started out from a blank solution you can get this package by running this command in your package manager console: Install-Package Swashbuckle.AspNetCore and then add the required services and middleware to your program.cs file.

Your program.cs file should look like the following:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.

builder.Services.AddControllers();

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddApiVersioning(opt =>
                                    {
                                        opt.DefaultApiVersion = new ApiVersion(1, 0);
                                        opt.AssumeDefaultVersionWhenUnspecified = true;
                                        opt.ReportApiVersions = true;
                                        opt.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(),
                                                                                        new HeaderApiVersionReader("x-api-version"),
                                                                                        new MediaTypeApiVersionReader("x-api-version"));
                                    });

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI();
}

app.UseRequestLocalization(options);
app.UseStaticFiles();
app.UseMiddleware<LocalizerMiddleware>();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

When launching the application wight now, we would get an error telling us that we have an unhandled exception while executing the request.

Fetch the version of our APIs with ApiExplorer

The first thing we do is implementing logic to get information about version for each of our endpoints. To achieve this we have to add the nuget package: Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer. This package is responsible for providing metadata for the APIs (of course based on how we decorate them with attributes). In this guide it will return the Version information for each of our actions. You can install it with this command:

Install-Package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
dotnet add package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer

Next off we have to add logic to program.cs to get the ApiExplorer to work:

var builder = WebApplication.CreateBuilder(args);

// Add services to the container.
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.AddApiVersioning(opt =>
                                    {
                                        opt.DefaultApiVersion = new ApiVersion(1, 0);
                                        opt.AssumeDefaultVersionWhenUnspecified = true;
                                        opt.ReportApiVersions = true;
                                        opt.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(),
                                                                                        new HeaderApiVersionReader("x-api-version"),
                                                                                        new MediaTypeApiVersionReader("x-api-version"));
                                    });
// Add ApiExplorer to discover versions
builder.Services.AddVersionedApiExplorer(setup =>
{
    setup.GroupNameFormat = "'v'VVV";
    setup.SubstituteApiVersionInUrl = true;
});

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();

Then we inject the ApiExplorer into the configuration part of program.cs. This is the part that allows us to use the collected metadata in our SwaggerUI middleware. Awesome!

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(options =>
    {
        foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
        {
            options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json",
                description.GroupName.ToUpperInvariant());
        }
    });
}

app.UseStaticFiles();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();
var apiVersionDescriptionProvider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseSwagger();
    app.UseSwaggerUI(options =>
    {
        foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions.Reverse())
        {
            options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json",
                description.GroupName.ToUpperInvariant());
        }
    });
}

app.UseStaticFiles();

app.UseHttpsRedirection();

app.UseAuthorization();

app.MapControllers();

app.Run();

By using the Reverse() method on our IApiVersionDescriptionProvider, we can control what version is showed first in the Swagger documentation for our API. I like to add Reverse() as it will show the most recent version of the API to the client and not confuse them with old deprecated versions of the API.

If we launch it now, Swagger would not pickup all versions of our API… How can that be?! Well we need to configure some options for Swagger. The best way to do that is by extending IConfigurationNamedOptions<SwaggerGenOptions> with a few methods. Lets do just that.

Configure SwaggerOptions to register all API versions

Right now Swagger is only generating documentation for one of our versions – v1. If we only had a single version of our API we could easily create the documentation inside the SwaggerGen() service. Well… I dont like to hardcode things (only if i really have to). To get around that we have to create a new class named ConfigureSwaggerOptions in the root of our project.

The code you are gonna need inside that class looks like the following:

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

    public ConfigureSwaggerOptions(
        IApiVersionDescriptionProvider provider)
    {
        _provider = provider;
    }

    /// <summary>
    /// Configure each API discovered for Swagger Documentation
    /// </summary>
    /// <param name="options"></param>
    public void Configure(SwaggerGenOptions options)
    {
        // add swagger document for every API version discovered
        foreach (var description in _provider.ApiVersionDescriptions)
        {
            options.SwaggerDoc(
                description.GroupName,
                CreateVersionInfo(description));
        }
    }

    /// <summary>
    /// Configure Swagger Options. Inherited from the Interface
    /// </summary>
    /// <param name="name"></param>
    /// <param name="options"></param>
    public void Configure(string name, SwaggerGenOptions options)
    {
        Configure(options);
    }

    /// <summary>
    /// Create information about the version of the API
    /// </summary>
    /// <param name="description"></param>
    /// <returns>Information about the API</returns>
    private OpenApiInfo CreateVersionInfo(
            ApiVersionDescription desc)
    {
        var info = new OpenApiInfo()
        {
            Title = ".NET Core (.NET 6) Web API",
            Version = desc.ApiVersion.ToString()
        };

        if (desc.IsDeprecated)
        {
            info.Description += " This API version has been deprecated. Please use one of the new APIs available from the explorer.";
        }

        return info;
    }
}

Then we only have to Configure our newly created Swagger Options Configuration class in program.cs.

// Add services to the container.
builder.Services.AddControllers();

builder.Services.AddApiVersioning(opt =>
                                    {
                                        opt.DefaultApiVersion = new ApiVersion(1, 0);
                                        opt.AssumeDefaultVersionWhenUnspecified = true;
                                        opt.ReportApiVersions = true;
                                        opt.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(),
                                                                                        new HeaderApiVersionReader("x-api-version"),
                                                                                        new MediaTypeApiVersionReader("x-api-version"));
                                    });

builder.Services.AddVersionedApiExplorer(setup =>
{
    setup.GroupNameFormat = "'v'VVV";
    setup.SubstituteApiVersionInUrl = true;
});

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

builder.Services.ConfigureOptions<ConfigureSwaggerOptions>();

var app = builder.Build();

Test Swagger with API Versioning v1 and v2

Now for the fun part – fire up your Web API and check if it works. You should now have a Swagger interface where you are able to select from two versions like I have done in the video below:

Swagger with API Versioning (.NET 6 ) .NET Core Web API

Summary

Out of the box Swagger API Documentation is ready to use when creating a project using a template. The problem comes in when we start to create multiple versions of our routes – I can easily mess things up, even for the most organized. By using ApiExplorer we can achieve a nice and quite easy way to create api versioning for our API.

Instead of relying on the default SwaggerGen() method we configured some new Swagger Configuration Options by giving it a NamedOptions implementation to instruct the API how to handle verisons at runtime. In this short tutorial you learned about:

  • Swagger Integration.
  • API Versioning.
  • How to integrate API versioning in Swagger Documentation.

If you got any issues, questions or suggestions, please let me know in the comments. Happy coding!

Tags: .NET 6.Net CoreAPIASP.NET CoreC#DevelopmentWeb
Previous Post

How to scrape prices from competitors e-commerce websites using Python

Next Post

How to make ASP.NET Core API Documentation using Swagger and ReDoc (.NET 6)

Christian

Christian

Hello 👋 My name is Christian and I am 26 years old. I'm an educated Software Developer with a primary focus on C#, .NET Core, Python, and PowerShell. Currently, I'm expanding my skills in Software Robots and Cloud Architecture. In some of my spare time, I share my knowledge about tech stuff on my blog.

Related Posts

watchdog
ASP.NET Core

The #1 guide to show real-time .NET 6 logs for Web Apps and APIs in a modern way using WatchDog for Free

by Christian
4. August 2022
0

A reader recently asked me for a more modern way to view log files for requests and exceptions in a...

Read more
restful web api

How to build a RESTful Web API using ASP.NET Core and Entity Framework Core (.NET 6)

25. Juli 2022
dynamically register entities

How to Dynamically Register Entities in DbContext by Extending ModelBuilder?

23. Juli 2022
Dockerize ASP.NET Core

How to Compose an ASP.NET Core Web API (.NET 6) with an MS SQL Server 2022 on Linux in Docker

19. Juli 2022
pattern matching in switch

How to do pattern matching in switch statements – C# version >= 7.0

11. Juli 2022
Next Post
api documentation using swagger

How to make ASP.NET Core API Documentation using Swagger and ReDoc (.NET 6)

Comments 2

  1. George says:
    4 Monaten ago

    Hi Christian, Thanks for a great blog. This saved me a ton of time and I learned a lot to boot. One issue I did want to bring up is in the ConfigureSwaggerOptions class. You need to replace the code with the following:

    private readonly IApiVersionDescriptionProvider _provider;

    public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
    {
    _provider = provider;
    }

    Otherwise it will generate a compile error.

    Antworten
    • Christian Schou says:
      3 Monaten ago

      Hi George

      Happy to hear that you learned something from it. I totally get what you mean, I have made a typo when writing the code from my solution to the blog. I have updated the post to reflect your comment.

      Thank you! 🙂

      Antworten

Schreibe einen Kommentar Antworten abbrechen

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert

Christian Schou

Christian Schou

Software Developer

Hello - my name is Christian and I am 26 years old. I'm an educated Software Developer with a primary focus on C#, .NET Core, Python, and PowerShell. Currently, I'm expanding my skills in Software Robots and Cloud Architecture. In some of my spare time, I share my knowledge about tech stuff on my blog.

Recent articles

personal website
Career

Top 6 things to add on your personal website to get hired for a tech job

by Christian
7. August 2022
0

Back in the days before the internet was a thing like it is today, we used to have business cards...

Read more
watchdog

The #1 guide to show real-time .NET 6 logs for Web Apps and APIs in a modern way using WatchDog for Free

4. August 2022
get hired for a tech job

5 tips to help you get hired for a tech job

31. Juli 2022
restful web api

How to build a RESTful Web API using ASP.NET Core and Entity Framework Core (.NET 6)

25. Juli 2022
dynamically register entities

How to Dynamically Register Entities in DbContext by Extending ModelBuilder?

23. Juli 2022

Christian Schou

Software Developer

Hello - my name is Christian and I am 26 years old. I'm an educated Software Developer with a primary focus on C#, .NET Core, Python, and PowerShell. Currently, I'm expanding my skills in Software Robots and Cloud Architecture. In some of my spare time, I share my knowledge about tech stuff on my blog.

Recent articles

personal website

Top 6 things to add on your personal website to get hired for a tech job

7. August 2022
watchdog

The #1 guide to show real-time .NET 6 logs for Web Apps and APIs in a modern way using WatchDog for Free

4. August 2022
get hired for a tech job

5 tips to help you get hired for a tech job

31. Juli 2022
  • de_DEDeutsch
    • da_DKDansk
    • en_USEnglish
    • hi_INहिन्दी
    • pt_BRPortuguês do Brasil
  • Contact
  • Datenschutzrichtlinie
  • Nutzungsbedingungen

© 2022 Christian Schou - All rights reserved.

No Result
View All Result
  • Home
  • Blog
    • Programming
      • C#
      • PowerShell
      • Python
      • SQL
    • WordPress
      • Tutorials
    • Cloud
    • Home Automation
      • Home Assistant
    • Career
  • Services
  • Glossary
  • About

© 2022 Christian Schou - All rights reserved.

I use cookies on my website to give you the most relevant experience by remembering your preferences and repeat visits. By clicking “Accept”, you consent to the use of ALL the cookies.
Do not sell my personal information.
Cookie settingsACCEPT
Privacy & Cookies Policy

Privacy Overview

This website uses cookies to improve your experience while you navigate through the website. Out of these cookies, the cookies that are categorized as necessary are stored on your browser as they are essential for the working of basic functionalities of the website. We also use third-party cookies that help us analyze and understand how you use this website. These cookies will be stored in your browser only with your consent. You also have the option to opt-out of these cookies. But opting out of some of these cookies may have an effect on your browsing experience.
Necessary
immer aktiv
Necessary cookies are absolutely essential for the website to function properly. This category only includes cookies that ensures basic functionalities and security features of the website. These cookies do not store any personal information.
Functional
Functional cookies help to perform certain functionalities like sharing the content of the website on social media platforms, collect feedbacks, and other third-party features.
Performance
Performance cookies are used to understand and analyze the key performance indexes of the website which helps in delivering a better user experience for the visitors.
Analytics
Analytical cookies are used to understand how visitors interact with the website. These cookies help provide information on metrics the number of visitors, bounce rate, traffic source, etc.
CookieDauerBeschreibung
__gads1 year 24 daysThe __gads cookie, set by Google, is stored under DoubleClick domain and tracks the number of times users see an advert, measures the success of the campaign and calculates its revenue. This cookie can only be read from the domain they are set on and will not track any data while browsing through other sites.
_ga2 yearsThe _ga cookie, installed by Google Analytics, calculates visitor, session and campaign data and also keeps track of site usage for the site's analytics report. The cookie stores information anonymously and assigns a randomly generated number to recognize unique visitors.
_ga_0J2F6JVWSD2 yearsThis cookie is installed by Google Analytics.
_gat_gtag_UA_84232734_11 minuteSet by Google to distinguish users.
_gid1 dayInstalled by Google Analytics, _gid cookie stores information on how visitors use a website, while also creating an analytics report of the website's performance. Some of the data that are collected include the number of visitors, their source, and the pages they visit anonymously.
YouTube2 yearsYouTube sets this cookie via embedded youtube-videos and registers anonymous statistical data. I embed YouTube videos in my articles/tutorials - you won't get the full experience of the articles if this is deactivated.
Advertisement
Advertisement cookies are used to provide visitors with relevant ads and marketing campaigns. These cookies track visitors across websites and collect information to provide customized ads.
CookieDauerBeschreibung
IDE1 year 24 daysGoogle DoubleClick IDE cookies are used to store information about how the user uses the website to present them with relevant ads and according to the user profile.
test_cookie15 minutesThe test_cookie is set by doubleclick.net and is used to determine if the user's browser supports cookies.
VISITOR_INFO1_LIVE5 months 27 daysA cookie set by YouTube to measure bandwidth that determines whether the user gets the new or old player interface.
YSCsessionYSC cookie is set by Youtube and is used to track the views of embedded videos on Youtube pages.
yt-remote-connected-devicesneverYouTube sets this cookie to store the video preferences of the user using embedded YouTube video.
yt-remote-device-idneverYouTube sets this cookie to store the video preferences of the user using embedded YouTube video.
Others
Other uncategorized cookies are those that are being analyzed and have not been classified into a category as yet.
SPEICHERN & AKZEPTIEREN
Unterstützt von CookieYes Logo