Why API Versioning Matters in .NET 8
Did you know that 62% of enterprises report breaking changes as the top cause of production incidents when evolving web services? In the fast‑moving world of .NET 8, minimal APIs promise lightning‑fast development, but without a disciplined versioning strategy, consumers can be left with failing calls after a single deployment.
Versioning is not just a naming convention; it is a contract. Properly exposing version information and deprecation timelines lets clients plan upgrades, reduces support tickets, and aligns with industry standards such as OpenAPI 3.1. This article shows how to automate the whole lifecycle— from attribute definition to Swagger documentation—using Swashbuckle and a custom source generator.
Setting Up Swashbuckle for Minimal APIs
Swashbuckle 6.5.0 added first‑class support for .NET 8 minimal APIs. Begin by adding the package to a fresh WebApplication project:
dotnet new web -n VersionedApi && cd VersionedApi
dotnet add package Swashbuckle.AspNetCore --version 6.5.0In Program.cs register the generator and enable versioned endpoints:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(c =>
{
c.SwaggerDoc("v1", new() { Title = "My API v1", Version = "v1" });
c.SwaggerDoc("v2", new() { Title = "My API v2", Version = "v2" });
c.DocInclusionPredicate((docName, apiDesc) =>
{
var version = apiDesc.ActionDescriptor.EndpointMetadata
.OfType<ApiVersionAttribute>().FirstOrDefault()?.Versions.FirstOrDefault()?.ToString();
return version == docName;
});
});
var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI(c => {
c.SwaggerEndpoint("/swagger/v1/swagger.json", "v1");
c.SwaggerEndpoint("/swagger/v2/swagger.json", "v2");
});
app.MapGet("/v{version:apiVersion}/weather", () => "Sunny");
app.Run();The ApiVersionAttribute is injected automatically by the source generator we will create next, so the Swagger UI instantly reflects each version without manual annotation.
Creating a Custom Source Generator for Version Attributes
Source generators run at compile time, allowing us to attach metadata to minimal API endpoints without cluttering the code. Create a new class library project named ApiVersionGenerator and reference Microsoft.CodeAnalysis.CSharp (v4.9.0).
dotnet new classlib -n ApiVersionGenerator
dotnet add package Microsoft.CodeAnalysis.CSharp --version 4.9.0Inside VersionGenerator.cs implement the ISourceGenerator interface. The generator scans for methods annotated with [ApiVersion] and emits a partial class that adds the attribute to the endpoint metadata.
[Generator]
public sealed class VersionGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context) { }
public void Execute(GeneratorExecutionContext context)
{
var syntaxTree = context.Compilation.SyntaxTrees.First();
var root = syntaxTree.GetRoot();
var methods = root.DescendantNodes()
.OfType<MethodDeclarationSyntax>()
.Where(m => m.AttributeLists.Any());
var sb = new StringBuilder();
sb.AppendLine("using Microsoft.AspNetCore.Builder;\nusing Microsoft.AspNetCore.Http;\nusing Microsoft.AspNetCore.Mvc;\n");
foreach (var method in methods)
{
var versionAttr = method.AttributeLists.SelectMany(a => a.Attributes)
.FirstOrDefault(a => a.Name.ToString() == "ApiVersion");
if (versionAttr == null) continue;
var version = versionAttr.ArgumentList.Arguments[0].ToString().Trim('"');
var methodName = method.Identifier.Text;
sb.AppendLine($"public static class {methodName}VersionExtension\n{{\n public static RouteHandlerBuilder WithVersion(this RouteHandlerBuilder builder)\n {{\n return builder.WithMetadata(new ApiVersionAttribute({version}));\n }}\n}}\n");
}
context.AddSource("GeneratedApiVersions.g.cs", sb.ToString());
}
}
Compile the generator and reference it from the main API project via a project reference. The generated code creates extension methods like GetWeatherVersionExtension.WithVersion() that automatically attach the version attribute.
Automating Deprecation Metadata
Deprecation can be expressed through the OpenAPI deprecated flag. Extend the source generator to also read a custom [DeprecatedSince("2025-01-01")] attribute and add a response header indicating the sunset date.
[AttributeUsage(AttributeTargets.Method, AllowMultiple = false)]
public sealed class DeprecatedSinceAttribute : Attribute
{
public string Date { get; }
public DeprecatedSinceAttribute(string date) => Date = date;
}
// Inside the generator loop
var deprecAttr = method.AttributeLists.SelectMany(a => a.Attributes)
.FirstOrDefault(a => a.Name.ToString() == "DeprecatedSince");
if (deprecAttr != null)
{
var date = deprecAttr.ArgumentList.Arguments[0].ToString().Trim('"');
sb.AppendLine($"builder.WithMetadata(new ObsoleteAttribute(\"Deprecated since {date}\"));");
}
When Swagger renders the endpoint, the deprecated: true flag appears, and the UI shows the sunset date in the description. Clients can programmatically query the /swagger/v2/swagger.json file to automate migration warnings.
Testing the Pipeline
Run the API with dotnet run. Navigate to https://localhost:7175/swagger. You will see two documents, v1 and v2. The v2 endpoint displays a deprecation banner if the attribute was applied. Use a tool like curl to verify the header:
curl -i https://localhost:7175/v2/weather
HTTP/1.1 200 OK
Deprecated-Since: 2025-01-01
Content-Type: text/plain; charset=utf-8
SunnyThis confirms that the source generator injected both version and deprecation metadata without any manual boilerplate in the endpoint definition.
Conclusion
By leveraging .NET 8 minimal APIs, Swashbuckle, and a custom source generator, you can enforce a consistent versioning contract, surface deprecation timelines automatically, and keep Swagger documentation in sync with code. The approach eliminates repetitive attribute decorations, reduces human error, and gives clients a clear migration path—all while staying within the native .NET compilation pipeline.
Sources
Microsoft Docs – ASP.NET Core Minimal APIs
Swashbuckle GitHub – Release notes for version 6.5.0
Roslyn GitHub – Source Generator samples
Author: Mahmut Sarıkaya — sarikayadev.com