practical-aspnetcore 的 ASP.NET Core 10 示例过时清理报告:从 Swashbuckle/NSwag 到内置 API 的三大迁移实践

原创2026-10-13 18:09:561,995 阅读
文章标签:示例工程

practical-aspnetcore 的 ASP.NET Core 10 示例过时清理报告:从 Swashbuckle/NSwag 到内置 API 的三大迁移实践

导读

本文基于 OUT-OF-DATE.md 这份由仓库生成的《Outdated ASP.NET Core 10 Samples Report》,系统讲解 practical-aspnetcore 项目在升级到 ASP.NET Core 10 时对 637 个示例的过时排查结果:14 个过时示例中 12 个已完成迁移、2 个按低优先级保留。文章将结合仓库中的真实源码,逐一拆解 OpenAPI(Swashbuckle/NSwag → 内置 OpenAPI)、Server-Sent Events(手工实现 → 内置 SSE)、后台服务(自定义基类 → BackgroundService)三套迁移模式的"迁移前/迁移后"代码对比与收益,供你在自己的项目中参考执行。

一、报告背景:为何要扫描 637 个示例

.NET 10 / ASP.NET Core 10 带来了一批"开箱即用"的新能力,使得此前依赖第三方包或手写样板代码的示例变得过时。practical-aspnetcore 仓库对此做了一次系统性的过时扫描,报告生成于 2026-03-06,结论如下:

  • 被分析示例总数:637
  • 识别出的过时示例:14
  • 已完成迁移:12 ✅
  • 剩余未迁移:2

迁移状态总览(来自 OUT-OF-DATE.md 的 Migration Status 章节):

示例 类别 迁移内容
minimal-api/open-api-1 OpenAPI Swashbuckle → 内置
minimal-api/open-api-2 OpenAPI Swashbuckle → 内置
minimal-api/map-group-2 OpenAPI Swashbuckle → 内置
minimal-api/map-group-3 OpenAPI Swashbuckle → 内置
minimal-api/map-4 OpenAPI Swashbuckle → 内置
authentication/authentication-4 OpenAPI Swashbuckle → 内置
authentication/authentication-5 OpenAPI Swashbuckle → 内置
mini/minimal-api-pokedex OpenAPI Swashbuckle → 内置
mvc/nswag OpenAPI NSwag → 内置
sse SSE 手工实现 → 内置
ihosted-service/ihosted-service-1 后台任务 自定义基类 → BackgroundService

保留的两个示例:

示例 优先级 说明
mvc/nswag-2 LOW 备选方案,保留作参考
generic-host/generic-host-1/2 LOW 在非 Web 场景下仍然有效

二、迁移模式一:OpenAPI(9 个示例从 Swashbuckle/NSwag 迁移到内置 OpenAPI)

这是本次迁移中数量最多的一类。原因在于 .NET 10 的内置 OpenAPI 支持已足够成熟:不再需要 Swashbuckle.AspNetCore 或 NSwag 的 AddEndpointsApiExplorer() + AddSwaggerGen() + UseSwagger() + UseSwaggerUI() 四件套。

2.1 迁移前(Swashbuckle 时代)

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

var app = builder.Build();
app.UseSwagger();
app.UseSwaggerUI();

app.MapGet("/greeting", Hello.GetGreeting).WithOpenApi(op => { ... });

2.2 迁移后(内置 OpenAPI)

builder.Services.AddOpenApi();

var app = builder.Build();
app.MapOpenApi();
app.MapScalarApiReference();

/// <summary>
/// Return greeting given name
/// </summary>
app.MapGet("/greeting", Hello.GetGreeting);

2.3 仓库源码佐证:open-api-1 的完整形态

迁移后的 projects/minimal-api/open-api-1/Program.cs 展示了完整形态:

using Scalar.AspNetCore;

var builder = WebApplication.CreateBuilder();
builder.Services.AddOpenApi();

var app = builder.Build();
app.MapOpenApi();
app.MapScalarApiReference();

app.MapGet("/", () => Results.Content("""<html>...</html>""", "text/html"));

app.MapGet("/greeting", Hello.GetGreeting);

app.Run();

public record Person(string Name);

/// <summary>
/// Return greeting given name
/// </summary>
public static class Hello
{
    /// <param name="name">The name of the person to greet</param>
    public static IResult GetGreeting(string name) => Results.Ok(new Person(name));
}

配套的 projects/minimal-api/open-api-1/open-api-1.csproj 揭示了两个关键细节:

<TargetFramework>net10.0</TargetFramework>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.11" />
<PackageReference Include="Scalar.AspNetCore" Version="2.16.18" />
  • GenerateDocumentationFile=true:开启 XML 文档注释生成,这是内置 OpenAPI 利用 <summary>/<param>/<response> 注释填充文档元数据的前提;
  • 依赖大幅精简:只保留 Microsoft.AspNetCore.OpenApi(框架内置能力的包封装)和 Scalar.AspNetCore(用于渲染交互式 API 文档 UI),彻底移除 Swashbuckle。

2.4 XML 注释驱动的文档增强(open-api-2)

迁移后的 projects/minimal-api/open-api-2/Program.cs 展示了如何用 XML 注释描述响应码,并结合 Results<Ok<Person>, NotFound> 强类型结果:

/// <summary>
/// Return greeting given name
/// </summary>
public static class Hello
{
    /// <param name="name">The name of the person to greet</param>
    /// <response code="200">Returns the person with the greeting</response>
    /// <response code="404">If name is null or whitespace</response>
    public static Results<Ok<Person>, NotFound> GetGreeting(string name)
    {
        if (string.IsNullOrWhiteSpace(name))
            return TypedResults.NotFound();

        return TypedResults.Ok(new Person(name));
    }
}

这一模式与 projects/net10/README.md 中记录的 open-api-8(XML 注释元数据)、open-api-10(<response> 元素)、open-api-11(<returns> 元素填充 200 响应)一脉相承,属于同一套内置 OpenAPI 能力栈。

2.5 应用到路由组、MVC 与完整应用

迁移并非只发生在最小 API 上,仓库中其余几个迁移示例验证了内置 OpenAPI 的覆盖面:

三、迁移模式二:Server-Sent Events(sse 示例从手工实现迁移到内置 SSE)

.NET 10 为 Minimal API 提供了 Results.ServerSentEvents(...) 内置支持,彻底告别手写 text/event-stream 响应头、手动写 data: 前缀、手动 Flush 的样板代码。

3.1 迁移前(手工实现)

app.MapGet("/sse", async context =>
{
    context.Response.ContentType = "text/event-stream";
    await context.Response.WriteAsync($"data: {data}\n");
    await context.Response.Body.FlushAsync();
});

3.2 迁移后(内置支持)

app.MapGet("/sse", (CancellationToken ct) =>
{
    async IAsyncEnumerable<string> GetEvents([EnumeratorCancellation] CancellationToken ct)
    {
        while (!ct.IsCancellationRequested)
        {
            yield return $"Event at {DateTime.UtcNow}";
            await Task.Delay(1000, ct);
        }
    }
    return Results.ServerSentEvents(GetEvents(ct));
});

3.3 仓库源码佐证:sse 示例的完整实现

迁移后的 projects/sse/Program.cs 在真实实现中比报告示例多了两个值得注意的点:

  1. Accept 头校验:只有客户端携带 Accept: text/event-stream 时才返回 SSE 流,否则返回 Results.BadRequest;
  2. 命名事件类型:Results.ServerSentEvents(CounterAsync(cancellationToken), eventType: "greeting") 可为事件指定类型名,浏览器端用 source.addEventListener('greeting', ...) 监听。
app.MapGet("/sse", async context =>
{
    if (context.Request.Headers.Accept == "text/event-stream")
    {
        return Results.ServerSentEvents(CounterAsync(cancellationToken), eventType: "greeting");
    }
    else
    {
        return Results.BadRequest("Unsupported Accept header. Use 'text/event-stream'.");
    }
});

注意 projects/sse/sse.csproj 是纯 Microsoft.NET.Sdk.Web 项目,没有任何额外包引用——SSE 是框架内置能力。

3.4 内置 SSE 的能力纵深(net10/sse-2/3/4)

projects/net10/README.md 记录了三个递进的 SSE 示例,可以作为本迁移的进阶参考:

  • sse-2:最小形态,Results.ServerSentEvents(CounterAsync(cancellationToken), "greeting");
  • sse-3:使用命名参数 eventType: "greeting",浏览器端通过 addEventListener 分门别类接收;
  • sse-4:引入 System.Net.ServerSentEvents 命名空间下的 SseItem<string>,在同一个流中混合发送带事件类型与默认消息类型的事件:
using System.Net.ServerSentEvents;

async IAsyncEnumerable<SseItem<string>> GetEventsAsync([EnumeratorCancellation] CancellationToken cancellationToken)
{
    int count = 0;
    while (true && !cancellationToken.IsCancellationRequested)
    {
        yield return new SseItem<string>($"hello world {++count}", "greeting");
        yield return new SseItem<string>($"{DateTime.UtcNow}");
        await Task.Delay(3000, cancellationToken);
    }
}

四、迁移模式三:IHostedService(用 BackgroundService 基类简化后台任务)

4.1 迁移前(自定义基类)

public abstract class HostedService : IHostedService, IDisposable { /* 20+ lines */ }

迁移前的完整形态可以在 projects/ihosted-service/ihosted-service-2/Program.cs 中看到:需要手动实现 StartAsync、StopAsync、Dispose,手动管理 Timer,并处理 Timer.Change(Timeout.Infinite, 0) 之类的停止细节:

public class GreeterUpdaterService : IHostedService, IDisposable
{
    Timer _timer;

    public Task StartAsync(CancellationToken cancellationToken)
    {
        _timer = new Timer(DoWork, null, TimeSpan.Zero, TimeSpan.FromSeconds(1));
        return Task.CompletedTask;
    }

    public Task StopAsync(CancellationToken cancellationToken)
    {
        _timer?.Change(Timeout.Infinite, 0);
        return Task.CompletedTask;
    }

    public void Dispose() => _timer?.Dispose();
}

4.2 迁移后(BackgroundService)

public class MyService : BackgroundService
{
    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            // Do work
            await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
        }
    }
}

4.3 仓库源码佐证:ihosted-service-1 的完整实现

迁移后的 projects/ihosted-service/ihosted-service-1/Program.cs 通过 AddHostedService<GreeterUpdaterService>() 注册,只需重写一个 ExecuteAsync 方法:

builder.Services.AddSingleton<Greeter>();
builder.Services.AddHostedService<GreeterUpdaterService>();

public class GreeterUpdaterService : BackgroundService
{
    private readonly Greeter _greeter;

    public GreeterUpdaterService(Greeter greeter) => _greeter = greeter;

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            _greeter.Counter++;
            await Task.Delay(TimeSpan.FromSeconds(1), stoppingToken);
        }
    }
}

从源码结构看,BackgroundService 基类替你封装了 StartAsync/StopAsync 的编排与 IHostedService 接口适配,开发者只需关注业务循环,并且 stoppingToken 让优雅停机成为标配。这也印证了 projects/ihosted-service/README.md 中"BackgroundService 是 .NET 10 中简化的后台任务模式"的定位。

五、迁移收益对照

OUT-OF-DATE.md 的 Benefits 章节给出了迁移前后的量化对比:

维度 迁移前 迁移后
依赖 Swashbuckle.AspNetCore 内置(无需包)
OpenAPI 版本 3.0 3.1
AOT 兼容 否 是
代码简洁度 样板代码更多 XML 注释即可
性能 良好 更好(源码生成器)

需要说明的是:表中的"内置(无需包)"指不再需要 Swashbuckle/NSwag 这类第三方文档包,但仓库实际迁移后的示例仍引用 Microsoft.AspNetCore.OpenApi(框架能力包)与 Scalar.AspNetCore(文档 UI),例如 projects/minimal-api/map-4/map-4.csproj、projects/authentication/authentication-4/authentication-4.csproj 均为此组合。

六、保留的两个示例:为什么"未迁移"不代表"过时"

6.1 mvc/nswag-2:NSwag 备选方案

被标记为 LOW 优先级保留的 projects/mvc/nswag-2/Program.cs 展示了另一套文档组织思路:用 [Tags] 特性给控制器动作分组(Basic/Intermediate/Advanced),并用 [ApiExplorerSettings(IgnoreApi = true)] 隐藏内部端点。它作为"备选方案"保留,供需要区分文档分组的场景参考,而不是缺陷。

6.2 generic-host/generic-host-1/2:非 Web 场景依然有效

projects/generic-host/generic-host-1/Program.cs 与 projects/generic-host/generic-host-2/Program.cs 展示的是不依赖 Web 服务器的 Generic Host 用法:Host.CreateDefaultBuilder(args).ConfigureServices(...).Build().RunAsync()(或 RunConsoleAsync()),配合 IHostedService 承载后台任务。这类场景与 ASP.NET Core 10 的 Web 新特性无关,因此判定为"在非 Web 场景下仍然有效"而保留。仓库 projects/generic-host/README.md 也明确 Generic Host 适合消息处理、后台任务等长生命周期应用。

七、如何验证这些迁移

仓库 README.md 给出了统一的运行方式:进入示例目录后执行 dotnet watch run,例如:

cd projects/minimal-api/open-api-1
dotnet watch run

迁移后的效果验证路径:

  • 访问 /openapi/v1.json 查看内置 OpenAPI 生成的 JSON 文档;
  • 访问 /scalar 打开 Scalar 交互式 API 文档(projects/minimal-api/open-api-1/Program.cs 首页即提供这两个入口链接);
  • 需要 YAML 格式时参考 projects/net10/open-api-9/Program.cs 的 AddOpenApiDocument() 与 /openapi/{documentName}.yaml 路由;
  • SSE 示例通过浏览器打开 / 页面,观察 EventSource('/sse') 每秒推送的实时消息。

八、参考资源

小结

这份报告的价值在于它给出了一个可复用的升级方法论:当新版本框架把"第三方包能力"内置化时,逐示例排查 → 分类迁移(文档生成、实时推送、后台任务)→ 对仍适用于旧场景的示例按低优先级保留。迁移后的 12 个示例在仓库中均可以直接运行验证,其代码形态(open-api-1、sse、ihosted-service-1)即是你在自己的 .NET 10 项目里落地这三套新 API 的最佳范本。

登录后查看全文
practical-aspnetcore