API 版本控制策略
大约 11 分钟约 3188 字
API 版本控制策略
简介
API 版本控制是 API 生命周期管理的重要组成部分。理解 URL 版本、Header 版本、媒体类型版本的实现方式,以及版本迁移和废弃策略,有助于设计向后兼容且易于演进的 API。
特点
版本控制方式
三种版本策略
// 方式 1: URL 版本(最常用,最直观)
// GET /api/v1/users
// GET /api/v2/users
// 方式 2: Header 版本
// GET /api/users
// Header: X-API-Version: 2.0
// 或
// Header: Accept: application/vnd.myapi.v2+json
// 方式 3: Query String 版本
// GET /api/users?api-version=2.0
// 安装
// dotnet add package Asp.Versioning.Http
// dotnet add package Asp.Versioning.Mvc
// dotnet add package Asp.Versioning.ApiExplorer
// 基础配置
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true; // 在响应头中报告支持的版本
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(), // URL 路径
new HeaderApiVersionReader("X-API-Version"), // 自定义 Header
new QueryStringApiVersionReader("api-version"), // Query String
new MediaTypeApiVersionReader("v") // Accept Header
);
})
.AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
// 版本路由配置
builder.Services.AddEndpointsApiExplorer();
var app = builder.Build();
app.MapControllers();Minimal API 版本控制
// Minimal API 版本控制
var versionSet = app.NewApiVersionSet("Users")
.HasApiVersion(1.0)
.HasApiVersion(2.0)
.ReportApiVersions()
.Build();
// V1 端点
var usersV1 = app.MapGroup("/api/v{version:apiVersion}/users")
.WithApiVersionSet(versionSet);
usersV1.MapGet("/", (AppDbContext db) =>
db.Users.Select(u => new UserV1Dto(u.Id, u.Name, u.Email)))
.HasApiVersion(1.0)
.WithOpenApi(op =>
{
op.OperationId = "GetUsersV1";
op.Summary = "获取用户列表 (V1)";
return op;
});
usersV1.MapGet("/{id}", (int id, AppDbContext db) =>
{
var user = db.Users.Find(id);
return user == null ? Results.NotFound() : Results.Ok(new UserV1Dto(user.Id, user.Name, user.Email));
})
.HasApiVersion(1.0);
// V2 端点(返回更多字段)
var usersV2 = app.MapGroup("/api/v{version:apiVersion}/users")
.WithApiVersionSet(versionSet);
usersV2.MapGet("/", (AppDbContext db) =>
db.Users.Select(u => new UserV2Dto(u.Id, u.Name, u.Email, u.Avatar, u.Bio, u.CreatedAt)))
.HasApiVersion(2.0)
.WithOpenApi(op =>
{
op.OperationId = "GetUsersV2";
op.Summary = "获取用户列表 (V2,包含更多信息)";
return op;
});
usersV2.MapGet("/{id}", (int id, AppDbContext db) =>
{
var user = db.Users.Find(id);
return user == null ? Results.NotFound() : Results.Ok(new UserV2Dto(user.Id, user.Name, user.Email, user.Avatar, user.Bio, user.CreatedAt));
})
.HasApiVersion(2.0);
// 版本间共享端点
app.MapGet("/api/v{version:apiVersion}/users/{id}/avatar", (int id) =>
{
return Results.File(new byte[0], "image/png");
})
.HasApiVersion(1.0)
.HasApiVersion(2.0)
.WithApiVersionSet(versionSet);
// DTO 版本
public record UserV1Dto(int Id, string Name, string Email);
public record UserV2Dto(int Id, string Name, string Email, string? Avatar, string? Bio, DateTime CreatedAt);Controller 版本控制
// 方式 1: 特性标注(推荐)
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class UsersController : ControllerBase
{
// V1 和 V2 共享的端点
[HttpGet("{id}")]
public IActionResult GetUser(int id) => Ok(new { Id = id });
// V1 特有
[HttpGet]
[MapToApiVersion("1.0")]
public IActionResult GetUsersV1() => Ok("V1 Users");
// V2 特有
[HttpGet]
[MapToApiVersion("2.0")]
public IActionResult GetUsersV2() => Ok("V2 Users with more fields");
// V2 新增端点
[HttpGet("{id}/preferences")]
[MapToApiVersion("2.0")]
public IActionResult GetUserPreferences(int id) => Ok(new { Theme = "dark" });
}
// 方式 2: 独立 Controller(推荐用于差异大的版本)
[ApiController]
[Route("api/v{version:apiVersion}/users")]
[ApiVersion("1.0")]
public class UsersV1Controller : ControllerBase
{
[HttpGet]
public IActionResult GetUsers() => Ok("V1 Users");
}
[ApiController]
[Route("api/v{version:apiVersion}/users")]
[ApiVersion("2.0")]
public class UsersV2Controller : ControllerBase
{
[HttpGet]
public IActionResult GetUsers() => Ok("V2 Users");
[HttpGet("search")]
public IActionResult SearchUsers(string q) => Ok($"Search: {q}");
}版本迁移策略
向后兼容设计
// 原则:新增字段不破坏旧版本
// 1. 新增字段使用默认值
// 2. 旧字段保留(可标记 Obsolete)
// 3. 使用 DTO 分离版本
// 兼容的变更(不需要新版本):
// ✅ 新增可选字段
// ✅ 新增端点
// ✅ 放宽验证规则
// 不兼容的变更(需要新版本):
// ❌ 删除或重命名字段
// ❌ 修改字段类型
// ❌ 收紧验证规则
// ❌ 删除端点
// V1 DTO
public class CreateUserRequestV1
{
[Required] public string Name { get; set; } = "";
[Required][EmailAddress] public string Email { get; set; } = "";
}
// V2 DTO(新增字段,旧字段保留)
public class CreateUserRequestV2
{
[Required] public string Name { get; set; } = "";
[Required][EmailAddress] public string Email { get; set; } = ""
[Phone] public string? Phone { get; set; } // 新增可选
public string? Avatar { get; set; } // 新增可选
public List<string>? Tags { get; set; } // 新增可选
}
// 版本适配器
public class UserRequestAdapter
{
public CreateUserRequestV2 Adapt(CreateUserRequestV1 v1)
{
return new CreateUserRequestV2
{
Name = v1.Name,
Email = v1.Email,
Phone = null,
Avatar = null,
Tags = null
};
}
}废弃版本管理
// Sunset Header — 通知客户端版本即将废弃
public class DeprecatedApiMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<DeprecatedApiMiddleware> _logger;
public DeprecatedApiMiddleware(RequestDelegate next, ILogger<DeprecatedApiMiddleware> logger)
{
_next = next;
_logger = logger;
}
public async Task InvokeAsync(HttpContext context)
{
await _next(context);
// 检查请求的 API 版本
var versionFeature = context.Features.Get<IApiVersionFeature>();
if (versionFeature?.RequestedApiVersion != null)
{
var version = versionFeature.RequestedApiVersion;
// V1 版本已废弃
if (version.MajorGroup == 1)
{
context.Response.Headers.Append("Sunset",
"Sat, 01 Jun 2025 00:00:00 GMT"); // 废弃日期
context.Response.Headers.Append("Link",
"</api/v2/users>; rel=\"successor-version\""); // 新版本链接
context.Response.Headers.Append("Deprecation", "true");
_logger.LogWarning("请求使用了已废弃的 API 版本: {Version}", version);
}
}
}
}
app.UseMiddleware<DeprecatedApiMiddleware>();
// 废弃时间线:
// 1. 发布 V2 → V1 添加 Sunset Header
// 2. 过渡期 6 个月 → 客户端迁移到 V2
// 3. 到期 → V1 返回 410 Gone
// 4. 清理 → 移除 V1 代码Swagger 多版本集成
多版本文档
// Swagger 多版本配置
builder.Services.AddSwaggerGen(options =>
{
options.CustomOperationIds(desc =>
$"{desc.HttpMethod}_{desc.RelativePath?.Replace("/", "_")}");
});
var app = builder.Build();
// 动态生成多版本 Swagger 文档
var apiVersionDescriptionProvider = app.Services.GetRequiredService<IApiVersionDescriptionProvider>();
app.UseSwagger(options =>
{
options.RouteTemplate = "swagger/{documentName}/swagger.json";
});
app.UseSwaggerUI(options =>
{
foreach (var description in apiVersionDescriptionProvider.ApiVersionDescriptions)
{
options.SwaggerEndpoint($"/swagger/{description.GroupName}/swagger.json",
$"API {description.GroupName}");
}
options.RoutePrefix = "swagger";
});
// 自定义文档描述
builder.Services.ConfigureOptions<ConfigureSwaggerOptions>();
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 {description.ApiVersion}",
Version = description.ApiVersion.ToString(),
Description = description.IsDeprecated ? "⚠️ 此版本已废弃" : "当前版本"
});
}
}
}优点
缺点
版本控制策略对比
URL 版本 vs Header 版本
// URL 版本(推荐)
// GET /api/v1/users
// GET /api/v2/users
// 优点:
// 1. 直观明确,客户端一眼就能看出使用的版本
// 2. 浏览器可以直接访问和测试
// 3. CDN 缓存友好(不同版本 URL 不同)
// 4. API 网关路由规则简单
// 5. 日志分析方便(URL 中直接包含版本信息)
// 缺点:
// 1. URL 结构变化可能导致代码重构
// 2. 每个版本需要独立的路由配置
// Header 版本
// GET /api/users
// X-API-Version: 2.0
// 优点:
// 1. URL 保持不变,更 "RESTful"
// 2. 版本控制对客户端透明
// 缺点:
// 1. 不直观,难以在浏览器中直接测试
// 2. 调试困难(需要额外设置 Header)
// 3. CDN 缓存不友好(相同 URL 不同版本)
// 4. API 网关路由规则复杂
// 生产建议:优先使用 URL 版本,简单直观
// 如果对 URL 美观有严格要求,使用 Header 版本版本化最佳实践
// 1. 版本号规范:使用语义化主版本号
// ✅ /api/v1/users, /api/v2/users
// ❌ /api/v1.2.3/users(太细粒度,维护成本高)
// ❌ /api/users/v1(不常见,客户端容易混淆)
// 2. 版本间共享逻辑的抽象
// 将共享的业务逻辑抽取到 Service 层
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class OrdersController : ControllerBase
{
private readonly IOrderService _orderService;
// V1 和 V2 共享的端点
[HttpGet("{id}")]
[MapToApiVersion("1.0")]
[MapToApiVersion("2.0")]
public async Task<IActionResult> GetOrder(int id)
{
var order = await _orderService.GetByIdAsync(id);
return order == null ? NotFound() : Ok(order);
}
// V2 新增分页参数
[HttpGet]
[MapToApiVersion("2.0")]
public async Task<IActionResult> GetOrders(
[FromQuery] int page = 1,
[FromQuery] int pageSize = 20,
[FromQuery] string? sortBy = null)
{
var result = await _orderService.GetPagedAsync(page, pageSize, sortBy);
return Ok(result);
}
}
// 3. 使用 API 版本中间件记录版本使用情况
public class ApiVersionLoggingMiddleware
{
private readonly RequestDelegate _next;
private readonly ILogger<ApiVersionLoggingMiddleware> _logger;
public async Task InvokeAsync(HttpContext context)
{
var versionFeature = context.Features.Get<IApiVersionFeature>();
if (versionFeature?.RequestedApiVersion != null)
{
var version = versionFeature.RequestedApiVersion.ToString();
var path = context.Request.Path;
_logger.LogInformation("API 请求: 版本={Version}, 路径={Path}", version, path);
// 记录到指标系统,监控各版本的使用情况
// MetricsApi.RecordApiVersionUsage(version, path);
}
await _next(context);
}
}
// 4. 版本化响应模型
// V1 和 V2 返回不同的 DTO,通过映射层转换
public class OrderV1Dto
{
public int Id { get; set; }
public string CustomerName { get; set; } = "";
public decimal TotalAmount { get; set; }
}
public class OrderV2Dto
{
public int Id { get; set; }
public string CustomerName { get; set; } = "";
public decimal TotalAmount { get; set; }
public OrderStatus Status { get; set; } // V2 新增
public List<OrderItemDto> Items { get; set; } // V2 新增:包含明细
public DateTime CreatedAt { get; set; } // V2 新增
public string OrderNumber { get; set; } = ""; // V2 新增:订单号
}
// 映射配置(AutoMapper)
public class OrderMappingProfile : Profile
{
public OrderMappingProfile()
{
CreateMap<Order, OrderV1Dto>();
CreateMap<Order, OrderV2Dto>()
.ForMember(d => d.Items, opt => opt.MapFrom(s => s.OrderItems));
}
}版本化的替代方案
无版本 API(Backward Compatible)
// 某些团队选择不做版本化,而是保证完全向后兼容
// 优点:URL 永远不变,没有多版本维护成本
// 缺点:限制了 API 的演进能力,可能产生大量可选字段
// 向后兼容的原则:
// 1. 只做增量变更(新增字段、新增端点)
// 2. 新增字段必须有默认值(旧客户端忽略新字段)
// 3. 不删除旧字段(可以标记为 Deprecated)
// 4. 不修改字段的类型和语义
// 5. 不收紧验证规则
// 示例:通过 Profile 控制返回字段
// GET /api/users?fields=name,email,avatar
// 通过查询参数控制返回的字段,避免多版本功能开关(Feature Flags)与版本化结合
// 将版本化与功能开关结合,实现更灵活的功能发布
public class FeatureFlags
{
public bool EnableV2UserApi { get; set; } = false;
public bool EnableV2OrderApi { get; set; } = false;
}
// 在 Controller 中结合版本和功能开关
[HttpGet]
[MapToApiVersion("2.0")]
public async Task<IActionResult> GetUsersV2()
{
if (!_featureFlags.EnableV2UserApi)
return NotFound("此功能尚未开放");
// V2 逻辑...
}
// 功能开关的优势:
// 1. 不需要修改代码即可开启/关闭功能
// 2. 可以针对特定用户或环境开启
// 3. 适合灰度发布(先对 10% 用户开放新版本)
// 4. 可以在版本发布前进行 A/B 测试总结
API 版本控制推荐使用 URL 路径方式(/api/v{version}/users),直观且易于缓存。ASP.NET Core 通过 AddApiVersioning() 配置版本策略,支持 URL、Header、Query String 和 MediaType 四种方式。版本迁移遵循向后兼容原则:新增可选字段不破坏旧版本,删除或修改字段需要新版本。废弃版本通过 Sunset Header 和 Deprecation 头通知客户端,建议 6 个月过渡期。Swagger 自动为每个版本生成独立文档。
关键知识点
- 先分清这个主题位于请求链路、后台任务链路还是基础设施链路。
- 服务端主题通常不只关心功能正确,还关心稳定性、性能和可观测性。
- 任何框架能力都要结合配置、生命周期、异常传播和外部依赖一起看。
项目落地视角
- 画清请求进入、业务执行、外部调用、日志记录和错误返回的完整路径。
- 为关键链路补齐超时、重试、熔断、追踪和结构化日志。
- 把配置与敏感信息分离,并明确不同环境的差异来源。
常见误区
- 只会堆中间件或组件,不知道它们在链路中的执行顺序。
- 忽略生命周期和线程池、连接池等运行时资源约束。
- 没有监控和测试就对性能或可靠性下结论。
进阶路线
- 继续向运行时行为、可观测性、发布治理和微服务协同深入。
- 把主题和数据库、缓存、消息队列、认证授权联动起来理解。
- 沉淀团队级模板,包括统一异常处理、配置约定和基础设施封装。
适用场景
- 当你准备把《API 版本控制策略》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合 API 服务、后台任务、实时通信、认证授权和微服务协作场景。
- 当需求开始涉及稳定性、性能、可观测性和发布流程时,这类主题会成为基础设施能力。
落地建议
- 先定义请求链路与失败路径,再决定中间件、过滤器、服务边界和依赖方式。
- 为关键链路补日志、指标、追踪、超时与重试策略。
- 环境配置与敏感信息分离,避免把生产参数写死在代码或镜像里。
排错清单
- 先确认问题发生在路由、模型绑定、中间件、业务层还是基础设施层。
- 检查 DI 生命周期、配置来源、序列化规则和认证上下文。
- 查看线程池、连接池、缓存命中率和外部依赖超时。
复盘问题
- 如果把《API 版本控制策略》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《API 版本控制策略》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《API 版本控制策略》最大的收益和代价分别是什么?
