Read time: 4 minutes

Today I’ll dive into a topic that is often overlooked by developers when building APIs: versioning.

Versioning is a critical aspect of API design, as it allows you to make changes to your API without breaking existing clients.

Sadly, many developers don’t think about versioning until it’s too late, and they are forced to make breaking changes to their APIs, which can lead to a lot of headaches and unhappy clients.

But the good thing is that you can start versioning your ASP.NET Core APIs very quickly with the help of a cool NuGet package.

Let’s dive in.


Why version your APIs?

As more and more of your API endpoints start getting consumed by your clients, it will become increasingly difficult to make changes to the API without breaking existing clients.

For instance, say today you have an API endpoint that returns the details of a video game. Today, the response this endpoint sends back might look like this:

{
  "id": 2, 
  "name": "FIFA 23",
  "price": 69.99
}

Now, let’s say that, as it often happens, business requirements change in a couple of ways:

  1. Our game store is expanding globally and prices need to include currency information to avoid confusion
  2. Customers want to know the genre each game belongs to, so they can make a more informed decision

So we change our API endpoint response to address these new requirements:

{
  "id": 2,
  "price": "USD 69.99",
  "details": {
    "title": "FIFA 23",
    "genre": "Sports"
  }
}

However, if we make this change in the existing API endpoint, we will break all the clients that are currently consuming this endpoint, because:

  • They are not expecting the price to be a string with currency information
  • The name field has been renamed to title and is now nested under a new details property

So, instead of introducing such dramatic changes to the existing API, we can create a new version of the API that includes these changes, and let the clients decide when they are ready to consume the new version of the API.

Let’s see how to implement this in 5 quick steps:


Step 1: Add a new version specific DTO

Instead of modifying our existing Game DTO, we can create new version-specific DTOs that represent the updated endpoint response:

public record GameSummaryDtoV2(int Id, string Price, GameDetailsDtoV2 Details);

public record GameDetailsDtoV2(string Title, string Genre);

We may also need to add new mapping logic to turn Game entities into the new DTO format:

public static GameSummaryDtoV2 AsDtoV2(this Game game)
{
    return new GameSummaryDtoV2(
        game.Id,
        "USD " + game.Price,
        new GameDetailsDtoV2(
            game.Name,
            game.Genre
        )
    );
}   


Step 2: Implement the new V2 endpoint

We will leave the current endpoint as is, and create a new endpoint that returns the response using our new GameSummaryDtoV2 format:

app.MapGet("games/{id}", async (IGamesRepository repository, int id) =>
{
    Game? game = await repository.GetAsync(id);
    return game is not null ? Results.Ok(game.ToDtoV2()) : Results.NotFound();
});

But the problem is that even when we return the new response format, we can’t have the exact same route as the existing endpoint, because that would cause a conflict.

For reference, here’s what our V1 endpoint looks like:

app.MapGet("games/{id}", async (IGamesRepository repository, int id) =>
{
    Game? game = await repository.GetAsync(id);
    return game is not null ? Results.Ok(game.AsDtoV1()) : Results.NotFound();
});

How to deal with this?

Time to introduce versioning.


Step 3: Add the ASP.NET API Versioning NuGet package

You can certainly implement versioning manually, but you can save some time by using the popular Asp.Versioning.Http NuGet package instead:

dotnet add package Asp.Versioning.Http

This package provides a set of conventions and attributes that make it easy to version your APIs without having to write a lot of boilerplate code or learn new routing concepts.

Let’s see how to use it.


Step 4: Implement API versioning

First thing to do is register the API versioning services in Program.cs:

builder.Services.AddApiVersioning();

Next, we need to add a new route group builder that can be used to define all versioned endpoints:

var gamesGroup = app.NewVersionedApi()
                    .MapGroup("/games")
                    .HasApiVersion(1.0)
                    .HasApiVersion(2.0);

And now we can use the group builder to specify to which API version each of our endpoints belongs:

gamesGroup.MapGet("/{id}", async (IGamesRepository repository, int id) =>
{
    Game? game = await repository.GetAsync(id);
    return game is not null ? Results.Ok(game.AsDtoV1()) : Results.NotFound();
})
.MapToApiVersion(1.0);

gamesGroup.MapGet("/{id}", async (IGamesRepository repository, int id) =>
{
    Game? game = await repository.GetAsync(id);
    return game is not null ? Results.Ok(game.ToDtoV2()) : Results.NotFound();
})
.MapToApiVersion(2.0);

Now if you run your service and try this request:

GET http://localhost:5115/games/2?api-version=1.0

You will get the response in the V1 format:

{
    "id": 2,
    "name": "Final Fantasy XIV",
    "price": 59.99
}

And if you try this other request, which uses 2.0 as the API version:

GET http://localhost:5115/games/2?api-version=2.0

You now get the response in the V2 format:

{
    "id": 2,
    "price": "USD 59.99",
    "details": {
        "title": "Final Fantasy XIV",
        "genre": "RolePlaying"
    }
}

Your API is now versioned!

There’s just one more issue to tackle: how to handle the case when the client doesn’t specify the API version in the request?


Step 5: Configuring the default API version

Our clients are not specifying any API version currently, so if we suddenly demand that they specify the api-version query parameter, they will still break.

To handle this, you can configure a default API version that will be used when the client doesn’t specify one.

Here’s how to do it by updating the API Versioning services registration:

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new(1.0);
    options.AssumeDefaultVersionWhenUnspecified = true;
})

With that, if the client comes with an unversioned request like this;

GET http://localhost:5115/games/2

They will keep getting the response in the V1 format:

{
    "id": 2,
    "name": "Final Fantasy XIV",
    "price": 59.99
}

That should give client developers enough time to update their code to consume the new version of the API.

And that’s it for this issue.

I hope it helps!



Whenever you’re ready, there are 4 ways I can help you:

  1. .NET Cloud Developer Bootcamp:​ Everything you need to build production ready .NET applications for the Azure cloud at scale.

  2. ​All-Access Pass: A complete catalog of premium courses, with continuous access to new training and updates.

  3. ​Patreon Community: Join for exclusive discounts on all my in-depth courses and access my Discord server for community support and discussions.

  4. Promote yourself to 20,000+ subscribers by sponsoring this newsletter.