API 版本控制
大约 13 分钟约 3855 字
API 版本控制
简介
API 版本控制是管理 API 变更的策略,确保客户端在 API 演进过程中不受破坏性变更(Breaking Changes)影响。ASP.NET Core 通过 Microsoft.AspNetCore.Mvc.Versioning 包提供多种版本控制方案,包括 URL 路径版本、请求头版本、查询参数版本和媒体类型版本。
为什么需要 API 版本控制
在 API 的生命周期中,以下场景会导致破坏性变更:
V1: { "name": "iPhone", "price": 999 }
V2: { "productName": "iPhone", "unitPrice": 999, "currency": "CNY" } // 字段重命名
V3: { "product": { "name": "iPhone" }, "pricing": { "amount": 999 } } // 结构重构
没有版本控制 -> 旧客户端解析失败 -> 生产事故
有版本控制 -> 旧客户端继续使用 V1 -> 新客户端使用 V2 -> 平滑过渡API 版本生命周期
版本发布 -> 稳定运行 -> 标记废弃 -> 停止新接入 -> 正式下线
| | | | |
v v v v v
v1.0 v1.0 v1.0(Dep) v1.0(只读) 返回 410
v2.0 v2.0 v2.0 v2.0 v2.0
v3.0 v3.0 v3.0 v3.0特点
基本配置
安装和配置
// ============================================
// NuGet 包安装
// ============================================
// dotnet add package Microsoft.AspNetCore.Mvc.Versioning
// dotnet add package Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer
var builder = WebApplication.CreateBuilder(args);
// ============================================
// 方式 A:基本配置(简单场景)
// ============================================
builder.Services.AddApiVersioning(options =>
{
// 默认版本(客户端未指定版本时使用)
options.DefaultApiVersion = new ApiVersion(1, 0);
// 客户端未指定版本时是否使用默认版本
// true = 自动使用默认版本,false = 返回 400
options.AssumeDefaultVersionWhenUnspecified = true;
// 在响应头中报告支持的版本和废弃的版本
options.ReportApiVersions = true;
// 响应头名称自定义(可选)
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new HeaderApiVersionReader("api-version", "x-api-version"),
new QueryStringApiVersionReader("api-version"),
new MediaTypeApiVersionReader("v")
);
});
// ============================================
// 方式 B:完整配置(生产场景)
// ============================================
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(2, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.SuppressApiVersionHeader = false; // 显示 api-supported-versions 头
// 多种版本读取方式(按优先级排序)
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(), // 1. URL 路径
new QueryStringApiVersionReader("ver"), // 2. 查询参数
new HeaderApiVersionReader("X-API-Version") // 3. 请求头
);
// 错误响应配置
options.ErrorResponses = new ApiVersionErrorResponseOptions
{
// 自定义不支持的版本错误响应
ResponseBuilder = context =>
{
return new ObjectResult(new
{
Error = "Unsupported API version",
SupportedVersions = context.SupportedApiVersions.Select(v => $"v{v}"),
DeprecatedVersions = context.DeprecatedApiVersions.Select(v => $"v{v}"),
Message = $"请使用以下版本之一: {string.Join(", ",
context.SupportedApiVersions.Select(v => $"v{v}"))}"
})
{
StatusCode = StatusCodes.Status400BadRequest
};
}
};
});
// ============================================
// 配置 API Explorer(Swagger 支持)
// ============================================
builder.Services.AddApiExplorer(options =>
{
// 分组名称格式:'v1', 'v2'
options.GroupNameFormat = "'v'VVV";
// 替换 URL 中的版本占位符
options.SubstituteApiVersionInUrl = true;
});
var app = builder.Build();版本化控制器
// ============================================
// V1 版本 — 简单的产品接口
// ============================================
[ApiController]
[ApiVersion("1.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
public class ProductsV1Controller : ControllerBase
{
/// <summary>
/// 获取所有产品(V1 格式:扁平结构)
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(ProductListV1), StatusCodes.Status200OK)]
public IActionResult GetAll()
{
var result = new ProductListV1
{
Data = new List<ProductV1>
{
new() { Id = 1, Name = "iPhone", Price = 999.0m }
}
};
return Ok(result);
}
/// <summary>
/// 获取产品详情(V1 格式)
/// </summary>
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(ProductV1), StatusCodes.Status200OK)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public IActionResult GetById(int id)
{
var product = new ProductV1 { Id = id, Name = "iPhone", Price = 999.0m };
return Ok(product);
}
}
// V1 数据模型
public class ProductV1
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public decimal Price { get; set; }
}
public class ProductListV1
{
public List<ProductV1> Data { get; set; } = new();
}
// ============================================
// V2 版本 — 增强版:分类、分页、多币种
// ============================================
[ApiController]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/[controller]")]
[Produces("application/json")]
public class ProductsV2Controller : ControllerBase
{
/// <summary>
/// 获取所有产品(V2 格式:分页 + 分类 + 元信息)
/// </summary>
[HttpGet]
[ProducesResponseType(typeof(PagedResult<ProductV2>), StatusCodes.Status200OK)]
public IActionResult GetAll(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? category = null)
{
var result = new PagedResult<ProductV2>
{
Items = new List<ProductV2>
{
new()
{
Id = 1,
ProductName = "iPhone 15",
UnitPrice = 999.0m,
Currency = "CNY",
Category = "Electronics",
Tags = new[] { "phone", "apple" }
}
},
TotalCount = 1,
Page = page,
PageSize = pageSize
};
return Ok(result);
}
/// <summary>
/// 创建产品(V2 新增)
/// </summary>
[HttpPost]
[ProducesResponseType(typeof(ProductV2), StatusCodes.Status201Created)]
public IActionResult Create([FromBody] CreateProductV2 request)
{
var product = new ProductV2
{
Id = 1,
ProductName = request.ProductName,
UnitPrice = request.UnitPrice,
Currency = request.Currency ?? "CNY",
Category = request.Category ?? "Uncategorized"
};
return CreatedAtAction(nameof(GetById), new { id = product.Id }, product);
}
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(ProductV2), StatusCodes.Status200OK)]
public IActionResult GetById(int id)
{
var product = new ProductV2
{
Id = id,
ProductName = "iPhone 15",
UnitPrice = 999.0m,
Currency = "CNY",
Category = "Electronics"
};
return Ok(product);
}
}
// V2 数据模型
public class ProductV2
{
public int Id { get; set; }
public string ProductName { get; set; } = string.Empty;
public decimal UnitPrice { get; set; }
public string Currency { get; set; } = "CNY";
public string Category { get; set; } = string.Empty;
public string[] Tags { get; set; } = Array.Empty<string>();
}
public class CreateProductV2
{
[Required]
public string ProductName { get; set; } = string.Empty;
[Range(0.01, double.MaxValue)]
public decimal UnitPrice { get; set; }
public string? Currency { get; set; }
public string? Category { get; set; }
}
public class PagedResult<T>
{
public List<T> Items { get; set; } = new();
public int TotalCount { get; set; }
public int Page { get; set; }
public int PageSize { get; set; }
public int TotalPages => (int)Math.Ceiling(TotalCount / (double)PageSize);
public bool HasNext => Page < TotalPages;
public bool HasPrevious => Page > 1;
}单控制器多版本
[ApiController]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
[Route("api/v{version:apiVersion}/orders")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
public OrdersController(IOrderService orderService)
{
_orderService = orderService;
}
/// <summary>
/// 获取订单详情(V1 和 V2 共用)
/// </summary>
[HttpGet("{id:int}")]
[MapToApiVersion("1.0")]
[MapToApiVersion("2.0")]
public async Task<IActionResult> GetById(int id)
{
var order = await _orderService.GetByIdAsync(id);
if (order == null) return NotFound();
return Ok(order);
}
/// <summary>
/// 获取订单列表 — V1 格式(无分页)
/// </summary>
[HttpGet]
[MapToApiVersion("1.0")]
public async Task<IActionResult> GetAllV1()
{
var orders = await _orderService.GetAllAsync();
return Ok(new { Version = "1.0", Data = orders });
}
/// <summary>
/// 获取订单列表 — V2 格式(分页 + 筛选)
/// </summary>
[HttpGet]
[MapToApiVersion("2.0")]
public async Task<IActionResult> GetAllV2(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
[FromQuery] OrderStatus? status = null)
{
var result = await _orderService.GetPagedAsync(page, pageSize, status);
return Ok(new { Version = "2.0", ...result });
}
/// <summary>
/// 取消订单 — V2 新增接口
/// </summary>
[HttpPost("{id:int}/cancel")]
[MapToApiVersion("2.0")]
public async Task<IActionResult> Cancel(int id, [FromBody] CancelOrderRequest request)
{
await _orderService.CancelAsync(id, request.Reason);
return Ok(new { Id = id, Status = "cancelled" });
}
/// <summary>
/// 导出订单 — V2 新增接口
/// </summary>
[HttpGet("export")]
[MapToApiVersion("2.0")]
public async Task<IActionResult> Export([FromQuery] DateTime? startDate = null)
{
var bytes = await _orderService.ExportAsync(startDate);
return File(bytes, "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"orders.xlsx");
}
}Minimal API 版本控制
// ============================================
// Minimal API 中的版本控制
// ============================================
var versionSet = app.NewApiVersionSet()
.HasApiVersion(1.0)
.HasApiVersion(2.0)
.ReportApiVersions()
.Build();
// V1 端点
app.MapGet("/api/v{version:apiVersion}/products", (
[AsParameters] ProductQueryV1 query,
IProductService service) =>
{
return service.GetAll(query);
})
.WithApiVersionSet(versionSet)
.MapToApiVersion(1.0);
// V2 端点(分页增强)
app.MapGet("/api/v{version:apiVersion}/products", (
[AsParameters] ProductQueryV2 query,
IProductService service) =>
{
return service.GetPaged(query);
})
.WithApiVersionSet(versionSet)
.MapToApiVersion(2.0);
// 两个版本共用的端点
app.MapGet("/api/v{version:apiVersion}/products/{id:int}",
(int id, IProductService service) =>
{
return service.GetById(id) is { } product
? Results.Ok(product)
: Results.NotFound();
})
.WithApiVersionSet(versionSet)
.MapToApiVersion(1.0, 2.0);版本废弃
[ApiController]
[ApiVersion("1.0", Deprecated = true)] // 标记 V1 为废弃
[ApiVersion("2.0")]
[ApiVersion("3.0")]
[Route("api/v{version:apiVersion}/users")]
public class UsersController : ControllerBase
{
/// <summary>
/// V1 — 已废弃,将在 2026-06-01 下线
/// </summary>
[HttpGet]
[MapToApiVersion("1.0")]
[Obsolete("请使用 V2 或 V3 版本,V1 将于 2026-06-01 正式下线")]
public IActionResult GetAllV1()
{
Response.Headers.Append("Sunset", "Sat, 01 Jun 2026 00:00:00 GMT");
Response.Headers.Append("Deprecation", "true");
Response.Headers.Append("Link",
"</api/v2/users>; rel=\"successor-version\"");
return Ok("V1 - 已废弃");
}
/// <summary>
/// V2 — 当前稳定版本
/// </summary>
[HttpGet]
[MapToApiVersion("2.0")]
public IActionResult GetAllV2()
{
return Ok("V2 - 当前版本");
}
/// <summary>
/// V3 — 最新版本
/// </summary>
[HttpGet]
[MapToApiVersion("3.0")]
public IActionResult GetAllV3()
{
return Ok("V3 - 最新版本");
}
}
// 废弃版本的响应头:
// api-supported-versions: 1.0, 2.0, 3.0
// api-deprecated-versions: 1.0
// Sunset: Sat, 01 Jun 2026 00:00:00 GMT
// Deprecation: true
// Link: </api/v2/users>; rel="successor-version"版本约束(Conventions)
// 通过 Convention 限制特定版本的行为
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
// V1 只允许 GET 和 POST
options.Conventions.Controller<ProductsV1Controller>()
.HasApiVersion(1, 0)
.Action(c => c.GetById(default!)).HasApiVersion(1, 0)
.Action(c => c.GetAll()).HasApiVersion(1, 0)
.Action(c => c.Create(default!)).HasApiVersion(1, 0);
});
// 通过 AllowMultipleVersions 允许未标注版本的方法匹配所有版本
options.Conventions.Controller<SharedController>()
.AllowMultipleVersions();自定义版本适配器
/// <summary>
/// 自定义版本读取器 — 从 JWT Token 中提取 API 版本
/// </summary>
public class JwtApiVersionReader : IApiVersionReader
{
public string ParameterName { get; }
public JwtApiVersionReader(string parameterName = "api-version")
{
ParameterName = parameterName;
}
public string? Read(HttpRequest request)
{
// 从 Authorization 头的 JWT Token 中提取版本
var authHeader = request.Headers.Authorization.ToString();
if (string.IsNullOrEmpty(authHeader) || !authHeader.StartsWith("Bearer "))
return null;
var token = authHeader.Substring("Bearer ".Length);
// 解析 JWT 获取 api-version claim
// 实际项目中应使用 JwtSecurityTokenHandler
var claims = ParseJwtClaims(token);
return claims.TryGetValue(ParameterName, out var version) ? version : null;
}
private static Dictionary<string, string> ParseJwtClaims(string token)
{
// 简化实现,实际应使用 System.IdentityModel.Tokens.Jwt
return new Dictionary<string, string>();
}
}Swagger 集成
// ============================================
// Swagger 配置 — 为每个 API 版本生成独立文档
// ============================================
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();
// 配置 Swagger 版本选项
builder.Services.ConfigureOptions<ConfigureSwaggerOptions>();
// ConfigureSwaggerOptions.cs
public class ConfigureSwaggerOptions : IConfigureOptions<SwaggerGenOptions>
{
private readonly IApiVersionDescriptionProvider _provider;
public ConfigureSwaggerOptions(IApiVersionDescriptionProvider provider)
{
_provider = provider;
}
public void Configure(SwaggerGenOptions options)
{
foreach (var description in _provider.ApiVersionDescriptions)
{
options.SwaggerDoc(description.GroupName, new OpenApiInfo
{
Title = $"My API",
Version = description.ApiVersion.ToString(),
Description = GetDescription(description),
Contact = new OpenApiContact
{
Name = "API Team",
Email = "api@example.com",
Url = new Uri("https://api.example.com")
},
License = new OpenApiLicense
{
Name = "MIT",
Url = new Uri("https://opensource.org/licenses/MIT")
}
});
}
}
private static string GetDescription(ApiVersionDescription description)
{
if (description.IsDeprecated)
{
return $"**此 API 版本已废弃。** 请迁移到 V{description.ApiVersion.MajorVersion + 1}。";
}
return $"API V{description.ApiVersion} — 当前稳定版本";
}
}
// Program.cs — 配置 Swagger UI
app.UseSwagger();
app.UseSwaggerUI(options =>
{
var descriptions = app.DescribeApiVersions();
// 为每个版本添加 Swagger 端点
foreach (var desc in descriptions)
{
var url = $"/swagger/{desc.GroupName}/swagger.json";
var name = desc.IsDeprecated
? $"{desc.GroupName} (已废弃)"
: desc.GroupName;
options.SwaggerEndpoint(url, name);
}
// 默认展开所有操作
options.DocExpansion(DocExpansion.List);
});中间件版本路由
/// <summary>
/// 版本检测中间件 — 记录 API 版本使用情况
/// </summary>
public class ApiVersionLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ApiVersionLoggingMiddleware> _logger;
public ApiVersionLoggingMiddleware(
RequestDelegate next,
ILogger<ApiVersionLoggingMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
// 记录请求的 API 版本
var apiVersion = context.GetRequestedApiVersion();
_logger.LogInformation(
"请求路径: {Path}, API版本: {ApiVersion}",
context.Request.Path, apiVersion);
await _next(context);
}
}
// 注册中间件
app.UseMiddleware<ApiVersionLoggingMiddleware>();版本策略对比
| 策略 | URL 示例 | 优点 | 缺点 | 推荐度 |
|---|---|---|---|---|
| URL 路径 | /api/v1/users | 清晰直观,易缓存 | 路由变更 | 推荐 |
| Query 参数 | /api/users?api-version=1.0 | 简单,无需改路由 | URL 不整洁 | 一般 |
| Header | api-version: 1.0 | URL 不变 | 不易测试/调试 | 特定场景 |
| Media Type | Accept: application/vnd.myapi.v2+json | 最 RESTful | 复杂,学习成本高 | 高级 |
版本策略选择指南
选择决策树:
1. 是否是公开 API(面向第三方)?
是 -> 使用 URL 路径版本(最清晰)
否 -> 继续
2. 客户端是否能控制请求头?
能 -> 使用 Header 版本(URL 更干净)
不能 -> 使用 URL 路径版本
3. 是否需要极致的 RESTful?
是 -> 使用 Media Type 版本
否 -> 使用 URL 路径版本
推荐默认方案:URL 路径版本 (/api/v{version}/resource)版本迁移策略
数据层版本适配
/// <summary>
/// 数据层版本适配器 — 同一数据库,不同版本返回不同格式
/// </summary>
public interface IProductResponseAdapter
{
object Adapt(ProductEntity entity);
object Adapt(IEnumerable<ProductEntity> entities);
}
public class ProductV1Adapter : IProductResponseAdapter
{
public object Adapt(ProductEntity entity) => new
{
entity.Id,
entity.Name,
entity.Price
};
public object Adapt(IEnumerable<ProductEntity> entities) => new
{
Data = entities.Select(e => Adapt(e))
};
}
public class ProductV2Adapter : IProductResponseAdapter
{
public object Adapt(ProductEntity entity) => new
{
entity.Id,
ProductName = entity.Name,
UnitPrice = entity.Price,
Currency = entity.Currency,
entity.Category,
Tags = entity.Tags?.Split(',', StringSplitOptions.RemoveEmptyEntries)
?? Array.Empty<string>()
};
public object Adapt(IEnumerable<ProductEntity> entities) => new
{
Items = entities.Select(e => Adapt(e)),
TotalCount = entities.Count()
};
}
// 在服务中使用
public class ProductService
{
private readonly AppDbContext _context;
private readonly IHttpContextAccessor _httpContextAccessor;
public ProductService(AppDbContext context, IHttpContextAccessor httpContextAccessor)
{
_context = context;
_httpContextAccessor = httpContextAccessor;
}
public async Task<object> GetAllAsync()
{
var products = await _context.Products.ToListAsync();
var version = _httpContextAccessor.HttpContext?.GetRequestedApiVersion();
IProductResponseAdapter adapter = version?.MajorVersion >= 2
? new ProductV2Adapter()
: new ProductV1Adapter();
return adapter.Adapt(products);
}
}版本弃用通知服务
/// <summary>
/// API 版本弃用通知服务
/// </summary>
public class ApiDeprecationService
{
private readonly ILogger<ApiDeprecationService> _logger;
private readonly TimeSpan _notifyBeforeShutdown = TimeSpan.FromDays(90);
public ApiDeprecationService(ILogger<ApiDeprecationService> logger)
{
_logger = logger;
}
public DeprecationInfo GetDeprecationInfo(string version)
{
return version switch
{
"1.0" => new DeprecationInfo
{
Version = "1.0",
IsDeprecated = true,
ShutdownDate = new DateTime(2026, 6, 1),
MigrationGuide = "https://docs.example.com/migration/v1-to-v2",
ReplacementVersion = "2.0",
Contact = "api-migration@example.com"
},
_ => new DeprecationInfo
{
Version = version,
IsDeprecated = false
}
};
}
}
public class DeprecationInfo
{
public string Version { get; set; } = string.Empty;
public bool IsDeprecated { get; set; }
public DateTime? ShutdownDate { get; set; }
public string? MigrationGuide { get; set; }
public string? ReplacementVersion { get; set; }
public string? Contact { get; set; }
}优点
缺点
总结
API 版本控制推荐使用 URL 路径方式(/api/v{version}/resource),最直观易懂。通过 Microsoft.AspNetCore.Mvc.Versioning 配置多种版本策略。单控制器用 MapToApiVersion 区分版本方法。废弃版本标记 Deprecated = true,配合 Sunset 和 Deprecation 响应头引导客户端升级。Swagger 集成为每个版本生成独立文档。建议最多同时维护 2-3 个版本,制定明确的废弃和下线策略(至少提前 90 天通知)。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
- 版本控制是 API 生命周期管理的核心能力,需要与发布流程联动。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
- 为每个版本定义明确的生命周期:发布、稳定、废弃、下线。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
- 版本号不遵循语义化版本规范,随意递增。
- 没有版本废弃计划,旧版本无限期保留。
- 不同版本的响应模型差异过大,增加适配器复杂度。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
- 研究自动化的 API 变更检测和兼容性验证工具。
适用场景
- 当你准备把《API 版本控制》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
- 制定版本管理规范:语义化版本号、废弃通知期、下线流程。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
- 检查版本路由是否正确匹配,查看响应头中的
api-supported-versions。 - 检查
ApiVersionReader配置是否包含所需的版本读取方式。
复盘问题
- 如果把《API 版本控制》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《API 版本控制》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《API 版本控制》最大的收益和代价分别是什么?
