Asp.Versioning 官方文档:
https://dotnet.github.io/aspnet-api-versioning
本文使用的是 Asp.Versioning.Mvc 6.4.1,对应 .NET 6 的稳定线。
先给结论:网上大量教程说"Swagger 多版本必须引 Asp.Versioning.Mvc.ApiExplorer"------这句话只在自动化分组路线 下成立,即依赖 ApiDescription.GroupName 或 IApiVersionDescriptionProvider 去枚举版本。本文走的是另一条 Swashbuckle 原生路线:DocInclusionPredicate 直接反射控制器上的 [ApiVersion] 完成归组,整条链路不经过 IApiExplorer 的版本增强层,所以那个包在这里是冗余的。
少一个依赖,筛选逻辑全部摊在明面上,版本清单只有一个来源。下面是完整代码。
装包:

一、Swagger 配置
cs
using Asp.Versioning;
using Microsoft.OpenApi.Any;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
namespace Extensions_Api.Configurations;
/// <summary>Swagger 配置。</summary>
public static class SwaggerConfigExtensions
{
/// <summary>注册 Swagger 的生成规则。</summary>
public static IServiceCollection AddSwaggerConfig(this IServiceCollection services, string[] apiVersions)
{
services.AddSwaggerGen(options =>
{
// 有 XML 注释文件时才加载,没有文件不影响启动。
var xmlFile = $"{System.Reflection.Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
if (File.Exists(xmlPath))
{
options.IncludeXmlComments(xmlPath, true);
}
// 给路径里的 version 参数填上默认值。
options.ParameterFilter<VersionDefaultValueFilter>();
// 每个版本单独生成一份 Swagger 文档。
foreach (var version in apiVersions)
{
options.SwaggerDoc(version, new OpenApiInfo
{
Title = $"API {version.ToUpper()}",
Version = version
});
}
// 同一路径有多个接口时取第一个,避免文档生成失败。
options.ResolveConflictingActions(api => api.First());
// 按控制器上的 ApiVersion 把接口分到对应版本文档。
options.DocInclusionPredicate((docName, apiDesc) =>
{
if (!apiDesc.TryGetMethodInfo(out var methodInfo))
{
return false;
}
var versions = methodInfo.DeclaringType?
.GetCustomAttributes(true)
.OfType<ApiVersionAttribute>()
.SelectMany(attr => attr.Versions)
.Select(version => $"v{version.MajorVersion}")
.ToList();
return versions?.Contains(docName) ?? false;
});
// Swagger 右上角显示 JWT 输入框,Token 放在 Authorization 请求头。
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Description = "JWT授权",
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer"
});
// 所有接口都带上 Bearer 认证,否则调试时不会提交 Token。
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
new List<string>()
}
});
});
return services;
}
/// <summary>启用 Swagger 和按版本切换的 Swagger UI。</summary>
public static WebApplication UseSwaggerConfig(this WebApplication app, string[] apiVersions)
{
// 只在开发环境开放 Swagger。
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
// 右上角按 v1、v2 切换文档。
app.UseSwaggerUI(options =>
{
foreach (var version in apiVersions)
{
options.SwaggerEndpoint($"/swagger/{version}/swagger.json", version);
}
});
}
return app;
}
}
/// <summary>Swagger 参数过滤器:为 API 版本号参数设置默认值。</summary>
public class VersionDefaultValueFilter : IParameterFilter
{
/// <summary>为路径中的 version 参数填充默认值。</summary>
public void Apply(OpenApiParameter parameter, ParameterFilterContext context)
{
if (parameter.Name == "version" && parameter.In == ParameterLocation.Path && parameter.Schema != null)
{
var version = context.DocumentName.StartsWith("v", StringComparison.OrdinalIgnoreCase)
? context.DocumentName[1..]
: context.DocumentName;
parameter.Schema.Default = new OpenApiString(version);
parameter.Example = new OpenApiString(version);
}
}
}
要点 :DocInclusionPredicate 四步走------拿 MethodInfo、反查 DeclaringType、收集控制器上的 ApiVersionAttribute、用 MajorVersion 拼成 v1 / v2 比对。不碰 IApiVersionDescriptionProvider,所以不需要 Asp.Versioning.Mvc.ApiExplorer。VersionDefaultValueFilter 从 DocumentName 反推版本号,同时填 Default 和 Example,Swagger UI 里 Try it out 时 {version} 已经预填好,不用手动敲。apiVersions 是唯一版本清单,SwaggerDoc 和 SwaggerEndpoint 都从它派生,不存在两处列表不同步的隐患。IsDevelopment() 收在扩展内,生产环境天然不暴露文档。
二、API 版本控制配置
cs
using Asp.Versioning;
namespace Extensions_Api.Configurations;
/// <summary>API 版本配置。</summary>
public static class ApiVersioningConfigExtensions
{
/// <summary>
/// 注册 API 版本规则。未传版本时默认使用 1.0,并在响应头中返回支持的版本。
/// </summary>
public static IServiceCollection AddApiVersioningConfig(this IServiceCollection services)
{
services.AddApiVersioning(options =>
{
// 请求未带版本号时使用的默认版本。
options.DefaultApiVersion = new ApiVersion(1, 0);
// 未传版本时按默认版本处理,避免直接返回 400。
options.AssumeDefaultVersionWhenUnspecified = true;
// 在响应头中返回当前接口支持的版本。
options.ReportApiVersions = true;
}).AddMvc();
return services;
}
}
要点 :DefaultApiVersion 配 AssumeDefaultVersionWhenUnspecified,不带版本的请求回退到 v1,而不是 400。ReportApiVersions 让响应头带上 api-supported-versions: 1.0, 2.0,调用方据此可以做灰度切换。末尾的 .AddMvc() 是整条链路的开关------这一句漏掉,前面所有配置全部失效,也是新手最常踩的坑。
三、Program.cs
cs
using Extensions_Api.Configurations;
// 创建应用构建器,读取配置、环境和依赖注入容器。
var builder = WebApplication.CreateBuilder(args);
// 当前开放的 API 版本,Swagger 按这些版本分别生成文档。
var apiVersions = new[] { "v1", "v2" };
// 注册控制器。
builder.Services.AddControllers();
// 注册接口发现,供 Swagger 读取路由和参数。
builder.Services.AddEndpointsApiExplorer();
// 注册 API 版本规则,未传版本时默认 1.0。
builder.Services.AddApiVersioningConfig();
// 按版本生成 Swagger 文档,并加上 JWT 输入框。
builder.Services.AddSwaggerConfig(apiVersions);
// 构建应用,之后开始配置请求管道。
var app = builder.Build();
// 开发环境启用 Swagger,右上角可切换 v1、v2。
app.UseSwaggerConfig(apiVersions);
// HTTP 请求重定向到 HTTPS。
app.UseHttpsRedirection();
// 启用授权中间件。
app.UseAuthorization();
// 把控制器路由映射到终结点。
app.MapControllers();
// 启动应用并开始监听请求。
app.Run();
要点 :apiVersions 是全链路唯一的版本来源------版本控制默认值、Swagger 文档生成、UI 下拉端点全部从这里派生。这不是省事,是消除不一致的根因 。AddEndpointsApiExplorer() 提供 ApiDescription 所需的 MethodInfo,是 DocInclusionPredicate 正常工作的前提,不能漏。
四、控制器(示例)
cs
// V1
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class WeatherForecastController : ControllerBase { /* 返回原始数组 */ }
// V2
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
public class WeatherForecastController : ControllerBase { /* 返回 WeatherForecastV2Result */ }
要点 :{version:apiVersion} 是路由约束,告诉路由系统这一段由版本控制逻辑解析,而不是当普通字符串。两个版本返回结构可以完全不同------V1 返回原始数组,V2 包一层 WeatherForecastV2Result 带 Version 字段。契约能独立演进而不炸老客户端,这才是多版本控制存在的意义 ,不然加个 v1 只是自欺欺人。
五、跑起来验证什么
访问 /swagger,右上角切版本。逐项验证:
-
切到 v1 只看到 v1 接口,切到 v2 只看到 v2 接口 ,且 v2 的响应 schema 是
WeatherForecastV2Result; -
点开接口时
version参数已自动预填,Try it out 不用手动敲; -
不带版本直接请求
/api/weatherforecast,回退到 v1,验证AssumeDefaultVersionWhenUnspecified生效; -
响应头带
api-supported-versions: 1.0, 2.0; -
把环境切到非 Development,Swagger 端点返回 404,验证环境隔离生效。



多版本控制的价值,从来不是路由里多一个 v1,而是它给"契约演进"提供了制度化的空间:老客户端继续走老契约,新客户端接入新契约,Swagger 文档如实反映当前版本的全貌,联调方不再靠猜。
这套方案的几个关键决策,值得带走:
| 决策点 | 选择 | 理由 |
|---|---|---|
| 版本控制库 | Asp.Versioning.Mvc 6.4.1 |
.NET 6 下的现行主线 |
| Swagger 分组 | DocInclusionPredicate 手动筛选 |
依赖面小、逻辑显式、与版本清单天然对齐 |
| 是否引入 ApiExplorer | 不需要 | 手动路线绕开 IApiExplorer 版本增强层 |
| 版本参数预填 | IParameterFilter |
对所有接口一次性生效,零侵入 |
| 版本来源 | 单一 apiVersions 数组 |
避免多处硬编码不同步 |
| Swagger 暴露 | 扩展内 IsDevelopment() 隔离 |
生产不暴露文档,职责内聚 |
如果后续版本粒度从 1.0、2.0 扩展到次版本,只需调整 apiVersions 数组与 Select 的拼接方式,整体结构无需重写。一个自洽的体系,应该在需求演进时保持骨架不动------这比"能跑"值钱得多。