API Versioning
Overview
- Web Api may be consumed by different clients like Angular/React applications, Mobile apps, Desktop apps, Third party systems, Microservices.
- Changing the contract may break existing clients.
- API Versioning allows us to evolve our API in a controlled way without breaking existing consumers
What is Web API Versioning?
- Web API Versioning is a technique that allows a single API to expose multiple versions, such as v1, v2, v3, and so on.
Why API Versioning Matters
- Prevents breaking existing clients when API contracts change.
- Allows older clients to continue working while newer ones adopt improved versions.
- Essential in enterprise apps, public APIs, mobile backends, and microservices.
API Contract vs Internal Implementation
- Contract: What clients see (URLs, methods, request/response structure, validation, status codes).
- Internal implementation: Server-side changes (logging, caching, database optimizations).
- Only contract changes require new versions; internal changes don’t.
When to create new API version
- We generally create a new API version when we introduce a breaking change.
Breaking vs Non-Breaking Changes
Breaking changes
- A breaking change is any change that may force an existing client to modify its code.
- Examples
- Renaming a response property
- Removing a response property
- Changing the data type of a field
- Changing request body structure
- Making an optional field mandatory
- Changing validation rules in a way that rejects older requests
- Changing the route structure in a way that old clients no longer match it
- Changing the status code behavior in a way that clients depend on
- Returning a completely different response model
- Changing authentication requirements unexpectedly
Non-breaking changes:
- A non-breaking change is one that does not require existing clients to update immediately.
- Examples
- Adding a new optional response field
- Adding a new optional query parameter
- Improving internal performance
- Fixing internal bugs without changing the contract
- Improving logging, tracing, or caching
- Changing internal database logic while keeping the same API behavior
API Version Lifecycle
- Inception & Release (Version is live)
- Active Adoption (Version is widely used)
- Deprecation Trigger (A successor is born, old version is flagged)
- Migration Window (Users move away)
- Sunset
Versioning Strategies
- Query String →
/api/products?api-version=1.0
- Header →
api-version: 1.0
- Media Type →
Accept: application/json;v=1.0
- URL Path →
/api/v1.0/products
Best use cases:
- Query string → easiest for testing.
- Header → clean URLs, good for internal APIs.
- Media type → aligns with HTTP standards.
- URL path → best for public APIs.
Implementation in ASP.NET Core
- Create new ASP.NET core web API project named APIVersioningDemo using .net 8.0
- Install packages version 8.0.0
Asp.Versioning.Mvc
Asp.Versioning.Mvc.ApiExplorer
- Decorate Controller with attribute
ApiVersion specifying the version number
[ApiVersion("1.0")]
- Create ConfigureSwaggerOptions.cs. Used to configure Swagger generation dynamically
based on the API versions discovered by ASP.NET Core API Versioning. Creates a separate Swagger document for each API version.
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider _provider;
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
{
_provider = provider;
}
public void Configure(SwaggerGenOptions options)
{
// Dynamically add a swagger document for every version discovered
foreach (var description in _provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, CreateInfoForApiVersion(description));
}
}
private static OpenApiInfo CreateInfoForApiVersion(ApiVersionDescription description)
{
var info = new OpenApiInfo()
{
Title = "My API",
Version = description.ApiVersion.ToString(),
Description = "An application with versioned endpoints."
};
if (description.IsDeprecated)
{
info.Description += " This API version has been deprecated.";
}
return info;
}
}
- Register ConfigureSwaggerOptions
builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
- Create SwaggerDefaultValues.cs. Used to autofill the version number
public class SwaggerDefaultValues : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var apiDescription = context.ApiDescription;
// 1. Set the operation's deprecated status automatically
operation.Deprecated |= apiDescription.IsDeprecated();
if (operation.Parameters == null)
{
return;
}
// 2. Loop through parameters to find and auto-fill the version header
foreach (var parameter in operation.Parameters)
{
var description = apiDescription.ParameterDescriptions
.First(p => p.Name == parameter.Name);
// Provide a description if it's empty
if (string.IsNullOrEmpty(parameter.Description))
{
parameter.Description = description.ModelMetadata?.Description;
}
// If the parameter has a default value defined by the API explorer, inject it
if (parameter.Schema.Default == null && description.DefaultValue != null)
{
parameter.Schema.Default = new OpenApiString(description.DefaultValue.ToString());
}
parameter.Required |= description.IsRequired;
}
}
}
- Register SwaggerDefaultValues.cs
builder.Services.AddSwaggerGen(options => options.OperationFilter<SwaggerDefaultValues>());
Using Querystring Versioning
- Add services related to ApiVersioning and ApiExplorer in program.cs
// register services
builder.Services.AddApiVersioning(options =>
{
options.AssumeDefaultVersionWhenUnspecified = false;
options.DefaultApiVersion = new ApiVersion(1, 0);
options.ReportApiVersions = true;
// Read the API version from the query string parameter named "api-version".
// Example: /api/products?api-version=2.0
options.ApiVersionReader = new QueryStringApiVersionReader("api-version");
})
.AddMvc()
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
- Configure Swagger UI in program.cs
// add middlewares
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
var provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
foreach (var description in provider.ApiVersionDescriptions)
{
var url = $"/swagger/{description.GroupName}/swagger.json";
var name = description.GroupName.ToUpperInvariant();
options.SwaggerEndpoint(url, name);
}
});
}
options.ApiVersionReader = new HeaderApiVersionReader("api-version");
Using URL Versioning
options.ApiVersionReader = new UrlSegmentApiVersionReader();
[Route("api/v{version:apiVersion}/products")]