C#  

Building a Versioned REST API in C# with Swagger

Introduction

As APIs grow, change is inevitable. New features get added, bugs get fixed, and sometimes entire models need to evolve. But if you're already serving clients—mobile apps, web frontends, third-party users—you can't just break things. That's where API versioning comes in.

In this guide, we’ll walk through how to build a versioned REST API using C# and ASP.NET Core, with full support for Swagger/OpenAPI documentation. You'll learn how to structure controllers by version, configure versioning in the pipeline, and make sure each version gets its own Swagger docs. It's a clean, scalable setup that’s easy to extend and safe for real-world use.

Step 1. Create a New Web API Project.

Create a new ASP.NET Core Web API project using the CLI or Visual Studio.

dotnet new webapi -n VersionedApiDemo

cd VersionedApiDemo

Optional. Clean up WeatherForecast.cs and its controller if you want a fresh start.

Step 2. Add Required NuGet Packages.

Install these NuGet packages.

dotnet add package Microsoft.AspNetCore.Mvc.Versioning
dotnet add package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
dotnet add package Swashbuckle.AspNetCore

These provide.

  • API versioning
  • Metadata for Swagger
  • Swagger UI

Step 3. Configure Versioning and Swagger.

Program.cs updated configuration

using Microsoft.AspNetCore.Mvc;
using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerGen;

var builder = WebApplication.CreateBuilder(args);

// Add versioning
builder.Services.AddApiVersioning(opt =>
{
    opt.DefaultApiVersion = new ApiVersion(1, 0);
    opt.AssumeDefaultVersionWhenUnspecified = true;
    opt.ReportApiVersions = true; // Adds headers like api-supported-versions
    opt.ApiVersionReader = new UrlSegmentApiVersionReader(); // /v{version}/controller
});

// Add versioned API explorer for Swagger
builder.Services.AddVersionedApiExplorer(opt =>
{
    opt.GroupNameFormat = "'v'VVV"; // e.g., v1, v2
    opt.SubstituteApiVersionInUrl = true;
});

// Add Swagger and bind it to the versioned explorer
builder.Services.AddTransient<IConfigureOptions<SwaggerGenOptions>, ConfigureSwaggerOptions>();
builder.Services.AddSwaggerGen();
builder.Services.AddControllers();

var app = builder.Build();

// Swagger UI with support for multiple versions
var apiVersionDescriptionProvider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
app.UseSwagger();
app.UseSwaggerUI(options =>
{
    foreach (var desc in apiVersionDescriptionProvider.ApiVersionDescriptions)
    {
        options.SwaggerEndpoint($"/swagger/{desc.GroupName}/swagger.json", desc.GroupName.ToUpperInvariant());
    }
});

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

ConfigureSwaggerOptions.cs enables versioned Swagger docs.

Create this new file in your project.

using Microsoft.AspNetCore.Mvc.ApiExplorer;
using Microsoft.Extensions.Options;
using Swashbuckle.AspNetCore.SwaggerGen;
using Microsoft.OpenApi.Models;

public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
    private readonly IApiVersionDescriptionProvider _provider;

    public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
    {
        _provider = provider;
    }

    public void Configure(SwaggerGenOptions options)
    {
        foreach (var desc in _provider.ApiVersionDescriptions)
        {
            options.SwaggerDoc(desc.GroupName, new OpenApiInfo
            {
                Title = "Versioned API Demo",
                Version = desc.ApiVersion.ToString(),
                Description = $"API Version {desc.ApiVersion}"
            });
        }
    }
}

This lets Swagger generate separate documentation per API version (e.g., /swagger/v1/swagger.json, /swagger/v2/swagger.json).

Step 4. Add Versioned Controllers.

Controllers/v1/ProductController.cs

using Microsoft.AspNetCore.Mvc;

namespace VersionedApiDemo.Controllers.v1
{
    [ApiController]
    [ApiVersion("1.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class ProductController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get() =>
            Ok(new { Version = "1.0", Product = "Coffee Mug" });
    }
}

Controllers/v2/ProductController.cs

using Microsoft.AspNetCore.Mvc;

namespace VersionedApiDemo.Controllers.v2
{
    [ApiController]
    [ApiVersion("2.0")]
    [Route("api/v{version:apiVersion}/[controller]")]
    public class ProductController : ControllerBase
    {
        [HttpGet]
        public IActionResult Get() =>
            Ok(new { Version = "2.0", Product = "Deluxe Coffee Mug", InStock = true });
    }
}

You can now access.

  • GET /api/v1/product
  • GET /api/v2/product

Each has its own logic, schema, and response.

How Versioning Works?

There are multiple strategies for API versioning in ASP.NET Core.

Strategy Description
URL segment /api/v1/product — clean and RESTful
Query string /api/product?api-version=1.0
Header api-version: 1.0

You can combine readers like this.

opt.ApiVersionReader = ApiVersionReader.Combine(
    new QueryStringApiVersionReader("api-version"),
    new HeaderApiVersionReader("x-api-version")
);

Try It Out

Run your app.

dotnet run

Visit: https://localhost:5001/swagger

You'll see separate Swagger docs for v1 and v2. You can test each version's endpoint right in the UI.

Best Practices

  • Keep old versions stable and backward compatible.
  • Use namespaces like Controllers.v1 to keep versions isolated.
  • Use [MapToApiVersion("x.y")] if you need multiple versions in one controller.
  • Use API versioning in Swagger for better docs and discoverability.
  • Use a deprecation plan when retiring older versions.

Summary

With ASP.NET Core + Swagger + API Versioning, you can,

  • Cleanly support multiple API versions
  • Document each version independently
  • Keep client integrations stable over time
  • Upgrade your services without breaking existing consumers

This setup is production-ready, scalable, and highly maintainable.

Conclusion

Versioning your API isn't just a nice-to-have—it’s a must when you're building systems that need to grow without breaking existing clients. With ASP.NET Core, adding version support is surprisingly straightforward, and with Swagger in the mix, you get clear, per-version documentation that makes life easier for both you and your API consumers.

By separating controllers by version, wiring up the right configuration, and exposing each version in Swagger, you’ve now got a setup that’s ready for production—and future-proofed for what’s ahead. Whether you're rolling out new features in v2 or maintaining legacy support in v1, this structure keeps your codebase clean and your API easy to work with.