Section 7 of 7
API Versioning
đ¯ What You'll Learn
- Why version APIs
- Versioning strategies (URL, header, query string)
- Asp.Versioning.Http package
- Version attributes
- Deprecating versions
- Best practices
Why Version APIs?
API versioning allows you to evolve your API while maintaining backward compatibility for existing clients.
When to Version
- Breaking changes: Removing/renaming properties
- Behavior changes: Different logic or validation
- New features: Major functionality additions
- Contract changes: Different request/response structure
âšī¸ Non-Breaking Changes
Adding optional properties, new endpoints, or fixing bugs typically don't require a new version.
Versioning Strategies
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URL Path | /api/v1/products |
Visible, simple | Clutters URLs |
| Query String | /api/products?v=1 |
Clean URLs | Easy to forget |
| Header | api-version: 1.0 |
Clean URLs | Not visible |
| Media Type | application/vnd.api.v1+json |
RESTful | Complex |
Setup API Versioning
Installation
Package Installation
Bash
dotnet add package Asp.Versioning.Http
dotnet add package Asp.Versioning.Mvc.ApiExplorer
Configuration
Program.cs
C#
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// Add API versioning
builder.Services.AddApiVersioning(options =>
{
// Default version if not specified
options.DefaultApiVersion = new ApiVersion(1, 0);
// Use default version when client doesn't specify
options.AssumeDefaultVersionWhenUnspecified = true;
// Report supported versions in response headers
options.ReportApiVersions = true;
// Read version from URL path
options.ApiVersionReader = new UrlSegmentApiVersionReader();
}).AddApiExplorer(options =>
{
// Format version as 'v'major[.minor]
options.GroupNameFormat = "'v'VVV";
// Substitute version in route template
options.SubstituteApiVersionInUrl = true;
});
var app = builder.Build();
app.MapControllers();
app.Run();
URL Path Versioning
Controllers/V1/ProductsController.cs
C#
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
public class ProductsController : ControllerBase
{
// GET /api/v1/products
[HttpGet]
public ActionResult GetProducts()
{
return Ok(new[] { "Product 1", "Product 2" });
}
}
Controllers/V2/ProductsController.cs
C#
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
// GET /api/v2/products
[HttpGet]
public ActionResult GetProducts()
{
return Ok(new
{
products = new[] { "Product 1", "Product 2" },
totalCount = 2,
version = "2.0"
});
}
}
Query String Versioning
Program.cs
C#
builder.Services.AddApiVersioning(options =>
{
// Read version from query string: ?api-version=1.0
options.ApiVersionReader = new QueryStringApiVersionReader("api-version");
});
Controller
C#
[ApiController]
[Route("api/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
// GET /api/products?api-version=1.0
[HttpGet]
[MapToApiVersion("1.0")]
public ActionResult GetProductsV1()
{
return Ok("Version 1.0");
}
// GET /api/products?api-version=2.0
[HttpGet]
[MapToApiVersion("2.0")]
public ActionResult GetProductsV2()
{
return Ok("Version 2.0");
}
}
Header Versioning
Program.cs
C#
builder.Services.AddApiVersioning(options =>
{
// Read version from header: api-version: 1.0
options.ApiVersionReader = new HeaderApiVersionReader("api-version");
});
Request Example
HTTP
GET /api/products HTTP/1.1
Host: api.inventrackapp.com
api-version: 2.0
Multiple Versioning Methods
Combine URL, Query, and Header
C#
builder.Services.AddApiVersioning(options =>
{
// Accept version from URL, query string, or header
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new QueryStringApiVersionReader("api-version"),
new HeaderApiVersionReader("api-version")
);
});
Deprecating Versions
Mark Version as Deprecated
C#
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
// v1.0 is deprecated but still works
}
Response Headers
Deprecation Headers
HTTP
HTTP/1.1 200 OK
api-supported-versions: 1.0, 2.0
api-deprecated-versions: 1.0
Complete InvenTrack Example
Program.cs
C#
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
}).AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
var app = builder.Build();
app.MapControllers();
app.Run();
Controllers/V1/ProductsController.cs
C#
namespace InvenTrack.Api.Controllers.V1;
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0", Deprecated = true)]
public class ProductsController : ControllerBase
{
// GET /api/v1/products
[HttpGet]
public ActionResult GetProducts()
{
return Ok(new[]
{
new { id = 1, name = "Laptop", price = 999.99 },
new { id = 2, name = "Mouse", price = 29.99 }
});
}
// GET /api/v1/products/1
[HttpGet("{id}")]
public ActionResult GetProduct(int id)
{
return Ok(new { id, name = "Laptop", price = 999.99 });
}
}
Controllers/V2/ProductsController.cs
C#
namespace InvenTrack.Api.Controllers.V2;
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
private readonly IProductService _productService;
public ProductsController(IProductService productService)
{
_productService = productService;
}
// GET /api/v2/products?page=1&pageSize=20
[HttpGet]
public async Task<ActionResult> GetProducts(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20)
{
var products = await _productService.GetPagedAsync(page, pageSize);
return Ok(new
{
data = products.Items,
pagination = new
{
page,
pageSize,
totalCount = products.TotalCount,
totalPages = products.TotalPages
},
version = "2.0"
});
}
// GET /api/v2/products/1
[HttpGet("{id}")]
public async Task<ActionResult> GetProduct(int id)
{
var product = await _productService.GetByIdAsync(id);
if (product == null)
return NotFound();
return Ok(new
{
product,
links = new
{
self = $"/api/v2/products/{id}",
category = $"/api/v2/categories/{product.CategoryId}"
}
});
}
}
Best Practices
- Semantic versioning: Use major.minor (1.0, 2.0)
- URL path preferred: Most visible and discoverable
- Default version: Always set a default
- Report versions: Include in response headers
- Deprecate gracefully: Mark old versions as deprecated
- Document changes: Maintain changelog
- Support 2-3 versions: Don't support too many
- Sunset policy: Communicate deprecation timeline
Version Lifecycle
- Active: Current version, fully supported
- Deprecated: Still works, but discouraged
- Sunset: Announced end-of-life date
- Retired: No longer available
Key Takeaways
- API versioning: Evolve APIs without breaking clients
- Strategies: URL path, query string, header, media type
- Asp.Versioning.Http: Official versioning package
- [ApiVersion]: Mark controller versions
- Deprecation: Mark old versions as deprecated
- URL path: Most common and visible
- Default version: Always configure
- Report versions: In response headers
đ Part VIII Complete!
Congratulations! You've completed Part VIII: Building Web APIs. You now understand:
- â RESTful principles and best practices
- â API controllers with [ApiController] attribute
- â Model binding from various sources
- â Validation with Data Annotations and FluentValidation
- â Minimal APIs for lightweight endpoints
- â DTOs and AutoMapper for separation of concerns
- â API versioning strategies
You now have the skills to build professional, production-ready Web APIs in ASP.NET Core! Keep up the excellent work! đ