API Versioning in ConnectSoft Templates¶
Purpose & Overview¶
API Versioning is a critical practice for managing the evolution of APIs over time while maintaining backward compatibility and allowing clients to migrate gradually. In ConnectSoft Templates and solutions, API versioning strategies can be implemented across REST API, GraphQL, gRPC, and CoreWCF service models to provide clear versioning mechanisms that align with Clean Architecture principles.
Why API Versioning?¶
API versioning provides several key benefits:
- Backward Compatibility: Support multiple API versions simultaneously
- Gradual Migration: Allow clients to migrate at their own pace
- Breaking Changes: Introduce breaking changes without disrupting existing clients
- Clear Contract: Explicitly declare API version expectations
- Evolution: Evolve APIs while maintaining stability
- Client Communication: Clear communication about deprecation and migration paths
Versioning Strategies¶
1. URL Path Versioning¶
Version is included in the URL path:
Pros:
- Explicit and visible
- Easy to cache
- Simple routing
Cons:
- URLs change between versions
- Can clutter URL structure
2. Query String Versioning¶
Version is specified as a query parameter:
Pros:
- Clean URLs
- Easy to implement
- Optional versioning
Cons:
- Less visible
- Can be forgotten
- Harder to cache
3. Header Versioning¶
Version is specified in HTTP headers:
Pros:
- Clean URLs
- RESTful
- Can use custom headers
Cons:
- Less discoverable
- Requires client changes
- Harder to test
4. Accept Header Versioning¶
Version is specified via content negotiation:
Pros:
- RESTful
- Supports content type negotiation
- Clean URLs
Cons:
- Complex to implement
- Less intuitive
- Browser testing challenges
Implementation for REST API¶
Using Microsoft.AspNetCore.Mvc.Versioning¶
// Program.cs or Startup.cs
public static IServiceCollection AddApiVersioning(this IServiceCollection services)
{
services.AddApiVersioning(options =>
{
// Default API version
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
// Report API versions in response headers
options.ReportApiVersions = true;
// Support multiple versioning schemes
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(), // /api/v1/...
new QueryStringApiVersionReader("api-version"), // ?api-version=1.0
new HeaderApiVersionReader("X-Version"), // X-Version: 1.0
new MediaTypeApiVersionReader("ver")); // Accept: application/json;ver=1.0
});
services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
return services;
}
Versioned Controllers¶
// V1 Controller
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class EntitiesV1Controller : ControllerBase
{
private readonly IEntitiesRetriever retriever;
private readonly IEntitiesProcessor processor;
private readonly IMapper mapper;
public EntitiesV1Controller(
IEntitiesRetriever retriever,
IEntitiesProcessor processor,
IMapper mapper)
{
this.retriever = retriever;
this.processor = processor;
this.mapper = mapper;
}
[HttpGet]
[MapToApiVersion("1.0")]
public async Task<IActionResult> GetEntity(
[FromQuery] GetEntityDetailsRequestV1 request,
CancellationToken token = default)
{
var input = this.mapper.Map<GetEntityDetailsInput>(request);
var entity = await this.retriever.GetEntityDetails(input, token);
if (entity == null)
{
return NotFound();
}
var response = this.mapper.Map<GetEntityDetailsResponseV1>(entity);
return Ok(response);
}
}
// V2 Controller
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class EntitiesV2Controller : ControllerBase
{
private readonly IEntitiesRetriever retriever;
private readonly IEntitiesProcessor processor;
private readonly IMapper mapper;
public EntitiesV2Controller(
IEntitiesRetriever retriever,
IEntitiesProcessor processor,
IMapper mapper)
{
this.retriever = retriever;
this.processor = processor;
this.mapper = mapper;
}
[HttpGet]
[MapToApiVersion("2.0")]
public async Task<IActionResult> GetEntity(
[FromQuery] GetEntityDetailsRequestV2 request,
CancellationToken token = default)
{
var input = this.mapper.Map<GetEntityDetailsInput>(request);
var entity = await this.retriever.GetEntityDetails(input, token);
if (entity == null)
{
return NotFound();
}
// V2 includes additional fields
var response = this.mapper.Map<GetEntityDetailsResponseV2>(entity);
return Ok(response);
}
}
Single Controller with Multiple Versions¶
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class EntitiesController : ControllerBase
{
private readonly IEntitiesRetriever retriever;
private readonly IMapper mapper;
[HttpGet]
[MapToApiVersion("1.0")]
public async Task<IActionResult> GetV1(
[FromQuery] GetEntityDetailsRequestV1 request,
CancellationToken token = default)
{
// V1 implementation
var input = this.mapper.Map<GetEntityDetailsInput>(request);
var entity = await this.retriever.GetEntityDetails(input, token);
var response = this.mapper.Map<GetEntityDetailsResponseV1>(entity);
return Ok(response);
}
[HttpGet]
[MapToApiVersion("2.0")]
public async Task<IActionResult> GetV2(
[FromQuery] GetEntityDetailsRequestV2 request,
CancellationToken token = default)
{
// V2 implementation with additional features
var input = this.mapper.Map<GetEntityDetailsInput>(request);
var entity = await this.retriever.GetEntityDetails(input, token);
var response = this.mapper.Map<GetEntityDetailsResponseV2>(entity);
return Ok(response);
}
}
Version-Specific DTOs¶
// V1 Request
public class GetEntityDetailsRequestV1
{
[Required]
public Guid ObjectId { get; set; }
}
// V2 Request (extends V1)
public class GetEntityDetailsRequestV2
{
[Required]
public Guid ObjectId { get; set; }
// V2 adds include related entities
public bool IncludeRelatedEntities { get; set; }
}
// V1 Response
public class GetEntityDetailsResponseV1
{
public EntityDtoV1 FoundEntity { get; set; } = null!;
}
// V2 Response (extends V1)
public class GetEntityDetailsResponseV2
{
public EntityDtoV2 FoundEntity { get; set; } = null!;
// V2 adds related entities
public List<RelatedEntityDto>? RelatedEntities { get; set; }
}
Swagger Integration¶
// SwaggerExtensions.cs
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "API",
Version = "v1",
Description = "Version 1 of the API"
});
c.SwaggerDoc("v2", new OpenApiInfo
{
Title = "API",
Version = "v2",
Description = "Version 2 of the API"
});
// Resolve version-specific schemas
c.ResolveConflictingActions(apiDescriptions => apiDescriptions.First());
});
// Use versioned Swagger UI
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "API V1");
options.SwaggerEndpoint("/swagger/v2/swagger.json", "API V2");
});
Implementation for GraphQL¶
Schema Versioning¶
GraphQL uses schema evolution rather than explicit versioning:
// Use different schema names for versions
services
.AddGraphQLServer("v1")
.AddQueryType<EntityQueriesV1>()
.AddType<EntityV1>();
services
.AddGraphQLServer("v2")
.AddQueryType<EntityQueriesV2>()
.AddType<EntityV2>();
// Map to different endpoints
endpoints.MapGraphQL("/graphql/v1").WithOptions(new GraphQLServerOptions
{
SchemaName = "v1"
});
endpoints.MapGraphQL("/graphql/v2").WithOptions(new GraphQLServerOptions
{
SchemaName = "v2"
});
Deprecation Strategy¶
Use GraphQL deprecation for gradual migration:
public class EntityV2
{
[GraphQLDescription("The unique identifier")]
public Guid ObjectId { get; set; }
[GraphQLDescription("The name")]
public string? Name { get; set; }
[GraphQLDescription("The status (deprecated: use statusCode instead)")]
[GraphQLDeprecated("Use statusCode instead")]
public string? Status { get; set; }
[GraphQLDescription("The status code")]
public int StatusCode { get; set; }
}
Implementation for gRPC¶
gRPC uses package namespacing for versioning:
// v1/service.proto
syntax = "proto3";
package connectsoft.service.v1;
service EntitiesService {
rpc GetEntity(GetEntityRequest)
returns (GetEntityResponse);
}
message GetEntityRequest {
string object_id = 1;
}
message GetEntityResponse {
Entity entity = 1;
}
// v2/service.proto
syntax = "proto3";
package connectsoft.service.v2;
service EntitiesService {
rpc GetEntity(GetEntityRequest)
returns (GetEntityResponse);
}
message GetEntityRequest {
string object_id = 1;
bool include_related_entities = 2; // V2 addition
}
message GetEntityResponse {
Entity entity = 1;
repeated RelatedEntity related_entities = 2; // V2 addition
}
Implementation for CoreWCF¶
CoreWCF uses namespace versioning:
// V1 Service Contract
[ServiceContract(Namespace = "http://connectsoft.com/service/v1")]
public interface IEntitiesServiceV1
{
[OperationContract]
Task<GetEntityResponse> GetEntityAsync(
GetEntityRequest request);
}
// V2 Service Contract
[ServiceContract(Namespace = "http://connectsoft.com/service/v2")]
public interface IEntitiesServiceV2
{
[OperationContract]
Task<GetEntityResponseV2> GetEntityAsync(
GetEntityRequestV2 request);
}
// Map to different endpoints
endpoints.UseSoapEndpoint<IEntitiesServiceV1>(
"/EntitiesService/v1",
...);
endpoints.UseSoapEndpoint<IEntitiesServiceV2>(
"/EntitiesService/v2",
...);
Version Deprecation Strategy¶
Deprecation Headers¶
[ApiController]
[ApiVersion("1.0", Deprecated = true)]
[Route("api/v{version:apiVersion}/[controller]")]
public class EntitiesV1Controller : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public async Task<IActionResult> GetEntity(...)
{
// Add deprecation header
Response.Headers.Add("X-API-Deprecated", "true");
Response.Headers.Add("X-API-Deprecation-Date", "2025-12-31");
Response.Headers.Add("X-API-Sunset-Date", "2026-06-30");
// Implementation...
}
}
Deprecation Documentation¶
services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new OpenApiInfo
{
Title = "API",
Version = "v1",
Description = "Version 1 of the API (Deprecated)",
Deprecated = true,
Extensions = new Dictionary<string, IOpenApiExtension>
{
{
"x-deprecation-date", new OpenApiString("2025-12-31")
},
{
"x-sunset-date", new OpenApiString("2026-06-30")
}
}
});
});
Best Practices¶
Do's¶
-
Plan versioning strategy early
- Decide on versioning approach before first release
- Document versioning policy
- Communicate with API consumers
-
Maintain backward compatibility
- Avoid breaking changes when possible
- Use additive changes (add fields, don't remove)
- Provide migration guides
-
Document version changes
- Maintain changelog
- Document breaking changes
- Provide migration examples
-
Support multiple versions temporarily
- Allow overlap period for migration
- Set clear deprecation timelines
- Monitor version usage
-
Version consistently
- Use semantic versioning (MAJOR.MINOR.PATCH)
- Increment major version for breaking changes
- Increment minor version for additive changes
-
Monitor version adoption
- Track which versions are in use
- Identify clients on deprecated versions
- Proactive migration support
Don'ts¶
-
Don't version too frequently
- Avoid unnecessary versions
- Group related changes
- Consider impact on clients
-
Don't break contracts unexpectedly
- Follow deprecation timelines
- Provide advance notice
- Support old versions during transition
-
Don't ignore version management
- Clean up old versions eventually
- Archive deprecated versions
- Maintain version documentation
Version Lifecycle¶
1. Active
↓
2. Deprecated (warnings, migration period)
↓
3. Sunset (read-only, no new features)
↓
4. Retired (removed)
Example Timeline¶
- V1 Active: 2024-01-01 to 2025-12-31
- V1 Deprecated: 2025-12-31 to 2026-06-30
- V1 Sunset: 2026-06-30 to 2026-12-31
- V1 Retired: After 2026-12-31
Summary¶
API Versioning in ConnectSoft Templates provides:
- ✅ Multiple Strategies: URL, query string, header, content negotiation
- ✅ Framework Support: Microsoft.AspNetCore.Mvc.Versioning integration
- ✅ Cross-Protocol: Versioning for REST, GraphQL, gRPC, CoreWCF
- ✅ Deprecation Support: Clear deprecation and sunset timelines
- ✅ Documentation: Swagger/OpenAPI integration
- ✅ Migration Path: Clear paths for client migration
- ✅ Backward Compatibility: Support for multiple versions simultaneously
By following these patterns, API versioning becomes a manageable process that allows APIs to evolve while maintaining stability and client compatibility.
Related Documentation¶
- REST API: REST API implementation patterns
- GraphQL: GraphQL implementation and schema versioning
- gRPC: gRPC implementation and package versioning
- CoreWCF: CoreWCF SOAP services and namespace versioning
- Service Model: Service contracts and DTOs
- Swagger: Swagger/OpenAPI integration and versioned documentation
- Scalar: Scalar API documentation with version support