接口版本控制解决的是一个很现实的问题:当业务需求变化导致接口的请求参数或返回结构发生调整时,如何保证已经接入的老客户端不被破坏。.NET Web API提供了多种成熟的版本管理手段,从最直观的URL路径版本到相对隐蔽的请求头版本,各有适用场景。本文将结合代码示例,详细讲解如何在ASP.NET Core项目中落地接口版本控制,并分析不同方案的取舍。

一、为什么要做接口版本控制
想象这样一个场景:你的订单查询接口最初只返回订单号和金额,后来产品要求增加用户信息和物流状态。如果直接在原有接口上修改返回结构,老版本的App在反序列化时可能因为字段缺失或多出而报错,线上事故就此产生。版本控制的核心价值就是让新旧结构并存,客户端按自己的节奏升级。
另一个常见动机是迭代节奏的差异。服务端可能每周都在发版,而客户端尤其是移动端App,用户升级周期可能是几个月甚至更久。没有版本隔离,服务端每一次破坏性变更都是一场赌博。通过版本控制,服务端可以明确声明哪些版本继续可用、哪些版本进入废弃流程,把变更的风险控制在可预期的范围内。
一般来说,版本控制要解决三个问题:请求如何标识自己期望的版本、服务端如何路由到对应版本的实现、以及文档如何清晰地展示多个版本。下面逐一展开。
二、三种主流的版本标识方式
1. URL路径版本
这是最直观的方式,版本直接出现在路由中,例如api/v1/orders和api/v2/orders。优点是肉眼可见、调试方便、对CDN缓存和网关转发都友好,缺点是URL会随着版本增加而变长,且严格来说版本并非资源的一部分,放在URL里不够符合RESTful的纯粹理念。不过工程实践中这几乎不影响它的流行程度,大部分团队的首选仍是路径版本。
2. 查询字符串版本
通过?api-version=2.0这样的查询参数传递版本。URL资源路径保持稳定,版本只是请求的一个附加条件。这种方式实现简单,浏览器直接访问就能测试,但缺点是容易被缓存策略忽略,导致不同版本的响应被缓存混用,需要在缓存配置上格外留心。
3. 请求头版本
把版本放在自定义请求头里,比如X-Api-Version: 2.0。这种方式URL最干净,也最符合一些团队“版本不属于资源标识”的设计主张。缺点是调试时必须借助Postman之类的工具,浏览器直接访问无法指定版本,客户端接入成本略高。
三种方式没有绝对优劣,团队统一即可。下面看看在ASP.NET Core中如何用官方推荐的工具包实现它们。
三、使用Asp.Versioning包实现版本控制
目前在.NET 7及之后的项目中,推荐使用Asp.Versioning.Mvc这个包(老项目常用的Microsoft.AspNetCore.Mvc.Versioning已进入维护状态)。先通过NuGet安装:
dotnet add package Asp.Versioning.Mvc
然后在Program.cs中注册服务:
builder.Services.AddApiVersioning(options =>
{
// 默认版本,客户端不指定版本时使用
options.DefaultApiVersion = new ApiVersion(1, 0);
// 未指定版本时是否使用默认版本
options.AssumeDefaultVersionWhenUnspecified = true;
// 响应头中返回可用版本信息
options.ReportApiVersions = true;
})
.AddMvc()
.AddApiExplorer(options =>
{
// 格式化版本号,例如 v1、v2
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});接着定义两个版本的控制器。这里演示URL路径版本,通过[ApiVersion]标注每个控制器所属的版本,路由中使用{version:apiVersion}占位符:
[ApiController]
[Route("api/{version:apiVersion}/orders")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class OrdersController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public IActionResult GetV1()
{
return Ok(new { version = "1.0", data = "老结构" });
}
[HttpGet]
[MapToApiVersion("2.0")]
public IActionResult GetV2()
{
return Ok(new { version = "2.0", data = "新结构,包含扩展字段" });
}
}这样请求api/v1/orders会命中GetV1,请求api/v2/orders会命中GetV2,两个版本的实现可以放在同一个控制器里,也可以拆成OrdersV1Controller和OrdersV2Controller两个类,后者在逻辑差异较大时更清晰。
如果想改用查询字符串或请求头方式,只需修改注册时的读取策略:
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
// 优先从查询字符串读取 api-version 参数
options.ApiVersionReader = ApiVersionReader.Combine(
new QueryStringApiVersionReader("api-version"),
new HeaderApiVersionReader("X-Api-Version"));
});上面的配置用了ApiVersionReader.Combine,意味着查询字符串和请求头两种方式都支持,客户端用哪种都可以,这在灰度迁移阶段特别实用。
四、版本废弃与Swagger文档集成
版本控制不是一锤子买卖,老版本最终要下线。可以通过[Deprecated]标记废弃版本:
[ApiController]
[Route("api/{version:apiVersion}/orders")]
[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("2.0")]
public class OrdersController : ControllerBase
{
// 省略实现
}标记废弃后,配合ReportApiVersions = true,响应头中会出现api-deprecated-versions: 1.0这样的提示,客户端可以据此感知升级压力。建议的做法是:先标记废弃并观察一段时间的老版本流量,流量降到接近零之后再正式移除代码,避免一刀切下线造成线上故障。
Swagger文档方面,需要让Swashbuckle识别多版本分组。核心是自定义IOperationFilter去掉路由中的版本参数,并按版本生成多个文档:
builder.Services.AddSwaggerGen(options =>
{
foreach (var description in builder.Services
.BuildServiceProvider()
.GetRequiredService<IApiVersionDescriptionProvider>()
.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, new OpenApiInfo
{
Title = "订单服务",
Version = description.ApiVersion.ToString()
});
}
});这样在Swagger UI的右上角就能切换v1和v2两套文档,前后端联调时一目了然。另外如果版本差异只涉及个别字段,也可以考虑用查询参数做小幅度的兼容开关,只有结构性变更才动用大版本号升级,避免版本号膨胀过快。
总结一下:URL路径版本适合大多数对外API,查询字符串和请求头适合对URL整洁度有要求的场景;用Asp.Versioning包配置成本低、与框架集成度高;版本废弃要走标记、观察、移除三步。把这套机制搭好之后,接口的持续演进就有了制度保障,服务端发版再也不用提心吊胆。
.NET Web API接口版本控制ApiVersion修改时间:2026-09-07 09:10:39