ASP.NET Core Web API

Last Updated: 9/17/2026

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

  1. Inception & Release (Version is live)
  2. Active Adoption (Version is widely used)
  3. Deprecation Trigger (A successor is born, old version is flagged)
  4. Migration Window (Users move away)
  5. 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);  
	      }
    });
}

Using Header Versioning

  • In program.cs
options.ApiVersionReader = new HeaderApiVersionReader("api-version");

Using URL Versioning

  • In program.cs
options.ApiVersionReader = new UrlSegmentApiVersionReader();
  • In controller
[Route("api/v{version:apiVersion}/products")]