×

Swashbuckle 已死?.NET 10中全面转向 Scalar

独孤求败 独孤求败 发表于2026-05-29 09:21:43 浏览14 评论0

抢沙发发表评论

多年来,Swashbuckle.AspNetCore 一直是 .NET 生态中事实上的 Swagger UI 解决方案。然而,随着 .NET 9/10 的演进,它已面临三大难以绕过的核心问题:

问题
实际影响
不支持 OpenAPI 3.1
无法使用 JSON Schema 新特性(如 const、Webhooks、$ref 同级属性)
上游仓库已归档
无安全补丁、无 .NET 新版本兼容性更新
绕过原生元数据系统
无法识别 Minimal API 中的 .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 特性对比

特性
Swagger UI (Swashbuckle)
Scalar
OpenAPI 版本
2.0 / 3.0
✅ 3.1
.NET 9/10 原生支持
交互式请求构建器
浏览器端 OAuth2 PKCE
⚠️ 部分支持
✅ 一等公民
多语言代码生成
✅ 15+ 语言
深色模式
第三方主题
✅ 内置
维护状态
❌ 已归档
✅ 活跃 (MIT)


结语

从 Swashbuckle 迁移到 Scalar + Microsoft.AspNetCore.OpenApi 并非复杂工程,而是一次架构范式升级

  • 从“反射猜测”转向“元数据声明”;
  • 从“外部依赖”转向“原生集成”;
  • 从“功能补丁”转向“标准遵循”(OpenAPI 3.1)。

遵循本文步骤,开发者可在 1 小时内完成迁移,获得:

  • ✅ 完整的 OpenAPI 3.1 支持
  • ✅ 现代化交互式文档体验
  • ✅ 浏览器端 OAuth2 PKCE 认证
  • ✅ 零依赖归档风险

💡 核心原则:让文档成为代码的“贵族”,而非事后补救的附属品。


群贤毕至

访客