$npx -y skills add wshaddix/dotnet-skills --skill dotnet-api-versioningVersioning HTTP APIs. Asp.Versioning.Http/Mvc, URL segment, header, query string, sunset.
| 1 | # dotnet-api-versioning |
| 2 | |
| 3 | API versioning strategies for ASP.NET Core using the `Asp.Versioning` library family. URL segment versioning (`/api/v1/`) is the preferred approach for simplicity and discoverability. This skill covers URL, header, and query string versioning with configuration for both Minimal APIs and MVC controllers, sunset policy enforcement, and migration from legacy packages. |
| 4 | |
| 5 | **Out of scope:** Minimal API endpoint patterns (route groups, filters, TypedResults) -- see [skill:dotnet-minimal-apis]. OpenAPI document generation per API version -- see [skill:dotnet-openapi]. Authentication and authorization per version -- see [skill:dotnet-api-security]. |
| 6 | |
| 7 | Cross-references: [skill:dotnet-minimal-apis] for Minimal API endpoint patterns, [skill:dotnet-openapi] for versioned OpenAPI documents. |
| 8 | |
| 9 | --- |
| 10 | |
| 11 | ## Package Landscape |
| 12 | |
| 13 | | Package | Target | Status | |
| 14 | |---------|--------|--------| |
| 15 | | `Asp.Versioning.Http` | Minimal APIs | **Current** | |
| 16 | | `Asp.Versioning.Mvc.ApiExplorer` | MVC controllers + API Explorer | **Current** | |
| 17 | | `Asp.Versioning.Mvc` | MVC controllers (no API Explorer) | **Current** | |
| 18 | | `Microsoft.AspNetCore.Mvc.Versioning` | MVC controllers | **Legacy** -- migrate to `Asp.Versioning.Mvc` | |
| 19 | | `Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer` | MVC + API Explorer | **Legacy** -- migrate to `Asp.Versioning.Mvc.ApiExplorer` | |
| 20 | |
| 21 | Install for Minimal APIs: |
| 22 | |
| 23 | ```xml |
| 24 | <PackageReference Include="Asp.Versioning.Http" Version="8.*" /> |
| 25 | ``` |
| 26 | |
| 27 | Install for MVC controllers: |
| 28 | |
| 29 | ```xml |
| 30 | <PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.*" /> |
| 31 | ``` |
| 32 | |
| 33 | --- |
| 34 | |
| 35 | ## URL Segment Versioning (Preferred) |
| 36 | |
| 37 | URL segment versioning embeds the version in the path (`/api/v1/products`). It is the simplest strategy, works with all HTTP clients, is cacheable, and clearly visible in logs and documentation. |
| 38 | |
| 39 | ### Minimal APIs |
| 40 | |
| 41 | ```csharp |
| 42 | builder.Services.AddApiVersioning(options => |
| 43 | { |
| 44 | options.DefaultApiVersion = new ApiVersion(1, 0); |
| 45 | options.AssumeDefaultVersionWhenUnspecified = true; |
| 46 | options.ReportApiVersions = true; // Adds api-supported-versions header |
| 47 | options.ApiVersionReader = new UrlSegmentApiVersionReader(); |
| 48 | }); |
| 49 | |
| 50 | var app = builder.Build(); |
| 51 | |
| 52 | var versionSet = app.NewApiVersionSet() |
| 53 | .HasApiVersion(new ApiVersion(1, 0)) |
| 54 | .HasApiVersion(new ApiVersion(2, 0)) |
| 55 | .ReportApiVersions() |
| 56 | .Build(); |
| 57 | |
| 58 | var v1 = app.MapGroup("/api/v{version:apiVersion}/products") |
| 59 | .WithApiVersionSet(versionSet) |
| 60 | .MapToApiVersion(new ApiVersion(1, 0)); |
| 61 | |
| 62 | var v2 = app.MapGroup("/api/v{version:apiVersion}/products") |
| 63 | .WithApiVersionSet(versionSet) |
| 64 | .MapToApiVersion(new ApiVersion(2, 0)); |
| 65 | |
| 66 | // V1: returns basic product info |
| 67 | v1.MapGet("/", async (AppDbContext db) => |
| 68 | TypedResults.Ok(await db.Products |
| 69 | .Select(p => new ProductV1Dto(p.Id, p.Name, p.Price)) |
| 70 | .ToListAsync())); |
| 71 | |
| 72 | // V2: returns extended product info with category |
| 73 | v2.MapGet("/", async (AppDbContext db) => |
| 74 | TypedResults.Ok(await db.Products |
| 75 | .Select(p => new ProductV2Dto(p.Id, p.Name, p.Price, p.Category, p.CreatedAt)) |
| 76 | .ToListAsync())); |
| 77 | ``` |
| 78 | |
| 79 | ### MVC Controllers |
| 80 | |
| 81 | ```csharp |
| 82 | builder.Services.AddApiVersioning(options => |
| 83 | { |
| 84 | options.DefaultApiVersion = new ApiVersion(1, 0); |
| 85 | options.AssumeDefaultVersionWhenUnspecified = true; |
| 86 | options.ReportApiVersions = true; |
| 87 | options.ApiVersionReader = new UrlSegmentApiVersionReader(); |
| 88 | }) |
| 89 | .AddMvc() |
| 90 | .AddApiExplorer(options => |
| 91 | { |
| 92 | options.GroupNameFormat = "'v'VVV"; // e.g., v1, v2 |
| 93 | options.SubstituteApiVersionInUrl = true; |
| 94 | }); |
| 95 | |
| 96 | // V1 controller |
| 97 | [ApiController] |
| 98 | [Route("api/v{version:apiVersion}/products")] |
| 99 | [ApiVersion("1.0")] |
| 100 | public sealed class ProductsController(AppDbContext db) : ControllerBase |
| 101 | { |
| 102 | [HttpGet] |
| 103 | public async Task<IActionResult> GetAll() => |
| 104 | Ok(await db.Products |
| 105 | .Select(p => new ProductV1Dto(p.Id, p.Name, p.Price)) |
| 106 | .ToListAsync()); |
| 107 | } |
| 108 | |
| 109 | // V2 controller -- use explicit route, not [controller] token |
| 110 | [ApiController] |
| 111 | [Route("api/v{version:apiVersion}/products")] |
| 112 | [ApiVersion("2.0")] |
| 113 | public sealed class ProductsV2Controller(AppDbContext db) : ControllerBase |
| 114 | { |
| 115 | [HttpGet] |
| 116 | public async Task<IActionResult> GetAll() => |
| 117 | Ok(await db.Products |
| 118 | .Select(p => new ProductV2Dto(p.Id, p.Name, p.Price, p.Category, p.CreatedAt)) |
| 119 | .ToListAsync()); |
| 120 | } |
| 121 | ``` |
| 122 | |
| 123 | --- |
| 124 | |
| 125 | ## Header Versioning |
| 126 | |
| 127 | Header versioning reads the API version from a custom request header. Keeps URLs clean but is less discoverable and harder to test from a browser. |
| 128 | |
| 129 | ```csharp |
| 130 | builder.Services.AddApiVersioning(options => |
| 131 | { |
| 132 | options.DefaultApiVersion = new ApiVersion(1, 0); |
| 133 | options.AssumeDefaultVersionWhenUnspecified = true; |
| 134 | options.ReportApiVersions = true; |
| 135 | options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version"); |
| 136 | }); |
| 137 | ``` |
| 138 | |
| 139 | Client request: |
| 140 | |
| 141 | ```http |
| 142 | GET /api/products HTTP/1.1 |
| 143 | Host: api.example.com |
| 144 | X |