ASP.NET Core Versioning Web APIs Step by step Implementation and Top 10 Questions and Answers
 Last Update: April 01, 2025      13 mins read      Difficulty-Level: beginner

ASP.NET Core Versioning Web APIs: A Detailed Guide for Beginners

Introduction API (Application Programming Interface) versioning is a fundamental aspect of designing robust and scalable web services. As your application grows and evolves, newer versions of the API are often necessary to introduce new features or improve existing ones. However, introducing a new version should not affect clients consuming the existing version. ASP.NET Core offers comprehensive support for API versioning, making it easier for developers to manage changes over time without breaking existing clients.

This guide will walk you through the basics of API versioning in ASP.NET Core, from setting up the environment to implementing various versioning strategies. By the end of this step-by-step tutorial, you should have a solid understanding of how to manage API versions effectively.


Step 1: Setup ASP.NET Core Project

1.1 Creating a New Project Before diving into versioning, you need an ASP.NET Core Web API project to work with. You can create this project either via the Visual Studio IDE or using the .NET Core CLI. Here’s how to do it using the CLI:

dotnet new webapi -n MyWebApiVersioningApp
cd MyWebApiVersioningApp

1.2 Adding Necessary NuGet Packages To enable API versioning, you need to add the Microsoft.AspNetCore.Mvc.Versioning NuGet package to your project. You can add it via the NuGet Package Manager Console or using the dotnet CLI:

dotnet add package Microsoft.AspNetCore.Mvc.Versioning

1.3 Basic API Controller Ensure you have an initial API controller. For simplicity, we’ll use a ValuesController:

using Microsoft.AspNetCore.Mvc;

namespace MyWebApiVersioningApp.Controllers
{
    [ApiController]
    [Route("api/[controller]")]
    public class ValuesController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get() => Ok(new string[] { "value1", "value2" });
    }
}

Step 2: Understanding Versioning Strategies

API versioning can be implemented in various ways, depending on your requirements and preferences. Common strategies include URI-based versioning, query string versioning, and header versioning. Let's explore each strategy in depth.

2.1 URI-Based Versioning In URI-based versioning, the version number is included in the API endpoint URL. This is the most popular and straightforward approach.

Example: GET api/v1/values for version 1 GET api/v2/values for version 2

2.2 Query String Versioning Query string versioning uses query parameters to specify the version number. This approach keeps URLs clean but might conflict with query parameters used for filtering or sorting data.

Example: GET api/values?version=1 GET api/values?version=2

2.3 Header Versioning Header versioning involves sending the version number in an HTTP request header. It doesn’t clutter the URL or query string but requires client applications to set the appropriate header.

Example: Set api-version header to 1 for version 1 Set api-version header to 2 for version 2


Step 3: Implementing URI-Based Versioning

3.1 Configuring Services First, you need to configure the API versioning services in your Startup.cs (Program.cs in .NET 6 and later):

using Microsoft.AspNetCore.Builder;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerGen;

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

// Add versioned API explorer, which also adds IApiVersionDescriptionProvider service
builder.Services.AddVersionedApiExplorer(o =>
{
    o.GroupNameFormat = "'v'VVV";
    o.SubstituteApiVersionInUrl = true;
});

builder.Services.AddApiVersioning(o =>
{
    o.ReportApiVersions = true;
    o.UseApiBehavior = true;
    o.AssumeDefaultVersionWhenUnspecified = true;
    o.DefaultApiVersion = new Microsoft.AspNetCore.Mvc.ApiVersion(1, 0);
    // Optionally, you can configure other versioning strategies
    // o.ApiVersionReader = new UrlSegmentApiVersionReader();
    o.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new QueryStringApiVersionReader("api-version"),
        new HeaderApiVersionReader("api-version"));
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
}

app.UseHttpsRedirection();
app.UseAuthorization();
app.MapControllers();
app.Run();

3.2 Adding Version Attributes to Controllers Next, you must apply version attributes to your controllers to specify the supported versions.

[ApiVersion("1.0")]
[ApiVersion("2.0")]
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
public class ValuesController : ControllerBase
{
    [HttpGet]
    public IActionResult Get() => Ok(new string[] { "value1", "value2" });

    [HttpGet]
    [MapToApiVersion("2.0")]
    public IActionResult GetV2() => Ok(new string[] { "value1 v2", "value2 v2" });
}

3.3 Testing URI-Based Versioning Now, test your API by sending requests to different URLs:

  • GET https://localhost:5001/api/v1/values should return ["value1", "value2"]
  • GET https://localhost:5001/api/v2/values should return ["value1 v2", "value2 v2"]

Step 4: Implementing Query String Versioning

4.1 Configuring Services Enable query string versioning by adjusting the ApiVersionReader in Startup.cs (Program.cs). The example above already includes query string versioning.

builder.Services.AddApiVersioning(o =>
{
    // ...
    o.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new QueryStringApiVersionReader("api-version"), // Enable query string versioning
        new HeaderApiVersionReader("api-version"));
    // ...
});

4.2 Testing Query String Versioning Test your API by appending the api-version query string:

  • GET https://localhost:5001/api/values?api-version=1 should return ["value1", "value2"]
  • GET https://localhost:5001/api/values?api-version=2 should return ["value1 v2", "value2 v2"]

Step 5: Implementing Header Versioning

5.1 Configuring Services Enable header versioning by adjusting the ApiVersionReader in Startup.cs (Program.cs). The example above already includes header versioning.

builder.Services.AddApiVersioning(o =>
{
    // ...
    o.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new QueryStringApiVersionReader("api-version"),
        new HeaderApiVersionReader("api-version")); // Enable header versioning
    // ...
});

5.2 Testing Header Versioning Test your API by setting the api-version header:

  • Use tools like Postman or curl:
    • GET https://localhost:5001/api/values with headers api-version: 1 should return ["value1", "value2"]
    • GET https://localhost:5001/api/values with headers api-version: 2 should return ["value1 v2", "value2 v2"]

Step 6: Advanced Versioning Strategies

6.1 Dealing with Removed or Deprecated Versions When removing or deprecating a version, you should clearly communicate this to your API consumers. You can do this by adding a deprecation header or providing a notice in your API documentation.

Example: Adding a deprecation header for version 1:

[HttpGet]
[MapToApiVersion("1.0")]
[ProducesResponseType(StatusCodes.Status410Gone, Type = typeof(string))]
public IActionResult GetV1()
{
    Response.Headers.Add("Deprecated", "true");
    Response.Headers.Add("Sunset", DateTime.UtcNow.AddDays(90).ToString("R"));
    return Content("This version will be deprecated. Please upgrade to v2.");
}

6.2 Multiple Version Strategies You can support multiple versioning strategies simultaneously. For example, you might want to use URI-based versioning for new features and header versioning for legacy clients.

Example:

builder.Services.AddApiVersioning(o =>
{
    o.ApiVersionReader = ApiVersionReader.Combine(
        new UrlSegmentApiVersionReader(),
        new HeaderApiVersionReader("api-version"));
});

6.3 Custom Version Attributes You can define custom actions or logic when a particular version is invoked using custom attributes.

Example:

public class DeprecatedAttribute : Attribute, IActionModelConvention
{
    public void Apply(ActionModel action)
    {
        var deprecated = new DeprecatedVersionResponseProvider();
        action.Filters.Add(new CustomResultFilterAttribute(deprecated));
    }
}

public class CustomResultFilterAttribute : ActionFilterAttribute
{
    private readonly DeprecatedVersionResponseProvider _deprecated;

    public CustomResultFilterAttribute(DeprecatedVersionResponseProvider deprecated)
    {
        _deprecated = deprecated;
    }

    public override void OnActionExecuting(ActionExecutingContext context)
    {
        var apiVersion = context.HttpContext.GetRequestedApiVersion();
        if (apiVersion != null && apiVersion.MajorVersion < 2)
        {
            context.Result = new ObjectResult(_deprecated.GetMessage(apiVersion))
            {
                StatusCode = (int)HttpStatusCode.Gone
            };
        }
        base.OnActionExecuting(context);
    }
}

public class DeprecatedVersionResponseProvider
{
    public string GetMessage(ApiVersion apiVersion) => $"Version {apiVersion.MajorVersion} is deprecated. Please upgrade to a newer version.";
}

Usage:

[HttpGet]
[MapToApiVersion("1.0")]
[Deprecated] // Apply custom attribute for deprecated versions
public IActionResult GetV1()
{
    return Ok(new string[] { "value1", "value2" });
}

Step 7: API Versioning with Swagger

7.1 Setting Up Swagger Integrate Swagger (OpenAPI) to document your API, making it easier for developers to understand and test different versions.

Install Swagger Packages:

dotnet add package Swashbuckle.AspNetCore

Configure Swagger in Startup.cs (Program.cs):

var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();

// Add API versioning
builder.Services.AddApiVersioning(o =>
{
    // ...
});

// Add versioned API explorer
builder.Services.AddVersionedApiExplorer(o =>
{
    o.GroupNameFormat = "'v'VVV";
    o.SubstituteApiVersionInUrl = true;
});

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
    // Build a list of API version descriptions, providing a URL-friendly name
    var provider = builder.Services.BuildServiceProvider().GetRequiredService<IApiVersionDescriptionProvider>();
    foreach (Description.ApiVersionDescription description in provider.ApiVersionDescriptions)
    {
        options.SwaggerDoc(description.GroupName, new OpenApiInfo
        {
            Title = "Sample API",
            Version = description.ApiVersion.ToString(),
            Description = "A simple ASP.NET Core Web API with versioning",
        });
    }

    // Define the Swagger generation options
    options.OperationFilter<SwaggerDefaultValues>();
});

var app = builder.Build();

// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
    app.UseDeveloperExceptionPage();
    app.UseSwagger();
    app.UseSwaggerUI(options =>
    {
        // Build a list of API version descriptions, providing a URL-friendly name
        var provider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
        foreach (Description.ApiVersionDescription description in provider.ApiVersionDescriptions)
        {
            options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json", description.GroupName.ToUpperInvariant());
        }
    });
}

// ...

app.Run();

7.2 Adding Swagger Default Values To ensure Swagger shows the correct parameters for different API versions, create a custom operation filter:

Create a new file SwaggerDefaultValues.cs:

using Microsoft.OpenApi.Models;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.Linq;

public class SwaggerDefaultValues : IOperationFilter
{
    private readonly IApiVersionDescriptionProvider _provider;

    public SwaggerDefaultValues(IApiVersionDescriptionProvider provider) => _provider = provider;

    public void Apply(OpenApiOperation operation, OperationFilterContext context)
    {
        if (operation.Parameters == null || operation.Parameters.Count == 0) return;

        // Remove version parameter from Swagger UI
        var apiVersionParameter = operation.Parameters.FirstOrDefault(p => p.Name == "api-version");
        if (apiVersionParameter != null)
        {
            operation.Parameters.Remove(apiVersionParameter);
        }

        var apiDescription = context.ApiDescription;
        operation.Description += $" (Api Version: {apiDescription.ApiVersion})";
        operation.Responses.Add("400", new OpenApiResponse { Description = "Bad Request" });
        operation.Responses.Add("401", new OpenApiResponse { Description = "Unauthorized" });

        var descriptionProvider = context.ApiDescription.ParameterDescriptions.First(p => p.Name == "api-version");
        if (descriptionProvider.RouteInfo != null)
        {
            operation.Parameters.Add(new OpenApiParameter
            {
                Name = "api-version",
                In = ParameterLocation.Path,
                Description = "API version number",
                Required = true,
                Schema = new OpenApiSchema { Type = "string", Default = new Microsoft.OpenApi.Any.OpenApiString(apiDescription.ApiVersion.ToString()) }
            });
        }

        var parameters = context.ApiDescription.ParameterDescriptions.Select(description =>
        {
            var parameter = new OpenApiParameter
            {
                Name = description.Name,
                In = ParameterLocation.Query,
                Description = description.ModelMetadata?.Description,
                Required = description.IsRequired || description.Name == "api-version",
                Schema = new OpenApiSchema { Type = description.ParameterType.Name }
            };

            if (parameter.Required && description.DefaultValue != null)
            {
                parameter.Default = new Microsoft.OpenApi.Any.OpenApiString(description.DefaultValue.ToString());
            }

            return parameter;
        });

        operation.Parameters.AddRange(parameters);

        var response = operation.Responses["200"];
        var apiVersion = apiDescription.ApiVersion.ToString();
        var responseDescription = response.Description.Replace("Api Version: ", $"Api Version: {apiVersion}");

        response.Description = responseDescription;

        var apiVersionParameterOpenApi = operation.Parameters.FirstOrDefault(p => p.Name == "api-version");
        if (apiVersionParameterOpenApi != null)
        {
            apiVersionParameterOpenApi.Example = new Microsoft.OpenApi.Any.OpenApiString(apiVersion);
        }
    }
}

Update Swagger configuration:

builder.Services.AddSwaggerGen(options =>
{
    // ...
    options.OperationFilter<SwaggerDefaultValues>();
});

7.3 Testing Swagger Documentation Run your application and navigate to the Swagger UI at https://localhost:5001/swagger. You should see separate documentation for each API version, allowing you to easily test and explore different endpoints.


Conclusion

API versioning is a crucial aspect of developing maintainable and scalable web APIs. ASP.NET Core provides robust support for implementing various versioning strategies, making it easier to manage changes without disrupting existing clients. By following the steps outlined in this guide, you should be well-equipped to version your web APIs effectively.

Remember to choose the versioning strategy that best fits your application's requirements and communicate version changes clearly to your API consumers. Happy coding!


Additional Resources

Feel free to explore these resources for more in-depth knowledge and advanced scenarios.