As your application evolves, changing requirements inevitably demand breaking changes—such as renaming JSON contract fields, altering response data types, restructuring payloads, or modifying database mappings. If you deploy these modifications directly to a live production API, existing client integrations (mobile apps, legacy single-page apps, external partners) will instantly break.

API Versioning allows your service to evolve independently from its consumers, running multiple contract versions side by side until clients can smoothly migrate.

This article walks through a complete, production-grade implementation of API versioning in an ASP.NET Core Web API using the official Asp.Versioning libraries.

1. Choosing an API Versioning Strategy

Before writing code, you need to decide how clients will transmit the requested version. The four primary strategies supported in modern .NET ecosystems are:

  1. URL Path Versioning (Recommended): Version embedded directly in the route path (/api/v1/products or /api/v2/products). Highly visible, intuitive, and easy to cache.

  2. Query String Versioning: Version passed via a query parameter (/api/products?api-version=2.0).

  3. Header Versioning: Version passed via a custom HTTP header (X-Version: 2.0).

  4. Media-Type (Content Negotiation) Versioning: Version passed inside the Accept header (Accept: application/json;version=2.0).

2. Step 1: Installing Required NuGet Packages

Install the foundational versioning packages into your API project:

Shell

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

3. Step 2: Configuring API Versioning in Program.cs

Register and configure the versioning engine in your dependency injection container. We'll configure it to report supported versions back to clients via custom HTTP response headers and assume version 1.0 if no version is explicitly supplied.

C#

// WebApi/Program.cs
using Asp.Versioning;

var builder = WebApplication.CreateBuilder(args);

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

// Configure API Versioning Services
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true; // Adds 'api-supported-versions' header to responses
    
    // Configure how clients specify the version (URL path segment by default)
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
})
.AddMvc() // Hooks versioning into ASP.NET Core Controllers
.AddApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VVV"; // Formats version as "v1", "v2" for Swagger
    options.SubstituteApiVersionInUrl = true;
});

var app = builder.Build();

// ... configure middleware pipeline
app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();

app.Run();

4. Step 3: Implementing Versioned Controllers

To maintain clean architecture and isolate breaking changes, organize your controllers by version or apply the version attributes cleanly.

Version 1 Implementation

C#

// WebApi/Controllers/V1/ProductsController.cs
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;

namespace WebApi.Controllers.V1;

[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/products")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id:int}")]
    public IActionResult GetById(int id)
    {
        // V1 Schema: Flat pricing model
        var product = new { Id = id, Name = "Enterprise Widget", Price = 99.99m };
        return Ok(product);
    }
}

Version 2 Implementation (Introducing Breaking Changes)

Suppose business requirements change in Version 2: prices must be decoupled into localized currencies with extra metadata. Instead of modifying V1 and crashing existing clients, we introduce V2 alongside it:

C#

// WebApi/Controllers/V2/ProductsController.cs
using Asp.Versioning;
using Microsoft.AspNetCore.Mvc;

namespace WebApi.Controllers.V2;

[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/products")]
public class ProductsController : ControllerBase
{
    [HttpGet("{id:int}")]
    public IActionResult GetById(int id)
    {
        // V2 Schema: Restructured contract with currency and rich metadata
        var product = new { 
            Id = id, 
            Title = "Enterprise Widget", 
            Pricing = new { Amount = 99.99m, Currency = "USD" },
            IsActive = true 
        };
        return Ok(product);
    }
}

5. Step 4: Handling Deprecation and Sunsetting

You should not keep obsolete API versions active indefinitely. When retiring an old version, mark it as deprecated first. This injects an automatic Deprecation header to alert consumers.

C#

[ApiController]
[ApiVersion("1.0", Deprecated = true)] // Marks V1 as deprecated
[Route("api/v{version:apiVersion}/products")]
public class ProductsControllerV1 : ControllerBase
{
    // Implementation for deprecated V1...
}

Conclusion

By implementing structured API versioning with Asp.Versioning, your ASP.NET Core Web API safely isolates breaking schema changes, protects downstream consumers from sudden crashes, and ensures seamless, incremental migration pathways.