FullStackHero provides built-in API versioning using Asp.Versioning , enabling you to evolve your API while maintaining backward compatibility.
Overview
The API versioning system provides:
URL Segment Versioning : Version identifiers in the URL path (e.g., /api/v1/users)
Default Versioning : Automatic fallback to v1.0 when no version is specified
API Explorer Integration : Versioned OpenAPI/Swagger documentation
Version Reporting : Response headers indicate supported versions
Configuration
API versioning is configured in the Web building block:
public static IServiceCollection AddHeroVersioning (
this IServiceCollection services )
{
services
. AddApiVersioning ( options =>
{
options . ReportApiVersions = true ;
options . DefaultApiVersion = new ApiVersion ( 1 , 0 );
options . AssumeDefaultVersionWhenUnspecified = true ;
options . ApiVersionReader = new UrlSegmentApiVersionReader ();
})
. AddApiExplorer ( options =>
{
options . GroupNameFormat = "'v'VVV" ;
options . SubstituteApiVersionInUrl = true ;
})
. EnableApiVersionBinding ();
return services ;
}
Configuration Options
Include api-supported-versions and api-deprecated-versions headers in responses.
The default API version when the client doesn’t specify one.
AssumeDefaultVersionWhenUnspecified
Use the default version when no version is provided in the request.
How to read the API version from the request. Uses UrlSegmentApiVersionReader for URL-based versioning.
URL Segment Versioning
API versions are specified in the URL path:
GET /api/v1/users
GET /api/v2/users
GET /api/v3/users
This is the most explicit and cache-friendly versioning strategy.
Defining API Versions
Version Sets
Group related endpoints under a version set:
var versionSet = app . NewApiVersionSet ()
. HasApiVersion ( new ApiVersion ( 1 , 0 ))
. HasApiVersion ( new ApiVersion ( 2 , 0 ))
. ReportApiVersions ()
. Build ();
var group = app . MapGroup ( "/api/v{version:apiVersion}/users" )
. WithApiVersionSet ( versionSet );
Versioned Endpoints
Map endpoints to specific API versions:
// V1 endpoint
group . MapGet ( "/" , GetUsersV1 )
. MapToApiVersion ( new ApiVersion ( 1 , 0 ))
. WithName ( "GetUsers_v1" )
. WithSummary ( "Get users (v1)" );
// V2 endpoint with enhanced response
group . MapGet ( "/" , GetUsersV2 )
. MapToApiVersion ( new ApiVersion ( 2 , 0 ))
. WithName ( "GetUsers_v2" )
. WithSummary ( "Get users (v2 - includes profile pictures)" );
Versioning Strategies
Feature Module Structure
Organize versions within feature folders:
Modules/Identity/Features/
├── v1/
│ ├── Users/
│ │ ├── GetUser/
│ │ │ ├── GetUserQuery.cs
│ │ │ ├── GetUserHandler.cs
│ │ │ └── GetUserEndpoint.cs
│ └── Tokens/
│ ├── GenerateToken/
│ └── RefreshToken/
├── v2/
│ ├── Users/
│ │ ├── GetUser/
│ │ │ ├── GetUserQuery.cs # V2 with enhanced fields
│ │ │ ├── GetUserHandler.cs
│ │ │ └── GetUserEndpoint.cs
│ └── ...
FullStackHero uses this approach: each version is a separate folder under the feature’s Features directory.
Shared Code
Extract common logic to shared services or base classes:
// Shared service used by both v1 and v2
public class UserService
{
public async Task < User > GetUserByIdAsync ( Guid id , CancellationToken ct )
{
// Common user retrieval logic
}
}
// V1 handler
public class GetUserV1Handler : IQueryHandler < GetUserV1Query , UserV1Response >
{
private readonly UserService _userService ;
public async ValueTask < UserV1Response > Handle (
GetUserV1Query query , CancellationToken ct )
{
var user = await _userService . GetUserByIdAsync ( query . UserId , ct );
return new UserV1Response ( user . Id , user . Name , user . Email );
}
}
// V2 handler with extended response
public class GetUserV2Handler : IQueryHandler < GetUserV2Query , UserV2Response >
{
private readonly UserService _userService ;
public async ValueTask < UserV2Response > Handle (
GetUserV2Query query , CancellationToken ct )
{
var user = await _userService . GetUserByIdAsync ( query . UserId , ct );
return new UserV2Response (
user . Id ,
user . Name ,
user . Email ,
user . ProfilePictureUrl , // New in V2
user . CreatedAt ); // New in V2
}
}
When ReportApiVersions is enabled, responses include version information:
HTTP / 1.1 200 OK
api-supported-versions : 1.0, 2.0
api-deprecated-versions : (none)
Comma-separated list of supported API versions
Comma-separated list of deprecated API versions
Deprecating API Versions
Mark versions as deprecated:
var versionSet = app . NewApiVersionSet ()
. HasDeprecatedApiVersion ( new ApiVersion ( 1 , 0 )) // Deprecated
. HasApiVersion ( new ApiVersion ( 2 , 0 ))
. HasApiVersion ( new ApiVersion ( 3 , 0 ))
. ReportApiVersions ()
. Build ();
Deprecated versions still work but are marked in response headers:
api-deprecated-versions : 1.0
Communicate deprecation timelines to API consumers well in advance. Provide migration guides to newer versions.
OpenAPI/Swagger Integration
API versions are automatically reflected in OpenAPI/Swagger documentation:
Separate Specs : Each version gets its own OpenAPI specification
Version Selector : Swagger UI includes a version dropdown
Substitution : The {version:apiVersion} placeholder is replaced with the actual version
{
"openapi" : "3.0.1" ,
"info" : {
"title" : "FSH API" ,
"version" : "v1"
},
"paths" : {
"/api/v1/users" : { ... },
"/api/v1/users/{id}" : { ... }
}
}
OpenAPI Learn more about OpenAPI configuration and documentation
Example: Versioning a Feature
Let’s version the “Get User” endpoint:
Create V1 Implementation
v1/GetUser/GetUserEndpoint.cs
public static RouteHandlerBuilder MapGetUserEndpoint (
this IEndpointRouteBuilder endpoints )
{
return endpoints . MapGet ( "/users/{id:guid}" ,
async ( Guid id , IMediator mediator , CancellationToken ct ) =>
{
var query = new GetUserV1Query ( id );
var result = await mediator . Send ( query , ct );
return TypedResults . Ok ( result );
})
. MapToApiVersion ( new ApiVersion ( 1 , 0 ))
. WithName ( "GetUser_v1" )
. RequirePermission ( "users.view" );
}
Add V2 with Breaking Changes
v2/GetUser/GetUserEndpoint.cs
public static RouteHandlerBuilder MapGetUserEndpoint (
this IEndpointRouteBuilder endpoints )
{
return endpoints . MapGet ( "/users/{id:guid}" ,
async ( Guid id , IMediator mediator , CancellationToken ct ) =>
{
var query = new GetUserV2Query ( id );
var result = await mediator . Send ( query , ct );
return TypedResults . Ok ( result );
})
. MapToApiVersion ( new ApiVersion ( 2 , 0 ))
. WithName ( "GetUser_v2" )
. RequirePermission ( "users.view" );
}
Define Version Set
var versionSet = app . NewApiVersionSet ()
. HasApiVersion ( new ApiVersion ( 1 , 0 ))
. HasApiVersion ( new ApiVersion ( 2 , 0 ))
. ReportApiVersions ()
. Build ();
var group = app . MapGroup ( "/api/v{version:apiVersion}" )
. WithApiVersionSet ( versionSet );
group . MapGetUserEndpoint ();
Register Endpoints
Both v1 and v2 handlers are registered in the DI container and routed based on the URL version.
Version Negotiation
Explicit Version
Client specifies the exact version:
GET /api/v2/users HTTP/1.1
Default Version
Client omits the version (uses default v1.0):
This is only possible when AssumeDefaultVersionWhenUnspecified is true.
Best Practices
Follow semantic versioning principles:
Major version (v1, v2, v3): Breaking changes
Minor version (v1.1, v1.2): Backward-compatible features
Patch version (v1.0.1): Backward-compatible bug fixes
Avoid Breaking Changes in Minor Versions
Only introduce breaking changes in major versions. Minor and patch versions should be backward-compatible.
Deprecate Before Removing
Mark versions as deprecated for at least one release cycle before removing them.
Document Changes Between Versions
Maintain a changelog that clearly documents what changed in each version.
Write integration tests for each supported API version to ensure they work independently.
Testing Versioned APIs
Test that version routing works correctly:
[ Theory ]
[ InlineData ( "v1" , "UserV1Response" )]
[ InlineData ( "v2" , "UserV2Response" )]
public async Task GetUser_ReturnsCorrectVersion (
string version , string expectedType )
{
// Arrange
var client = _factory . CreateClient ();
var userId = Guid . NewGuid ();
// Act
var response = await client . GetAsync (
$"/api/ { version } /users/ { userId } " );
// Assert
response . EnsureSuccessStatusCode ();
var content = await response . Content . ReadAsStringAsync ();
Assert . Contains ( expectedType , content );
}
OpenAPI Generate versioned OpenAPI specifications
Endpoints Learn about endpoint mapping and routing
CQRS Understand command and query patterns in versioned features
Testing Write tests for versioned APIs