API 版本控制
为什么需要版本控制?
核心原因
- 向后兼容:在不破坏现有客户端的情况下演进 API
- 灵活性:允许不同客户端使用不同版本
- 平滑迁移:给客户端时间迁移到新版本
- 并行开发:多个版本可以同时维护
安装包
dotnet add package Asp.Versioning.Mvc
dotnet add package Asp.Versioning.Mvc.ApiExplorer
版本控制策略
1. URL 路径版本控制
最直观和常用的方式,版本号在 URL 路径中。
- 配置
- V1 控制器
- V2 控制器
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// 添加 API 版本控制
builder.Services.AddApiVersioning(options =>
{
// 默认 API 版本
options.DefaultApiVersion = new ApiVersion(1, 0);
// 当客户端没有指定版本时使用默认版本
options.AssumeDefaultVersionWhenUnspecified = true;
// 在响应头中报告支持的版本
options.ReportApiVersions = true;
// URL 路径版本控制
options.ApiVersionReader = new UrlSegmentApiVersionReader();
}).AddApiExplorer(options =>
{
// 版本格式:'v'major[.minor][-status]
options.GroupNameFormat = "'v'VVV";
// 替换路由中的版本占位符
options.SubstituteApiVersionInUrl = true;
});
var app = builder.Build();
app.MapControllers();
app.Run();
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult GetUsers()
{
return Ok(new[]
{
new { Id = 1, Name = "张三" },
new { Id = 2, Name = "李四" }
});
}
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
return Ok(new { Id = id, Name = "张三" });
}
[HttpPost]
public IActionResult CreateUser([FromBody] CreateUserRequest request)
{
return CreatedAtAction(
nameof(GetUser),
new { id = 1, version = "1.0" },
new { Id = 1, Name = request.Name });
}
}
public class CreateUserRequest
{
public string Name { get; set; } = string.Empty;
}
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("2.0")]
public class UsersController : ControllerBase
{
[HttpGet]
public IActionResult GetUsers([FromQuery] int page = 1, [FromQuery] int pageSize = 10)
{
// V2 添加分页支持
return Ok(new
{
Page = page,
PageSize = pageSize,
TotalCount = 100,
Data = new[]
{
new { Id = 1, Name = "张三", Email = "zhangsan@example.com" },
new { Id = 2, Name = "李四", Email = "lisi@example.com" }
}
});
}
[HttpGet("{id}")]
public IActionResult GetUser(int id)
{
// V2 返回更多字段
return Ok(new
{
Id = id,
Name = "张三",
Email = "zhangsan@example.com",
CreatedAt = DateTime.UtcNow
});
}
[HttpPost]
public IActionResult CreateUser([FromBody] CreateUserRequestV2 request)
{
return CreatedAtAction(
nameof(GetUser),
new { id = 1, version = "2.0" },
new
{
Id = 1,
Name = request.Name,
Email = request.Email
});
}
}
public class CreateUserRequestV2
{
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
}
请求示例:
GET /api/v1/users
- V1 版本GET /api/v2/users?page=1&pageSize=10
- V2 版本
2. 查询字符串版本控制
版本号作为查询参数传递。
// Program.cs
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
// 查询字符串版本控制
options.ApiVersionReader = new QueryStringApiVersionReader("api-version");
});
// Controller
[ApiController]
[Route("api/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public IActionResult GetV1()
{
return Ok(new { Version = "1.0", Products = new[] { "Product 1", "Product 2" } });
}
[HttpGet]
[MapToApiVersion("2.0")]
public IActionResult GetV2()
{
return Ok(new
{
Version = "2.0",
Products = new[]
{
new { Id = 1, Name = "Product 1", Price = 100 },
new { Id = 2, Name = "Product 2", Price = 200 }
}
});
}
}
请求示例:
GET /api/products?api-version=1.0
GET /api/products?api-version=2.0
3. HTTP 头版本控制
版本号在 HTTP 请求头中传递。
// Program.cs
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
// HTTP 头版本控制
options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
});
// Controller
[ApiController]
[Route("api/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class OrdersController : ControllerBase
{
[HttpGet("{id}")]
[MapToApiVersion("1.0")]
public IActionResult GetV1(int id)
{
return Ok(new { Id = id, Status = "Pending" });
}
[HttpGet("{id}")]
[MapToApiVersion("2.0")]
public IActionResult GetV2(int id)
{
return Ok(new
{
Id = id,
Status = "Pending",
CreatedAt = DateTime.UtcNow,
Items = new[] { "Item 1", "Item 2" }
});
}
}
请求示例:
curl -H "X-Api-Version: 1.0" https://api.example.com/api/orders/1
curl -H "X-Api-Version: 2.0" https://api.example.com/api/orders/1
4. 媒体类型版本控制
版本号在 Accept 头的媒体类型中指定。
// Program.cs
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
// 媒体类型版本控制
options.ApiVersionReader = new MediaTypeApiVersionReader("version");
});
// Controller - 同上
请求示例:
curl -H "Accept: application/json;version=1.0" https://api.example.com/api/orders/1
curl -H "Accept: application/json;version=2.0" https://api.example.com/api/orders/1
5. 组合版本控制
支持多种版本控制方式。
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
// 组合多种版本读取方式
options.ApiVersionReader = ApiVersionReader.Combine(
new UrlSegmentApiVersionReader(),
new QueryStringApiVersionReader("api-version"),
new HeaderApiVersionReader("X-Api-Version"),
new MediaTypeApiVersionReader("version")
);
});
版本弃用
标记版本为已弃用
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0", Deprecated = true)] // 标记为已弃用
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
public IActionResult GetV1()
{
return Ok(new
{
Message = "此版本已弃用,请使用 v2",
Products = new[] { "Product 1", "Product 2" }
});
}
[HttpGet]
[MapToApiVersion("2.0")]
public IActionResult GetV2()
{
return Ok(new
{
Products = new[]
{
new { Id = 1, Name = "Product 1" },
new { Id = 2, Name = "Product 2" }
}
});
}
}
当访问已弃用的版本时,响应头会包含:
api-deprecated-versions: 1.0
api-supported-versions: 2.0
版本约定
使用约定配置版本
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
// 使用约定配置版本
options.Conventions.Controller<UsersV1Controller>()
.HasApiVersion(new ApiVersion(1, 0));
options.Conventions.Controller<UsersV2Controller>()
.HasApiVersion(new ApiVersion(2, 0));
});
// 控制器不需要 [ApiVersion] 特性
[ApiController]
[Route("api/v{version:apiVersion}/users")]
public class UsersV1Controller : ControllerBase
{
[HttpGet]
public IActionResult GetUsers()
{
return Ok(new[] { "User 1", "User 2" });
}
}
[ApiController]
[Route("api/v{version:apiVersion}/users")]
public class UsersV2Controller : ControllerBase
{
[HttpGet]
public IActionResult GetUsers()
{
return Ok(new[]
{
new { Id = 1, Name = "User 1" },
new { Id = 2, Name = "User 2" }
});
}
}
Swagger 集成
配置 Swagger 支持多版本
dotnet add package Swashbuckle.AspNetCore
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
// 添加 API 版本控制
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
options.ApiVersionReader = new UrlSegmentApiVersionReader();
}).AddApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
// 添加 Swagger
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "My API V1",
Description = "API 版本 1"
});
options.SwaggerDoc("v2", new OpenApiInfo
{
Version = "v2",
Title = "My API V2",
Description = "API 版本 2 - 添加了分页和更多字段"
});
});
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "V1");
options.SwaggerEndpoint("/swagger/v2/swagger.json", "V2");
});
}
app.MapControllers();
app.Run();
版本迁移策略
1. 共享代码
- 共享服务
- DTO 映射
// 共享服务接口
public interface IUserService
{
Task<List<User>> GetUsersAsync();
Task<User?> GetUserByIdAsync(int id);
Task<User> CreateUserAsync(CreateUserDto dto);
}
// 共享实现
public class UserService : IUserService
{
private readonly AppDbContext _context;
public UserService(AppDbContext context)
{
_context = context;
}
public async Task<List<User>> GetUsersAsync()
{
return await _context.Users.ToListAsync();
}
public async Task<User?> GetUserByIdAsync(int id)
{
return await _context.Users.FindAsync(id);
}
public async Task<User> CreateUserAsync(CreateUserDto dto)
{
var user = new User
{
Name = dto.Name,
Email = dto.Email
};
_context.Users.Add(user);
await _context.SaveChangesAsync();
return user;
}
}
// V1 DTO
public class UserDtoV1
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
}
// V2 DTO
public class UserDtoV2
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string Email { get; set; } = string.Empty;
public DateTime CreatedAt { get; set; }
}
// Mapper
public static class UserMapper
{
public static UserDtoV1 ToV1Dto(User user)
{
return new UserDtoV1
{
Id = user.Id,
Name = user.Name
};
}
public static UserDtoV2 ToV2Dto(User user)
{
return new UserDtoV2
{
Id = user.Id,
Name = user.Name,
Email = user.Email,
CreatedAt = user.CreatedAt
};
}
}
// V1 Controller
[ApiVersion("1.0")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
public UsersController(IUserService userService)
{
_userService = userService;
}
[HttpGet]
public async Task<IActionResult> GetUsers()
{
var users = await _userService.GetUsersAsync();
var dtos = users.Select(UserMapper.ToV1Dto);
return Ok(dtos);
}
}
// V2 Controller
[ApiVersion("2.0")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
public UsersController(IUserService userService)
{
_userService = userService;
}
[HttpGet]
public async Task<IActionResult> GetUsers()
{
var users = await _userService.GetUsersAsync();
var dtos = users.Select(UserMapper.ToV2Dto);
return Ok(dtos);
}
}
2. 渐进式迁移
[ApiController]
[Route("api/v{version:apiVersion}/[controller]")]
[ApiVersion("1.0")]
[ApiVersion("2.0")]
public class ProductsController : ControllerBase
{
[HttpGet]
[MapToApiVersion("1.0")]
[MapToApiVersion("2.0")]
public IActionResult GetProducts([FromQuery] int? page = null, [FromQuery] int? pageSize = null)
{
// V2 客户端传递分页参数,V1 客户端不传
if (HttpContext.GetRequestedApiVersion()?.MajorVersion == 1)
{
// V1 返回所有产品
return Ok(new[] { "Product 1", "Product 2", "Product 3" });
}
// V2 返回分页产品
page ??= 1;
pageSize ??= 10;
return Ok(new
{
Page = page,
PageSize = pageSize,
Data = new[] { "Product 1", "Product 2" }
});
}
}
最佳实践
最佳实践清单
1. 版本号策略
// ✅ 好:使用语义化版本
[ApiVersion("1.0")] // 主版本.次版本
[ApiVersion("1.1")] // 向后兼容的改进
[ApiVersion("2.0")] // 破坏性变更
// ❌ 差:过于细粒度
[ApiVersion("1.0.0.1")] // 不推荐
2. 默认版本
// ✅ 好:设置默认版本
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true; // 未指定版本时使用默认版本
});
// ❌ 差:不设置默认版本
// 客户端必须始终指定版本,增加使用复杂度
3. 报告支持的版本
// ✅ 好:在响应头中报告版本
builder.Services.AddApiVersioning(options =>
{
options.ReportApiVersions = true;
});
// 响应头会包含:
// api-supported-versions: 1.0, 2.0
// api-deprecated-versions: 0.9
4. URL 路径版本控制(推荐)
// ✅ 好:版本在 URL 中清晰可见
// GET /api/v1/users
// GET /api/v2/users
// ❌ 避免:版本隐藏在查询字符串或头中
// GET /api/users?version=1.0
// 不利于缓存和调试
5. 及时弃用旧版本
// ✅ 好:标记旧版本为已弃用
[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("2.0")]
public class UsersController : ControllerBase
{
// ...
}
// 给客户端足够的迁移时间(如 6 个月)
// 在文档中明确弃用时间表
6. 文档完善
// ✅ 好:为每个版本提供清晰的文档
options.SwaggerDoc("v1", new OpenApiInfo
{
Version = "v1",
Title = "My API V1",
Description = "初始版本",
Contact = new OpenApiContact { Email = "support@example.com" }
});
options.SwaggerDoc("v2", new OpenApiInfo
{
Version = "v2",
Title = "My API V2",
Description = "添加分页支持和更多字段\n\n**变更内容:**\n- 添加分页参数\n- 返回更多用户字段",
Contact = new OpenApiContact { Email = "support@example.com" }
});
7. 向后兼容
// ✅ 好:添加可选字段,保持向后兼容
public class UserDtoV2
{
public int Id { get; set; }
public string Name { get; set; } = string.Empty;
public string? Email { get; set; } // 可选字段
public DateTime? CreatedAt { get; set; } // 可选字段
}
// ❌ 差:删除字段或改变数据类型
// 这是破坏性变更,需要升级主版本号
版本控制对比
方式 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
URL 路径 | 清晰可见、易缓存、易调试 | URL 变更 | 推荐 - 公共 API |
查询字符串 | URL 不变 | 不利于缓存、易被忽略 | 内部 API |
HTTP 头 | URL 不变、符合 REST | 不易调试、需文档说明 | 企业 API |
媒体类型 | 符合 REST 标准 | 复杂度高、不直观 | 严格 REST API |
总结
关键要点
- 选择合适的版本控制策略(推荐 URL 路径)
- 设置默认版本,简化客户端使用
- 使用语义化版本号(主版本.次版本)
- 及时标记和弃用旧版本
- 在响应头中报告支持的版本
- 为每个版本提供完整文档
- 共享业务逻辑,只在表示层区分版本
- 给客户端足够的迁移时间