Back to blog
Backend Systemsintermediate

Version Your API So You Never Break Clients Again

Add proper API versioning to ASP.NET Core using URL segments, headers, and query strings. Cover deprecation, version sets for Minimal API, and versioned OpenAPI docs.

LearnixoApril 15, 20263 min read
.NETC#ASP.NET CoreAPI VersioningOpenAPI
Share:𝕏

Why Versioning Matters

Shipping a breaking change to /api/products will take down every client you have. API versioning lets you evolve your contract while existing consumers keep working on the version they were built against.

Install the package first:

Bash
dotnet add package Asp.Versioning.Mvc
dotnet add package Asp.Versioning.Mvc.ApiExplorer

Configure Versioning in Program.cs

C#
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true; // adds api-supported-versions header
    options.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new HeaderApiVersionReader("X-Api-Version"),
        new QueryStringApiVersionReader("api-version")
    );
})
.AddApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VVV";
    options.SubstituteApiVersionInUrl = true;
});

ReportApiVersions = true adds api-supported-versions: 1.0, 2.0 and api-deprecated-versions: 1.0 response headers — clients can programmatically detect when to upgrade.

URL Segment Versioning

This is the most visible and the easiest to test in a browser.

C#
[ApiController]
[Route("api/v{version:apiVersion}/products")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [MapToApiVersion("1.0")]
    public IActionResult GetV1()
    {
        return Ok(new { version = "1.0", products = new[] { "Widget A" } });
    }

    [HttpGet]
    [MapToApiVersion("2.0")]
    public IActionResult GetV2()
    {
        // V2 returns richer payload
        return Ok(new
        {
            version = "2.0",
            products = new[]
            {
                new { id = 1, name = "Widget A", sku = "WGT-001", inStock = true }
            }
        });
    }
}

Routes: GET /api/v1/products and GET /api/v2/products.

Header Versioning

No URL pollution — useful for internal APIs where you control all clients.

GET /api/products
X-Api-Version: 2.0

The reader is already registered above via HeaderApiVersionReader("X-Api-Version"). No controller changes needed — the version reader does the dispatch.

Query String Versioning

GET /api/products?api-version=2.0

Registered via QueryStringApiVersionReader("api-version"). Good for quick testing and webhooks where headers are awkward.

Deprecating an Old Version

Mark the version deprecated on the controller. Clients still get responses — nothing breaks — but the api-deprecated-versions header tells them to migrate.

C#
[ApiController]
[Route("api/v{version:apiVersion}/orders")]
[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("2.0")]
public class OrdersController : ControllerBase
{
    [HttpGet]
    [MapToApiVersion("1.0")]
    public IActionResult GetV1() => Ok(new { warning = "v1 is deprecated, migrate to v2" });

    [HttpGet]
    [MapToApiVersion("2.0")]
    public IActionResult GetV2() => Ok(new { orders = Array.Empty<object>() });
}

Response headers on a v1 call:

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

Version Sets for Minimal API

Controller-based versioning does not apply to Minimal API endpoints. Use ApiVersionSet instead.

C#
var versionSet = app.NewApiVersionSet()
    .HasApiVersion(new ApiVersion(1, 0))
    .HasApiVersion(new ApiVersion(2, 0))
    .ReportApiVersions()
    .Build();

app.MapGet("/api/v{version:apiVersion}/inventory", (ApiVersion version) =>
{
    return version.MajorVersion == 1
        ? Results.Ok(new { items = 42 })
        : Results.Ok(new { items = 42, lastUpdated = DateTime.UtcNow });
})
.WithApiVersionSet(versionSet)
.MapToApiVersion(1, 0)
.MapToApiVersion(2, 0);

Versioned OpenAPI Docs

Wire up Swagger to generate a separate spec per version:

C#
// Program.cs
var descriptions = app.DescribeApiVersions();

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

Register AddSwaggerGen to pick up the version groups:

C#
builder.Services.AddSwaggerGen(options =>
{
    var provider = builder.Services.BuildServiceProvider()
        .GetRequiredService<IApiVersionDescriptionProvider>();

    foreach (var description in provider.ApiVersionDescriptions)
    {
        options.SwaggerDoc(description.GroupName, new OpenApiInfo
        {
            Title = "My API",
            Version = description.ApiVersion.ToString(),
            Description = description.IsDeprecated
                ? "This version is deprecated. Migrate to the latest."
                : null
        });
    }
});

This gives you /swagger/v1/swagger.json and /swagger/v2/swagger.json — separate, accurate specs per version.

Key Rules

  • Start versioning on day one — retrofitting it is painful.
  • Never remove a version without a sunset period and api-deprecated-versions header in place.
  • Keep V1 and V2 controllers in separate folders (Controllers/V1/, Controllers/V2/) to avoid a single massive file.
  • Only version at the API boundary. Your domain and application layers should not know about API versions.

Enjoyed this article?

Explore the Backend Systems learning path for more.

Found this helpful?

Share:𝕏

Leave a comment

Have a question, correction, or just found this helpful? Leave a note below.