Skip to main content

API 版本控制

为什么需要版本控制?

核心原因
  • 向后兼容:在不破坏现有客户端的情况下演进 API
  • 灵活性:允许不同客户端使用不同版本
  • 平滑迁移:给客户端时间迁移到新版本
  • 并行开发:多个版本可以同时维护

安装包

dotnet add package Asp.Versioning.Mvc
dotnet add package Asp.Versioning.Mvc.ApiExplorer

版本控制策略

1. URL 路径版本控制

最直观和常用的方式,版本号在 URL 路径中。

// 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();

请求示例

  • 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. 共享代码

// 共享服务接口
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;
}
}

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 路径)
  • 设置默认版本,简化客户端使用
  • 使用语义化版本号(主版本.次版本)
  • 及时标记和弃用旧版本
  • 在响应头中报告支持的版本
  • 为每个版本提供完整文档
  • 共享业务逻辑,只在表示层区分版本
  • 给客户端足够的迁移时间

相关资源