👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
URI-Based API Versioning in ASP.NET Web API

URI-Based API Versioning in ASP.NET Web API

Author - Abdul Rahman (Bhai)

Web API

29 Articles

Improve

Table of Contents

  1. What we gonna do?
  2. Why we gonna do?
  3. How we gonna do?
  4. Summary

What we gonna do?

The version number in your API URL is the cheapest contract you can publish. Without it, the moment you rename a field, change a status code, or restructure a response body, every integration you have — mobile apps, third-party clients, internal services — breaks silently at runtime with no warning and no fallback. This article shows how to implement URI-based API versioning in an ASP.NET Core Web API using the Asp.Versioning.Mvc package so that multiple API versions coexist peacefully in a single application.

Why we gonna do?

There are several strategies for communicating the requested version to an API:


┌────────────────┬──────────────────────────────────────────────────┐
│ Strategy       │ Example                                          │
├────────────────┼──────────────────────────────────────────────────┤
│ URI segment    │ /api/v1/articles, /api/v2/articles               │
│ Query string   │ /api/articles?api-version=2.0                    │
│ Custom header  │ api-version: 2.0                                 │
│ Accept header  │ Accept: application/vnd.api+json;version=2.0     │
│ Media type     │ Content-Type: application/vnd.api.v2+json        │
└────────────────┴──────────────────────────────────────────────────┘
            

URI-based versioning is the most explicit strategy: the version is visible in every request log, browser bookmark, API reference, and error report — no extra tooling required to know which version a client is calling.

The real cost of skipping versioning only becomes obvious the first time you need to make a breaking change. Renaming a single JSON field is enough to bring down every integration you have:


// ❌ V1 consumers expect:  { "id": 1, "title": "..." }
// You rename the field to "heading". Every V1 client now crashes silently.
record Article(int Id, string Heading, string Author, string Channel);
            

Versioning solves this by letting you introduce v2 with the breaking change while keeping v1 alive for existing consumers. You can also mark v1 as deprecated — the framework will automatically advertise the retirement in every response header, giving clients time to migrate on their own schedule.

How we gonna do?

Step 1: Install the Versioning Package

Add the MVC-flavoured versioning package, which provides support for ApiController-based APIs:


dotnet add package Asp.Versioning.Mvc
            

Step 2: Register Versioning Services

Chain three calls in Program.cs. AddApiVersioning() sets the global defaults; .AddMvc() activates controller support; .AddApiExplorer() exposes version-aware metadata to tools like Scalar and OpenAPI generators:


using Asp.Versioning;

// Standard controller infrastructure (required)
builder.Services.AddControllers();

builder.Services
    .AddApiVersioning(options =>
    {
        options.AssumeDefaultVersionWhenUnspecified = true;
        options.DefaultApiVersion                   = new ApiVersion(1);
        options.ReportApiVersions                   = true;
    })
    .AddMvc()
    .AddApiExplorer(options =>
    {
        // Format: 'v'1 → "v1", 'v'2 → "v2"
        options.GroupNameFormat = "'v'V";
    });

// ... other middleware ...
app.MapControllers();
            

After this, three behaviours are active:

  • AssumeDefaultVersionWhenUnspecified — requests with no version in the URL are treated as v1 instead of returning a 400.
  • DefaultApiVersion = new ApiVersion(1) — the implicit version assigned to controllers that carry no [ApiVersion] attribute.
  • ReportApiVersions — every response carries an api-supported-versions header so clients can discover what is available.

Step 3: Define Models and an In-Memory Store

Before writing the controllers, define the response models and a lightweight in-memory store so the examples are self-contained:


record Article(int Id, string Title, string Author, string Channel);
record ArticleDetailDto(int Id, string Title, string Author, string Channel, bool IsFeatured);

static class ArticleStore
{
    public static readonly List<Article> All =
    [
        new(1, "Logging in ASP.NET Web API",       "Abdul Rahman", "WebAPI"),
        new(2, "JWT Auth in ASP.NET Web API",       "Abdul Rahman", "WebAPI"),
        new(3, "Authorization in ASP.NET Web API",  "Abdul Rahman", "WebAPI"),
        new(4, "API Versioning in ASP.NET Web API", "Abdul Rahman", "WebAPI"),
    ];
}
            

Article is the v1 shape. ArticleDetailDto adds a IsFeatured flag — a non-breaking addition that motivates introducing v2 while keeping v1 intact.

Step 4: Version Controllers with [ApiVersion] and URI Route Templates

Add [ApiVersion] at the controller class level to declare which version the controller serves. Embed v{version:apiVersion} in the route template so the version number is part of the URL — that is what makes this strategy URI-based:


[Produces(MediaTypeNames.Application.Json)]
[Consumes(MediaTypeNames.Application.Json)]
[ApiController]
[ApiVersion(1)]
[Route("api/v{version:apiVersion}/articles")]
[Tags("ArticleEndpoints")]
public class ArticlesV1Controller : ControllerBase
{
    /// <summary>Retrieves all articles.</summary>
    /// <returns>A list of articles.</returns>
    [HttpGet(Name = "GetArticlesV1")]
    [ProducesResponseType(typeof(IEnumerable<Article>), StatusCodes.Status200OK)]
    public ActionResult<IEnumerable<Article>> GetAll()
        => Ok(ArticleStore.All);

    /// <summary>Retrieves a single article by its identifier.</summary>
    /// <param name="id">The article identifier.</param>
    /// <returns>The matching article, or 404 if not found.</returns>
    [HttpGet("{id:int}", Name = "GetArticleByIdV1")]
    [ProducesResponseType(typeof(Article), StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public ActionResult<Article> GetById(int id)
    {
        var article = ArticleStore.All.FirstOrDefault(a => a.Id == id);
        return article is null ? NotFound() : Ok(article);
    }
}

[Produces(MediaTypeNames.Application.Json)]
[Consumes(MediaTypeNames.Application.Json)]
[ApiController]
[ApiVersion(2)]
[Route("api/v{version:apiVersion}/articles")]
[Tags("ArticleEndpoints")]
public class ArticlesV2Controller : ControllerBase
{
    /// <summary>Retrieves all articles with a featured flag (v2).</summary>
    /// <returns>A list of enriched articles.</returns>
    [HttpGet(Name = "GetArticlesV2")]
    [ProducesResponseType(typeof(IEnumerable<ArticleDetailDto>), StatusCodes.Status200OK)]
    public ActionResult<IEnumerable<ArticleDetailDto>> GetAll()
    {
        var enriched = ArticleStore.All
            .Select(a => new ArticleDetailDto(
                a.Id, a.Title, a.Author, a.Channel,
                IsFeatured: a.Id == 1));
        return Ok(enriched);
    }
}
            

Both controllers share the same route prefix — what separates them is the [ApiVersion] attribute. A request to GET /api/v1/articles is routed to ArticlesV1Controller; a request to GET /api/v2/articles is routed to ArticlesV2Controller. Both live in the same application with zero routing conflicts.

v2-api-versioning-scalar
Scalar UI showing v1 and v2 article endpoints side by side — each version gets its own document tab

Step 5: Deprecating an API Version

When a version is ready for retirement, pass Deprecated = true to its [ApiVersion] attribute. The endpoint continues to function, but every response now includes an api-deprecated-versions header to notify clients:


[ApiController]
[ApiVersion(1, Deprecated = true)]
[Route("api/v{version:apiVersion}/articles")]
[Tags("ArticleEndpoints")]
public class ArticlesV1Controller : ControllerBase
{
    // ... same actions as before
}
            

A request to the deprecated version now returns both headers:


api-supported-versions: 2.0
api-deprecated-versions: 1.0
            

Step 6: Calling Versioned Endpoints

Test the endpoints directly from a .http file:

@baseUrl = https://localhost:7001

### V1 — basic article list
GET {{baseUrl}}/api/v1/articles

### V1 — single article by id (non-integer id returns 404 — route won't match)
GET {{baseUrl}}/api/v1/articles/1

### V2 — enriched article list with featured flag
GET {{baseUrl}}/api/v2/articles

### Deprecated V1 — still responds, but includes api-deprecated-versions header
GET {{baseUrl}}/api/v1/articles
            

The v1 and v2 responses for GET /api/v{n}/articles:


// V1
[
  { "id": 1, "title": "Logging in ASP.NET Web API", "author": "Abdul Rahman", "channel": "WebAPI" },
  ...
]

// V2
[
  { "id": 1, "title": "Logging in ASP.NET Web API", "author": "Abdul Rahman", "channel": "WebAPI", "isFeatured": true },
  ...
]
            

Summary

  • Version your API early — adding versioning after a breaking change has already landed is too late for the consumers you already broke.
  • URI-based versioning (/api/v1/, /api/v2/) is the most explicit and discoverable strategy — the version is visible in logs, bookmarks, and every API reference without any extra tooling.
  • Asp.Versioning.Mvc wires up versioning in three lines: AddApiVersioning(), .AddMvc(), and .AddApiExplorer().
  • Decorate each controller class with [ApiVersion(n)] and embed v{version:apiVersion} in the route template — multiple version controllers can share the same route prefix with no conflicts.
  • Mark retiring versions with [ApiVersion(n, Deprecated = true)] — they keep working while the api-deprecated-versions header signals to clients that migration should begin.
  • Enable ReportApiVersions so every response carries an api-supported-versions header — clients can use this to detect when a newer version is available.

To document the versioned endpoints automatically in a live interactive UI, read Documenting Web API with OpenAPI and Scalar in ASP.NET. For securing those endpoints with JWT Bearer tokens, see Securing ASP.NET Web API with JWT Bearer Token Authentication.

👉🏼 Click here to Join I ❤️ .NET WhatsApp Channel to get 🔔 notified about new articles and other updates.
  • Web API
  • API Versioning
  • URI Versioning
  • Asp.Versioning.Mvc
  • [ApiVersion]
  • Breaking Change