By the end of this session, you will be able to:
Asp.Versioning| Time | Segment | Type | Duration |
|---|---|---|---|
| 0:00 | The problem — why versioning exists | Discussion | 15 min |
| 0:15 | The three versioning strategies | Theory | 25 min |
| 0:40 | Live demo — Asp.Versioning setup | Demo | 25 min |
| 1:05 | Break | — | 10 min |
| 1:15 | Lab — evolve a UserController v1 to v2 | Lab | 30 min |
| 1:45 | Deprecation strategies & real-world conventions | Theory | 15 min |
Imagine a mobile app consuming GET /api/users/{id} that returns:
Current response (v1)
{ "name": "Ahmed" }
A new client requests you split name into firstName and lastName.
name breaks — and you cannot force all users to update.
With versioning (v2)
{
"firstName": "Ahmed",
"lastName": "Hassan"
}
The backend can be deployed in minutes. Clients operate on entirely different timelines.
GET /api/v1/users
GET /api/users?api-version=1.0
Api-Version: 1.0
Accept: application/vnd.github.v3+json), Stripe.
Advantages
Trade-offs
GET /api/v1/users/42 HTTP/1.1
GET /api/v2/users/42 HTTP/1.1
Asp.Versioning.Mvc NuGet package. Installation, configuration, and two controllers co-existing side by side.Two NuGet packages are required:
Asp.Versioning.Mvc — core versioning attributes and route constraintsAsp.Versioning.Mvc.ApiExplorer — integrates versioning with Swagger / OpenAPI discovery (needed for Session 08)Microsoft.AspNetCore.Mvc.Versioning package. If you see tutorials referencing the old name, they are outdated.
dotnet add package Asp.Versioning.Mvc
dotnet add package Asp.Versioning.Mvc.ApiExplorer
builder.Services.AddControllers();
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
}).AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
DefaultApiVersion — falls back to v1.0 when no version is specifiedAssumeDefaultVersionWhenUnspecified — prevents a 400 error for clients that omit the versionReportApiVersions — adds api-supported-versions header to every response[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/users")]
public class UsersV1Controller : ControllerBase
{
[HttpGet("{id}")]
public IActionResult Get(int id) =>
Ok(new { id, name = "Ahmed Hassan" });
}
[ApiVersion("1.0")] declares which version this controller handlesv{version:apiVersion} is a route constraint that Asp.Versioning resolves automaticallyname field — this is the contract v1 clients depend on[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/users")]
public class UsersV2Controller : ControllerBase
{
[HttpGet("{id}")]
public IActionResult Get(int id) =>
Ok(new
{
id,
firstName = "Ahmed",
lastName = "Hassan",
createdAt = DateTime.UtcNow
});
}
v1 and v2 coexist in the same project. Asp.Versioning routes each request to the correct controller based on the URL segment.
Send these two requests and compare the responses:
GET /api/v1/users/1
GET /api/v2/users/1
Also inspect the response headers — with ReportApiVersions: true, every response includes:
api-supported-versions: 1.0, 2.0
v1 response
{ "id": 1, "name": "Ahmed Hassan" }
v2 response
{
"id": 1,
"firstName": "Ahmed",
"lastName": "Hassan",
"createdAt": "2026-01-15T10:00:00Z"
}
Starting from a v1 UserController that returns { "id": 1, "name": "Ahmed Hassan" }, add versioning so v1 and v2 coexist:
Asp.Versioning.Mvc and Asp.Versioning.Mvc.ApiExplorer via dotnet add package
AddApiVersioning config to Program.cs with default version 1.0
UsersV1Controller and UsersV2Controller with correct [ApiVersion] attributes
id, firstName, lastName, email, createdAt
GET /api/v1/users/1 still returns the old shape; GET /api/v2/users/1 returns the new shape
Deprecated = true to [ApiVersion("1.0")] and verify the api-deprecated-versions header appears
Set Deprecated = true on the [ApiVersion] attribute. Asp.Versioning automatically adds the deprecation header to all responses from that controller.
Sunset header (RFC 8594) to communicate the removal date[ApiController]
[ApiVersion("1.0", Deprecated = true)]
[Route("api/v{version:apiVersion}/users")]
public class UsersV1Controller : ControllerBase
{
// ...
}
api-supported-versions: 2.0
api-deprecated-versions: 1.0
Sunset: Sat, 01 Jan 2027 00:00:00 GMT
Deprecation: true
There is no universal rule — the right duration depends on who your clients are and what their update cycle looks like.
| Audience | Recommended minimum | Rationale |
|---|---|---|
| Startups / internal APIs | 3–6 months | Small teams, fast iteration, direct communication |
| Public APIs with mobile clients | 12–18 months | App store cycles and user update lag |
| Enterprise / B2B | 2+ years | Annual release cycles and formal validation processes |
2024-01-01) and pins each API key to the version active when the key was created. They maintain every past version indefinitely — eliminating accidental breakage at the cost of significant maintenance complexity.
| Convention | Example | Used by |
|---|---|---|
| Integer | v1, v2, v3 | GitHub, Twitter, most public REST APIs |
| Semantic (major.minor) | v1.0, v2.1 | Microsoft Azure REST APIs |
| Date-based | 2024-01-01 | Stripe |
v1, v2, and so on. This is simple, explicit, and what most employers will expect when they ask about API versioning experience.
[ApiVersion] attributes and route constraints handle all the routing automaticallyDeprecated = true, announce a sunset date, and give clients time to migrateSession 07 — Pagination, Filtering & Sorting
GET /api/v1/tasks does not return ten thousand records at onceapi-deprecated-versions header confirmed in Postman.
Add URL-based versioning to your Task API and introduce a v2 with a breaking response change.
What to build:
Asp.Versioning.Mvc and configure it in Program.cs with default version 1.0TasksV1Controller — returns tasks with id, title, isCompletedTasksV2Controller — returns tasks with id, title, status (string: "pending" / "in-progress" / "completed"), dueDate, createdAtDeprecated = trueAcceptance criteria:
GET /api/v1/tasks returns the old shape and includes api-deprecated-versions: 1.0 in the response headersGET /api/v2/tasks returns the new shape with the status fieldapi-supported-versions: 1.0, 2.0 on every responseSunset header (set to one year from today) whenever the requested version is flagged as deprecated.