APIs evolve over time - you need to rename fields, change data structures, or remove deprecated endpoints. Without versioning, any change can break existing clients. API versioning lets you introduce changes safely while giving clients time to migrate.
dotnet add package Asp.Versioning.Mvc
// Program.csvar builder = WebApplication.CreateBuilder(args);builder.Services.AddApiVersioning(options =>{options.DefaultApiVersion = new ApiVersion(1, 0);options.AssumeDefaultVersionWhenUnspecified = true;options.ReportApiVersions = true; // Adds api-supported-versions headeroptions.ApiVersionReader = ApiVersionReader.Combine(new UrlSegmentApiVersionReader(),new HeaderApiVersionReader("X-Api-Version"));}).AddApiExplorer(options =>{options.GroupNameFormat = "'v'VVV";options.SubstituteApiVersionInUrl = true;});
✅ Figure: Good example - Configure API versioning with URL and header support
The most visible and commonly used approach:
[ApiController][Route("api/v{version:apiVersion}/[controller]")][ApiVersion("1.0")]public class ProductsController : ControllerBase{[HttpGet]public IActionResult GetAll() => Ok(new[] { new { Id = 1, Name = "Product" } });}[ApiController][Route("api/v{version:apiVersion}/[controller]")][ApiVersion("2.0")]public class ProductsV2Controller : ControllerBase{[HttpGet]public IActionResult GetAll() => Ok(new[] { new { Id = 1, Title = "Product", Sku = "ABC123" } });}
GET /api/v1/products → Returns { Id, Name }GET /api/v2/products → Returns { Id, Title, Sku }
✅ Figure: Good example - URL versioning is clear and cacheable
Major version (v1 to v2) - Breaking changes:
Don't require new version - Non-breaking changes:
// v1 - Original responsepublic class ProductV1{public int Id { get; set; }public string Name { get; set; }}// v2 - Breaking change: Name renamed to Title, new required fieldpublic class ProductV2{public int Id { get; set; }public string Title { get; set; } // Renamed from Namepublic string Sku { get; set; } // New required field}
✅ Figure: Good example - Breaking changes require a new version
Signal to clients when versions will be retired:
[ApiController][Route("api/v{version:apiVersion}/products")][ApiVersion("1.0", Deprecated = true)] // Mark as deprecated[ApiVersion("2.0")]public class ProductsController : ControllerBase{[HttpGet][MapToApiVersion("1.0")]public IActionResult GetV1(){Response.Headers.Append("Sunset", "Sat, 01 Jan 2025 00:00:00 GMT");Response.Headers.Append("Deprecation", "true");return Ok(GetProductsV1());}[HttpGet][MapToApiVersion("2.0")]public IActionResult GetV2() => Ok(GetProductsV2());}
✅ Figure: Good example - Add Sunset header to communicate retirement date
| Strategy | URL | Pros | Cons |
| URL Path | /api/v1/products | Clear, cacheable, easy to test | URLs change between versions |
| Query String | /api/products?api-version=1 | URLs stay same | Easy to forget, less visible |
| Header | X-Api-Version: 1 | Clean URLs | Not visible, harder to test |
| Media Type | Accept: application/vnd.api.v1+json | RESTful purist approach | Complex, rarely used |
Recommendation: Use URL path versioning as primary, with header as fallback for edge cases.
Generate separate Swagger docs for each version:
builder.Services.AddSwaggerGen(options =>{options.SwaggerDoc("v1", new OpenApiInfo { Title = "My API", Version = "v1" });options.SwaggerDoc("v2", new OpenApiInfo { Title = "My API", Version = "v2" });});app.UseSwagger();app.UseSwaggerUI(options =>{options.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");options.SwaggerEndpoint("/swagger/v2/swagger.json", "My API V2");});
✅ Figure: Good example - Each API version gets its own Swagger documentation
// Track version usage for analyticsapp.Use(async (context, next) =>{var version = context.GetRequestedApiVersion();_telemetry.TrackEvent("ApiVersionUsed", new { Version = version?.ToString() });await next();});
✅ Figure: Good example - Monitor which versions are still in use before retiring
| Do | Avoid |
| Version from day one | Adding versioning later is painful |
| Use URL path versioning | Query string versioning (easy to forget) |
| Add Sunset headers for deprecated versions | Removing versions without notice |
| Support N and N-1 versions minimum | Supporting too many old versions |
| Document breaking changes clearly | Assuming clients will figure it out |