多年来,Swashbuckle.AspNetCore 一直是 .NET 生态中事实上的 Swagger UI 解决方案。然而,随着 .NET 9/10 的演进,它已面临三大难以绕过的核心问题:
const、Webhooks、$ref 同级属性).Produces<T>()、.ProducesProblem() 等声明
微软已在 .NET 9 中将 Swashbuckle 从 dotnet new webapi 模板中移除,并替换为 Microsoft.AspNetCore.OpenApi——这标志着官方态度的明确转向①。
替代方案:新一代文档栈
现代 .NET API 文档方案由两个独立组件构成,可灵活组合:
1. Microsoft.AspNetCore.OpenApi(官方文档生成器)
直接集成于 .NET 9+,无需额外包; 基于端点元数据生成 OpenAPI 3.1 文档,无需 XML 注释解析或反射; 与 Minimal API 深度协同,自动识别 .Produces<T>()、.WithName()等声明②。
2. Scalar.AspNetCore(开源交互式文档门户)
MIT 许可,活跃维护; 支持 OpenAPI 3.1 全特性渲染; 内置 OAuth2/PKCE 浏览器认证、15+ 语言代码生成、深色/浅色主题③。
★💡 关键优势:二者解耦设计,可单独替换任一组件。实践中组合使用是最优解。
迁移步骤详解
步骤 1:移除 Swashbuckle
dotnet remove package Swashbuckle.AspNetCore
# 同时移除所有 Swashbuckle.AspNetCore.* 子包
清理 Program.cs 中的相关调用:
// ❌ 删除以下代码
// builder.Services.AddSwaggerGen();
// app.UseSwagger();
// app.UseSwaggerUI();
步骤 2:添加新依赖
dotnet add package Microsoft.AspNetCore.OpenApi
dotnet add package Scalar.AspNetCore
步骤 3:注册文档生成服务
using Scalar.AspNetCore;
var builder = WebApplication.CreateBuilder(args);
// 注册原生 OpenAPI 生成器(文档名为 "v1")
builder.Services.AddOpenApi("v1", options =>
{
options.AddDocumentTransformer((doc, _, _) =>
{
doc.Info = new OpenApiInfo
{
Title = "Bookings API",
Version = "1.0",
Description = "Hotel booking management API."
};
return Task.CompletedTask;
});
});
var app = builder.Build();
步骤 4:映射端点(仅开发环境)
if (app.Environment.IsDevelopment())
{
// 生成 /openapi/v1.json
app.MapOpenApi("/openapi/v1.json");
// 挂载 Scalar UI 到 /scalar
app.MapScalarApiReference();
}
await app.RunAsync();
访问 https://localhost:5001/scalar 即可看到交互式 API 探索界面。
集成 JWT Bearer 认证
Swashbuckle 使用 AddSecurityDefinition,而新方案采用文档转换器(Document Transformer)模式:
using Microsoft.AspNetCore.Authentication;
using Microsoft.OpenApi.Models;
internal sealed class BearerSecuritySchemeTransformer(
IAuthenticationSchemeProvider authSchemeProvider)
: IOpenApiDocumentTransformer
{
public async Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken ct)
{
var schemes = await authSchemeProvider.GetAllSchemesAsync();
if (!schemes.Any(s => s.Name == "Bearer")) return;
document.Components ??= new OpenApiComponents();
document.Components.SecuritySchemes ??= new Dictionary<string, OpenApiSecurityScheme>();
document.Components.SecuritySchemes["Bearer"] = new OpenApiSecurityScheme
{
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "Enter your JWT token."
};
}
}
注册顺序至关重要:转换器必须在 AddOpenApi 之前注册到 DI 容器:
builder.Services.AddTransient<BearerSecuritySchemeTransformer>();
builder.Services.AddOpenApi("v1", options =>
{
options.AddDocumentTransformer<BearerSecuritySchemeTransformer>();
// 其他配置...
});
支持 OAuth2 Authorization Code + PKCE
Scalar 支持浏览器端直接完成 OAuth2 认证流程,无需 Postman:
app.MapScalarApiReference(opts =>
{
opts.WithTitle("Bookings API");
opts.AddAuthorizationCodeFlow("OAuth2", flow =>
{
flow.WithClientId("bookings-frontend-client")
.WithSelectedScopes("openid", "profile")
.WithPkce(Pkce.Sha256); // 强制使用 PKCE
});
});
★⚠️ 安全提醒:仅配置公共客户端(frontend client),切勿在此暴露后端机密客户端密钥。
多版本 API 支持
每个主版本独立注册文档:
builder.Services.AddOpenApi("v1", options => { /* v1 配置 */ });
builder.Services.AddOpenApi("v2", options => { /* v2 配置 */ });
app.MapOpenApi("/openapi/v1.json");
app.MapOpenApi("/openapi/v2.json");
app.MapScalarApiReference(opts =>
{
// 启用版本切换:/scalar/v1, /scalar/v2
opts.WithEndpointPrefix("/scalar/{documentName}");
});
Scalar UI 会自动提供版本下拉选择器。
常见陷阱与规避策略
1. .Produces<T>() 必须显式声明
原生管道不会推断响应类型,缺失 .Produces<T>() 将导致操作无响应模式:
// ❌ 错误:缺少 .Produces<BookingResponse>()
app.MapGet("/bookings/{id:guid}", GetBookingAsync)
.WithName("GetBooking")
.ProducesProblem(StatusCodes.Status404NotFound);
// OpenAPI 中无 200 响应定义!
// ✅ 正确:显式声明成功响应
app.MapGet("/bookings/{id:guid}", GetBookingAsync)
.WithName("GetBooking")
.Produces<BookingResponse>() // ← 必须添加
.ProducesProblem(StatusCodes.Status404NotFound);
2. 优先使用 TypedResults 而非 Results
Results.Ok(value) 返回 IResult,类型信息在编译时丢失;TypedResults.Ok(value) 返回 Ok<T>,类型可被静态分析:
// ❌ BAD:类型丢失
return Results.Ok(booking);
// ✅ GOOD:类型保留,自动写入 OpenAPI Schema
return TypedResults.Ok(booking);
3. 生产环境保护文档端点
MapOpenApi 和 MapScalarApiReference 默认公开访问,需显式授权:
// 方案 1:仅开发环境暴露
if (app.Environment.IsDevelopment())
{
app.MapOpenApi("/openapi/v1.json");
app.MapScalarApiReference();
}
// 方案 2:生产环境加授权策略
app.MapOpenApi("/openapi/v1.json")
.RequireAuthorization("InternalDeveloper");
app.MapScalarApiReference()
.RequireAuthorization("InternalDeveloper");
4. 转换器执行顺序
文档转换器按注册顺序执行。若转换器 B 依赖 A 的设置,必须先注册 A:
// ✅ 正确顺序
builder.Services.AddOpenApi("v1", options =>
{
options.AddDocumentTransformer<InitComponentsTransformer>(); // 先初始化 Components
options.AddDocumentTransformer<BearerSecuritySchemeTransformer>(); // 再添加安全方案
});
Scalar vs Swagger UI 特性对比
结语
从 Swashbuckle 迁移到 Scalar + Microsoft.AspNetCore.OpenApi 并非复杂工程,而是一次架构范式升级:
从“反射猜测”转向“元数据声明”; 从“外部依赖”转向“原生集成”; 从“功能补丁”转向“标准遵循”(OpenAPI 3.1)。
遵循本文步骤,开发者可在 1 小时内完成迁移,获得:
✅ 完整的 OpenAPI 3.1 支持 ✅ 现代化交互式文档体验 ✅ 浏览器端 OAuth2 PKCE 认证 ✅ 零依赖归档风险
★💡 核心原则:让文档成为代码的“贵族”,而非事后补救的附属品。