Skip to main content
The Intent.AspNetCore.Versioning module applies versioning to your ASP.NET Core services, enabling you to maintain multiple versions of your API simultaneously while evolving your application.

Overview

API versioning is essential for maintaining backward compatibility while introducing new features and changes. This module integrates Microsoft’s ASP.NET Core API Versioning library to provide a robust versioning strategy for your Web APIs.

What Gets Generated

ApiVersioningConfiguration

Configures API versioning for your application:
public static class ApiVersioningConfiguration
{
    public static IServiceCollection AddApiVersioningConfiguration(
        this IServiceCollection services)
    {
        services.AddApiVersioning(options =>
        {
            options.DefaultApiVersion = new ApiVersion(1, 0);
            options.AssumeDefaultVersionWhenUnspecified = true;
            options.ReportApiVersions = true;
            options.ApiVersionReader = ApiVersionReader.Combine(
                new UrlSegmentApiVersionReader(),
                new HeaderApiVersionReader("X-Api-Version"),
                new QueryStringApiVersionReader("api-version")
            );
        })
        .AddApiExplorer(options =>
        {
            options.GroupNameFormat = "'v'VVV";
            options.SubstituteApiVersionInUrl = true;
        });

        return services;
    }
}

ApiVersionSwaggerGenOptions

Integrates versioning with Swagger documentation:
public class ApiVersionSwaggerGenOptions : IConfigureOptions<SwaggerGenOptions>
{
    private readonly IApiVersionDescriptionProvider _provider;

    public ApiVersionSwaggerGenOptions(IApiVersionDescriptionProvider provider)
    {
        _provider = provider;
    }

    public void Configure(SwaggerGenOptions options)
    {
        foreach (var description in _provider.ApiVersionDescriptions)
        {
            options.SwaggerDoc(
                description.GroupName,
                new OpenApiInfo
                {
                    Title = $"My API {description.ApiVersion}",
                    Version = description.ApiVersion.ToString(),
                    Description = description.IsDeprecated 
                        ? "This API version has been deprecated." 
                        : "Current API version"
                });
        }
    }
}

Key Features

Multiple Versioning Strategies

URL path, query string, or header-based versioning

Swagger Integration

Separate documentation for each API version

Deprecation Support

Mark old versions as deprecated

Version Discovery

Clients can discover supported versions

Versioning Strategies

Version in the URL path:
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/products")]
public class ProductsV1Controller : ControllerBase
{
    [HttpGet]
    public ActionResult<List<ProductDtoV1>> GetAll()
    {
        // Version 1 implementation
    }
}

[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/products")]
public class ProductsV2Controller : ControllerBase
{
    [HttpGet]
    public ActionResult<List<ProductDtoV2>> GetAll()
    {
        // Version 2 implementation with new features
    }
}
Client requests:
GET /api/v1/products
GET /api/v2/products

Query String Versioning

Version in query parameter:
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/products")]
public class ProductsController : ControllerBase
{
    [HttpGet]
    [MapToApiVersion("1.0")]
    public ActionResult<List<ProductDtoV1>> GetAllV1()
    {
        // Version 1
    }

    [HttpGet]
    [MapToApiVersion("2.0")]
    public ActionResult<List<ProductDtoV2>> GetAllV2()
    {
        // Version 2
    }
}
Client requests:
GET /api/products?api-version=1.0
GET /api/products?api-version=2.0

Header Versioning

Version in custom header:
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/products")]
public class ProductsController : ControllerBase
{
    // Same implementation as query string versioning
}
Client requests:
GET /api/products
X-Api-Version: 1.0

GET /api/products
X-Api-Version: 2.0

Media Type Versioning

Version in Accept header:
services.AddApiVersioning(options =>
{
    options.ApiVersionReader = new MediaTypeApiVersionReader();
});
Client requests:
GET /api/products
Accept: application/json;v=1.0

GET /api/products
Accept: application/json;v=2.0

Version Declaration

Single Version

[ApiVersion("1.0")]
[ApiController]
[Route("api/v{version:apiVersion}/orders")]
public class OrdersController : ControllerBase
{
    // All endpoints are v1.0
}

Multiple Versions

[ApiVersion("1.0")]
[ApiVersion("2.0")]
[ApiController]
[Route("api/v{version:apiVersion}/orders")]
public class OrdersController : ControllerBase
{
    [HttpGet]
    public ActionResult<List<OrderDto>> GetAll()
    {
        // Shared across versions
    }

    [HttpGet("{id}")]
    [MapToApiVersion("1.0")]
    public ActionResult<OrderDtoV1> GetByIdV1([FromRoute] Guid id)
    {
        // Version 1 specific
    }

    [HttpGet("{id}")]
    [MapToApiVersion("2.0")]
    public ActionResult<OrderDtoV2> GetByIdV2([FromRoute] Guid id)
    {
        // Version 2 with additional data
    }
}

Version Neutral

[ApiVersionNeutral]
[ApiController]
[Route("api/health")]
public class HealthController : ControllerBase
{
    [HttpGet]
    public ActionResult<HealthStatus> Check()
    {
        // Available in all versions
    }
}

Deprecating Versions

Mark versions as deprecated:
[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("2.0")]
[ApiController]
[Route("api/v{version:apiVersion}/products")]
public class ProductsController : ControllerBase
{
    // Version 1.0 is deprecated but still functional
}
Response includes deprecation warning:
HTTP/1.1 200 OK
api-deprecated-versions: 1.0
api-supported-versions: 2.0

Swagger/OpenAPI Integration

Versioning automatically creates separate Swagger documents:
app.UseSwaggerUI(c =>
{
    c.SwaggerEndpoint("/swagger/v1/swagger.json", "My API V1");
    c.SwaggerEndpoint("/swagger/v2/swagger.json", "My API V2");
});
Swagger UI displays a dropdown to switch between versions.

API Explorer

Configure API explorer for versioning:
services.AddApiVersioning(options =>
{
    // versioning options...
})
.AddApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VVV"; // Format: v1, v2, v2.1
    options.SubstituteApiVersionInUrl = true;
    options.AssumeDefaultVersionWhenUnspecified = true;
});

Version Discovery

Clients can discover supported versions:
OPTIONS /api/v1/products
Response:
HTTP/1.1 200 OK
api-supported-versions: 1.0, 2.0
api-deprecated-versions: 1.0

Migration Strategies

Breaking Changes

Introduce breaking changes in a new version:
// V1 - Original
public class ProductDtoV1
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
}

// V2 - Breaking change: Price split into components
public class ProductDtoV2
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public decimal BasePrice { get; set; }
    public decimal TaxAmount { get; set; }
    public decimal TotalPrice { get; set; }
}

Additive Changes

Add new properties without breaking existing clients:
// V1 and V2 use same DTO
public class ProductDto
{
    public Guid Id { get; set; }
    public string Name { get; set; }
    public decimal Price { get; set; }
    
    // New in V2, ignored by V1 clients
    public string? Category { get; set; }
    public List<string>? Tags { get; set; }
}

Behavioral Changes

Same endpoint, different behavior:
[HttpGet("search")]
[MapToApiVersion("1.0")]
public ActionResult<List<ProductDto>> SearchV1([FromQuery] string query)
{
    // Simple search: name contains query
    return _products.Where(p => p.Name.Contains(query)).ToList();
}

[HttpGet("search")]
[MapToApiVersion("2.0")]
public ActionResult<List<ProductDto>> SearchV2([FromQuery] string query)
{
    // Advanced search: full-text search with ranking
    return _searchService.FullTextSearch(query);
}

Version Routing

Shared Base Route

[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public abstract class BaseApiController : ControllerBase
{
    // Shared functionality
}

[ApiVersion("1.0")]
public class ProductsController : BaseApiController
{
    // Inherits route: api/v1/products
}

Custom Routes per Version

[ApiVersion("1.0")]
[Route("api/v1/products")]
public class ProductsV1Controller : ControllerBase { }

[ApiVersion("2.0")]
[Route("api/v2/products")]
public class ProductsV2Controller : ControllerBase { }

Best Practices

  • Choose URL path versioning for simplicity and discoverability
  • Use semantic versioning (1.0, 2.0, 2.1)
  • Don’t version every change - only breaking changes
  • Document version differences clearly
  • Support at least 2 versions simultaneously
  • Give advance notice before deprecating versions
  • Provide migration guides for breaking changes
  • Use version-neutral endpoints for common functionality
  • Start new APIs at v1.0
  • Increment major version for breaking changes
  • Use minor versions (2.1) for significant non-breaking additions
  • Announce deprecation at least 6 months before removal
  • Use api-supported-versions header
  • Return api-deprecated-versions for old versions
  • Include version in error responses
  • Provide version-specific documentation

Handling Version Requests

Default Version

Serve default version when unspecified:
services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(2, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
});

Version Not Found

Return 400 Bad Request for invalid versions:
GET /api/v99/products

HTTP/1.1 400 Bad Request
{
  "error": {
    "code": "UnsupportedApiVersion",
    "message": "The HTTP resource that matches the request URI 'http://localhost/api/v99/products' does not support the API version '99'.",
    "innerError": null
  }
}

Testing Versioned APIs

Unit Tests

[Fact]
public async Task GetProducts_V1_ReturnsV1Format()
{
    // Arrange
    var controller = new ProductsV1Controller(_service);

    // Act
    var result = await controller.GetAll();

    // Assert
    var okResult = Assert.IsType<ActionResult<List<ProductDtoV1>>>(result);
    Assert.NotNull(okResult.Value);
}

[Fact]
public async Task GetProducts_V2_ReturnsV2Format()
{
    // Arrange
    var controller = new ProductsV2Controller(_service);

    // Act
    var result = await controller.GetAll();

    // Assert
    var okResult = Assert.IsType<ActionResult<List<ProductDtoV2>>>(result);
    Assert.NotNull(okResult.Value);
}

Integration Tests

[Fact]
public async Task GetProducts_WithV1Header_ReturnsV1Data()
{
    // Arrange
    var client = _factory.CreateClient();
    client.DefaultRequestHeaders.Add("X-Api-Version", "1.0");

    // Act
    var response = await client.GetAsync("/api/products");

    // Assert
    response.EnsureSuccessStatusCode();
    var products = await response.Content.ReadFromJsonAsync<List<ProductDtoV1>>();
    Assert.NotNull(products);
}

Installation

Intent.AspNetCore.Versioning

Dependencies

  • Intent.AspNetCore (>= 5.0.0)
  • Intent.AspNetCore.Controllers (>= 5.2.0)
  • Intent.Modelers.Services
  • Intent.Metadata.WebApi

Integration

Automatically integrates with:
  • Intent.AspNetCore.Swashbuckle - Version-aware Swagger documentation
  • Intent.AspNetCore.Scalar - Version-aware Scalar documentation

Next Steps

Swashbuckle

Document multiple API versions

Controllers

Create versioned controllers

Scalar

Modern docs for versioned APIs

Build docs developers (and LLMs) love