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

  1. Active: Current version, fully supported
  2. Deprecated: Still works, but discouraged
  3. Sunset: Announced end-of-life date
  4. 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! 🚀