practical-aspnetcore 的 ASP.NET Core 10 示例过时清理报告:从 Swashbuckle/NSwag 到内置 API 的三大迁移实践
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 的覆盖面:
- RouteGroupBuilder:projects/minimal-api/map-group-2/Program.cs 中,
app.MapGroup("/about").MapAboutApi().WithTags("about_apis").WithDescription(...)直接用内置 API 描述分组;projects/minimal-api/map-group-3/Program.cs 则展示了用.ExcludeFromDescription()将首页、非 API 端点从 OpenAPI 文档中剔除的用法。 - MVC 控制器:projects/mvc/nswag/Program.cs 展示了控制器场景下
AddControllers() + AddOpenApi()的组合,控制器动作上的<response code="200">注释直接成为文档内容。 - 完整 Web 应用:projects/mini/minimal-api-pokedex/src/Minimal.Api.Pokedex/Program.cs 中
AddOpenApi()与MapOpenApi()、MapScalarApiReference()配合MapPokedexApiRoutes()为整个宝可梦图鉴 API 生成文档。 - 认证场景:projects/authentication/authentication-4/Program.cs 在
AddJwtBearer()认证之上叠加内置 OpenAPI,说明 JWT 保护下的 API 同样可以无缝接入。
三、迁移模式二: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 在真实实现中比报告示例多了两个值得注意的点:
Accept头校验:只有客户端携带Accept: text/event-stream时才返回 SSE 流,否则返回Results.BadRequest;- 命名事件类型:
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')每秒推送的实时消息。
八、参考资源
- OUT-OF-DATE.md:本报告原文,含完整迁移清单与代码对比
- projects/net10/README.md:.NET 10 特性示例索引(内置 OpenAPI、内置 SSE)
- projects/net10/open-api-8 / open-api-9:内置 OpenAPI 的 XML 注释与 YAML 输出示例
- projects/net10/sse-2 / sse-3 / sse-4:内置 SSE 的递进示例
- projects/ihosted-service/README.md:BackgroundService 与 IHostedService 两种后台任务模式的定位说明
小结
这份报告的价值在于它给出了一个可复用的升级方法论:当新版本框架把"第三方包能力"内置化时,逐示例排查 → 分类迁移(文档生成、实时推送、后台任务)→ 对仍适用于旧场景的示例按低优先级保留。迁移后的 12 个示例在仓库中均可以直接运行验证,其代码形态(open-api-1、sse、ihosted-service-1)即是你在自己的 .NET 10 项目里落地这三套新 API 的最佳范本。