mcpField
D
dotnet-api-versioning

dotnet-api-versioning

github.com/wshaddix开发8668 次安装35 周前更新
代码仓库

关于 dotnet-api-versioning

为 ASP.NET Core 开发者提供基于 Asp.Versioning 的 API 版本管理指南,涵盖 Minimal APIs 与 MVC。内容包括多种版本读取策略、弃用通知及旧版包迁移。

主要功能

  1. 01URL、请求头和查询参数版本控制
  2. 02配置 Minimal APIs 与 MVC
  3. 03组合多个版本读取器
  4. 04设置 Sunset 弃用策略
  5. 05迁移旧版 NuGet 包

适用场景

  1. 01为新 API 选择版本标识策略
  2. 02为 API 版本提供弃用和迁移通知
  3. 03将旧版版本管理包迁移至 Asp.Versioning

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-api-versioning

安装后在 Claude Code 中输入/dotnet-api-versioning确认已加载。

详细介绍

dotnet-api-versioning

使用 Asp.Versioning 系列库为 ASP.NET Core 实现 API 版本控制策略。URL 路径段版本控制(/api/v1/)是首选方案,简单易用且易于发现。本技能涵盖 URL、标头和查询字符串版本控制,以及 Minimal API 和 MVC 控制器的配置、弃用策略的实施,以及从旧版程序包迁移。

不在范围内: Minimal API 端点模式(路由组、筛选器、TypedResults)——请参阅 [skill:dotnet-minimal-apis]。按 API 版本生成 OpenAPI 文档——请参阅 [skill:dotnet-openapi]。按版本进行身份验证和授权——请参阅 [skill:dotnet-api-security]。

交叉引用:[skill:dotnet-minimal-apis] 介绍 Minimal API 端点模式,[skill:dotnet-openapi] 介绍版本化 OpenAPI 文档。


程序包概览

程序包目标状态
Asp.Versioning.HttpMinimal API当前
Asp.Versioning.Mvc.ApiExplorerMVC 控制器 + API Explorer当前
Asp.Versioning.MvcMVC 控制器(无 API Explorer)当前
Microsoft.AspNetCore.Mvc.VersioningMVC 控制器旧版——迁移到 Asp.Versioning.Mvc
Microsoft.AspNetCore.Mvc.Versioning.ApiExplorerMVC + API Explorer旧版——迁移到 Asp.Versioning.Mvc.ApiExplorer

为 Minimal API 安装程序包:

<PackageReference Include="Asp.Versioning.Http" Version="8.*" />

为 MVC 控制器安装程序包:

<PackageReference Include="Asp.Versioning.Mvc.ApiExplorer" Version="8.*" />

URL 路径段版本控制(首选)

URL 路径段版本控制会将版本嵌入路径中(/api/v1/products)。这是最简单的策略,适用于所有 HTTP 客户端、支持缓存,并且在日志和文档中一目了然。

Minimal API
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true; // Adds api-supported-versions header
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
});

var app = builder.Build();

var versionSet = app.NewApiVersionSet()
    .HasApiVersion(new ApiVersion(1, 0))
    .HasApiVersion(new ApiVersion(2, 0))
    .ReportApiVersions()
    .Build();

var v1 = app.MapGroup("/api/v{version:apiVersion}/products")
    .WithApiVersionSet(versionSet)
    .MapToApiVersion(new ApiVersion(1, 0));

var v2 = app.MapGroup("/api/v{version:apiVersion}/products")
    .WithApiVersionSet(versionSet)
    .MapToApiVersion(new ApiVersion(2, 0));

// V1: returns basic product info
v1.MapGet("/", async (AppDbContext db) =>
    TypedResults.Ok(await db.Products
        .Select(p => new ProductV1Dto(p.Id, p.Name, p.Price))
        .ToListAsync()));

// V2: returns extended product info with category
v2.MapGet("/", async (AppDbContext db) =>
    TypedResults.Ok(await db.Products
        .Select(p => new ProductV2Dto(p.Id, p.Name, p.Price, p.Category, p.CreatedAt))
        .ToListAsync()));
MVC 控制器
builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = new UrlSegmentApiVersionReader();
})
.AddMvc()
.AddApiExplorer(options =>
{
    options.GroupNameFormat = "'v'VVV"; // e.g., v1, v2
    options.SubstituteApiVersionInUrl = true;
});

// V1 controller
[ApiController]
[Route("api/v{version:apiVersion}/products")]
[ApiVersion("1.0")]
public sealed class ProductsController(AppDbContext db) : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> GetAll() =>
        Ok(await db.Products
            .Select(p => new ProductV1Dto(p.Id, p.Name, p.Price))
            .ToListAsync());
}

// V2 controller -- use explicit route, not [controller] token
[ApiController]
[Route("api/v{version:apiVersion}/products")]
[ApiVersion("2.0")]
public sealed class ProductsV2Controller(AppDbContext db) : ControllerBase
{
    [HttpGet]
    public async Task<IActionResult> GetAll() =>
        Ok(await db.Products
            .Select(p => new ProductV2Dto(p.Id, p.Name, p.Price, p.Category, p.CreatedAt))
            .ToListAsync());
}

标头版本控制

标头版本控制从自定义请求标头中读取 API 版本。它能保持 URL 简洁,但不易发现,也更难通过浏览器进行测试。

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = new HeaderApiVersionReader("X-Api-Version");
});

客户端请求:

GET /api/products HTTP/1.1
Host: api.example.com
X-Api-Version: 2.0

查询字符串版本控制

查询字符串版本控制使用查询参数(默认值:api-version)。使用简单,但会使 URL 显得冗长,也可能与缓存策略冲突。

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(1, 0);
    options.AssumeDefaultVersionWhenUnspecified = true;
    options.ReportApiVersions = true;
    options.ApiVersionReader = new QueryStringApiVersionReader("api-version");
});

客户端请求:

GET /api/products?api-version=2.0 HTTP/1.1
Host: api.example.com

组合版本读取器

可以组合多个读取器。第一个解析出版本的读取器将生效。从一种策略迁移到另一种策略时,这种做法很有用:

options.ApiVersionReader = ApiVersionReader.Combine(
    new UrlSegmentApiVersionReader(),
    new HeaderApiVersionReader("X-Api-Version"),
    new QueryStringApiVersionReader("api-version"));

弃用策略

弃用策略会告知使用者某个 API 版本已弃用,即将移除。Sunset HTTP 响应标头遵循 RFC 8594。

builder.Services.AddApiVersioning(options =>
{
    options.DefaultApiVersion = new ApiVersion(2, 0);
    options.ReportApiVersions = true;
    options.Policies.Sunset(1.0)
        .Effective(new DateTimeOffset(2026, 6, 1, 0, 0, 0, TimeSpan.Zero))
        .Link("https://docs.example.com/api/migration-v1-to-v2")
            .Title("V1 to V2 Migration Guide")
            .Type("text/html");
});

对 v1 请求的响应标头:

api-supported-versions: 1.0, 2.0
api-deprecated-versions: 1.0
Sunset: Sun, 01 Jun 2026 00:00:00 GMT
Link: <https://docs.example.com/api/migration-v1-to-v2>; rel="sunset"; title="V1 to V2 Migration Guide"; type="text/html"
弃用某个版本

使用版本集(Minimal API)或特性(MVC)将某个版本标记为已弃用:

// Minimal APIs
var versionSet = app.NewApiVersionSet()
    .HasApiVersion(new ApiVersion(1, 0))
    .HasDeprecatedApiVersion(new ApiVersion(1, 0))
    .HasApiVersion(new ApiVersion(2, 0))
    .ReportApiVersions()
    .Build();

// MVC controllers
[ApiVersion("1.0", Deprecated = true)]
[ApiVersion("2.0")]
public sealed class ProductsController : ControllerBase { }

从旧版程序包迁移

使用 Microsoft.AspNetCore.Mvc.Versioning 的项目应迁移到 Asp.Versioning.Mvc(Minimal API 项目则迁移到 Asp.Versioning.Http)。除命名空间变更外,API 表面大体兼容:

旧版命名空间当前命名空间
Microsoft.AspNetCore.Mvc.VersioningAsp.Versioning
Microsoft.AspNetCore.Mvc.ApiExplorerAsp.Versioning.ApiExplorer

主要迁移步骤:

  1. 替换 NuGet 程序包引用
  2. 将 using 指令从 Microsoft.AspNetCore.Mvc.Versioning 更新为 Asp.Versioning
  3. 将服务注册从 services.AddApiVersioning()(旧版扩展方法)更新为 Asp.Versioning 提供的当前扩展方法
  4. 检查所有自定义 IApiVersionReader 实现是否存在破坏性变更

详细步骤请参阅迁移指南。


版本策略决策指南

策略优点缺点最适合
URL 路径段(/api/v1/)简单、直观、可缓存,兼容性好URL 会随版本变化公共 API、大多数项目(首选)
请求头(X-Api-Version: 1.0)URL 简洁,无需更改路径不易发现,测试更困难客户端受控的内部 API
查询字符串(?api-version=1.0)易于添加,无需更改路径URL 混杂,存在缓存键问题快速原型开发、兼容旧版

建议: 所有新项目都从 URL 路径段版本控制开始。仅在从现有策略迁移,或特定客户端限制要求时,才添加请求头或查询字符串读取方式。


Agent 注意事项

  1. 新项目不要使用旧版 Microsoft.AspNetCore.Mvc.Versioning 包——请使用 Asp.Versioning.Http(Minimal APIs)或 Asp.Versioning.Mvc(MVC 控制器)。
  2. 不要在包引用中硬编码版本号——使用版本范围(例如 8.*),以确保包版本与最新的兼容版本一致。
  3. 不要忘记设置 ReportApiVersions = true——否则客户端无法从响应头中发现可用版本。
  4. 不要以不一致的方式混用 MapToApiVersion 和路由组前缀——每个路由组都应只面向一个 API 版本。
  5. 弃用版本时不要遗漏日落策略——始终提供日落日期和迁移链接,以便使用者做好规划。
  6. 公共 API 不要使用 AssumeDefaultVersionWhenUnspecified = true——这会掩盖对使用者的版本控制要求。应要求明确选择版本。

前提条件

  • .NET 8.0+(LTS 基准)
  • 用于 Minimal APIs 的 Asp.Versioning.Http
  • 用于集成 API Explorer 的 MVC 控制器的 Asp.Versioning.Mvc.ApiExplorer

参考资料

评论

支持 Markdown