@TOC
使用 .NET 9 的 OpenAPI 的新增功能
了解
.NET 9
中的新Microsoft.AspNetCore.OpenApi
包,并将其与NSwag
和Swashbuckle.AspNetCore
进行比较。
- 2024 年 9 月 9 日,作者:Martin Costello
- 原文地址,https://blog.martincostello.com/whats-new-for-openapi-with-dotnet-9/
多年来,.NET
生态系统中的开发人员一直在使用 ASP.NET
和 ASP.NET Core
编写 API
,而 OpenAPI
一直是记录这些 API
的热门选择。OpenAPI
的核心是一个机器可读的文档,用于描述 API
中可用的 Endpoint
(端点)。它不仅包含有关参数、请求和响应的信息,还包含其他元数据,例如属性描述、与安全相关的元数据等。
然后,Swagger UI
等工具可以使用这些文档,为开发人员提供用户界面,以便快速轻松地与 API
交互,例如在测试时。随着最近基于 AI
的开发工具的激增,OpenAPI
作为一种以机器可以理解的方式描述 API
的方式变得更加重要。
长期以来,在运行时为 ASP.NET Core
生成 API
规范的两个最常见的库是 NSwag
和 Swashbuckle
。这两个库都提供了允许开发人员从现有代码中以 JSON
和/或 YAML
格式为其 API
生成丰富的 OpenAPI
文档的功能。然后,可以通过不同方式(例如使用属性或自定义代码)来扩充端点,以进一步丰富生成的文档,从而为其使用者提供出色的开发人员体验。
随着 ASP.NET Core 9
的即将发布,ASP.NET
团队为现有的 Microsoft.AspNetCore.OpenApi NuGet
包引入了新功能,该功能提供了一种为 ASP.NET Core Minimal API
生成 OpenAPI
文档的新方法。
在这篇文章中,我们将了解新功能,并将其与现有的 NSwag
和 Swashbuckle
库进行比较,以了解它在功能和性能方面的比较。
- NSwag,https://github.com/RicoSuter/NSwag
- Swashbuckle,https://github.com/domaindrivendev/Swashbuckle.AspNetCore
- ASP.NET Core 9,https://learn.microsoft.com/aspnet/core/release-notes/aspnetcore-9.0
- Microsoft.AspNetCore.OpenApi NuGet,https://www.nuget.org/packages/Microsoft.AspNetCore.OpenApi
为什么使用新的 OpenAPI 库?
您可能想知道,当 ASP.NET Core
中有两种用于生成 OpenAPI
文档的现有且流行的解决方案时,为什么还需要第三个新选项来参与竞争。虽然 NSwag
和 Swashbuckle
多年来一直为社区提供良好的服务,但最近这两个库的维护和更新量都有所下降。这导致每个新版本在这些库中利用和/或支持框架新功能的能力滞后。
虽然 Swashbuckle
在 2024
年宣布了该项目的新维护者(我是其中之一👋),并且现在对 .NET 8
提供了一流的支持,但它仍然是一个开源项目,免费提供并由志愿者在业余时间维护。由于这些限制,很难通过每年发布新的主要版本来跟上 .NET
生态系统的变化步伐。相比之下,Microsoft
的 ASP.NET
团队全职从事框架工作是有报酬的,因此可以投入时间来确保他们提供的库随着产品的发展而与最新功能和最佳实践保持同步。
新库的另一个激励因素是,本机 AoT
编译正成为部署 .NET
应用程序的一种越来越流行的方式,尤其是在云中,减少冷启动时间对于具有可变负载模式的大规模应用程序非常重要。NSwag
和 Swashbuckle
都严重依赖反射来生成其 OpenAPI
文档,但是当在编译为本机代码运行的应用程序中使用反射时,反射有许多限制。这使得这些库中的许多现有 Code Pattern
无法正常工作,因为需要修剪掉元数据,因为它似乎未使用。
虽然这两个库都可以重构以支持原生 AoT
,但这将是一项大量的工作,因为它需要对两个库的核心功能进行大量重写。作为一个 Swashbuckle
维护者来说,与它提供的好处相比,所需的工作量是如此之大,以至于这在现实中是不可能的。
然而,一个从头开始设计的全新库以支持原生 AoT
编译和 ASP.NET Core
的最新功能是一个非常不同的主张。任何新库都不受其现有功能的负担,而是可以从更适合 ASP.NET Core
生态系统的当前状态及其 2024
年及以后的需求的新设计开始。
Swashbuckle
维护者也不关心现场有新的库。我们不认为它是 Swashbuckle
的竞争对手 - 例如,新库仅支持 ASP.NET Core 9
及更高版本,而 Swashbuckle
对旧版本的 ASP.NET Core
具有更广泛的支持,包括 .NET Framework
。欢迎想要使用新功能并希望迁移的用户这样做,但我们不会很快停止维护 Swashbuckle
。我相信许多开发人员对他们现有的库选择感到满意,并将继续使用它,而不是投入时间和精力从一个库迁移到另一个库。
Microsoft.AspNetCore.OpenApi Features (and Gaps)
概括地说,新的 Microsoft.AspNetCore.OpenApi
包具有与 NSwag
和 Swashbuckle
相同的基本功能。它会在运行时为您的 ASP.NET Core
终端节点生成一个 OpenAPI
文档。端点的形状(例如其方法、路径、请求、响应、参数等)都源自您的应用程序代码。可以使用元数据(例如属性,如 和 )扩展声明,以便为生成过程提供其他信息,以根据需要描述端点和架构。[ProducesResponseType][Tags]
该库还与现有的 Microsoft.Extensions.ApiDescription.Server
包集成,以在生成时通过自定义 MSBuild
目标生成文档,该目标可以作为编译项目的一部分运行,以将 OpenAPI
文档生成为磁盘上的文件。这对于 CI/CD
场景(如 linting
)非常有用 - 例如,您可以将 spectral
作为构建管道的一部分运行,以验证 OpenAPI
文档是否有效并遵循建议的最佳实践。
与 Swashbuckle
一样,该包构建在 OpenAPI.NET
库之上,该库为 OpenAPI
规范的各种基元提供 C#
类型。这样做的好处是,将来添加对 OpenAPI
规范新版本的支持(例如 OpenAPI 3.1
)应该更容易,因为可以更新库以使用将来支持它的新版本,只需更新从端点生成类型的“胶水”, 而不是还需要完全实现规范本身。模型的 JSON
架构的生成建立在 .NET 9
中的新 JSON
架构支持之上,该支持由新类公开。JsonSchemaExporter
在端点级别添加了 OpenAPI
支持(think
和类似方法)。这允许 OpenAPI
文档耦合到 ASP.NET Core
中的其他机制中,例如授权、缓存等。MapGet()
如前所述,它还与原生 AoT
完全兼容,如果您想在已部署环境中向用户公开 API
文档,则允许您在运行时为 ASP.NET Core
应用程序生成 OpenAPI
文档,即使编译为原生代码时(例如在容器中运行时)。
若要添加对生成 OpenAPI
文档的最低级别的支持,可以在添加对 Microsoft.AspNetCore.OpenApi NuGet
包的引用后,将以下代码添加到 ASP.NET Core
应用程序:
var builder = WebApplication.CreateBuilder();
// Add services for generating OpenAPI documents
builder.Services.AddOpenApi();
var app = builder.Build();
// Add the endpoint to get the OpenAPI document
app.MapOpenApi();
// Your API endpoints
app.MapGet("/", () => "Hello world!");
app.Run();
然后,运行服务器并在浏览器中导航到 URL
将返回一个 JSON
形式的 OpenAPI
文档,该文档描述应用程序中的端点。
/openapi/v1/openapi.json
Transformers / 转换器
如果(或当)您需要进一步丰富文档,该库会提供许多扩展点,您可以使用转换器(ransformers)的概念来扩展文档或单个操作和/或架构(schemas
)。转换器为您提供了一种运行自定义代码的方法,以便在生成 OpenAPI
文档时对其进行修改,从而允许您添加其他元数据。
转换器可以注册为内联委托(inline delegates
),也可以注册为实现相应转换器接口 (或 ) 的类型。对于接口,这允许您在实现中实现使用各种附加服务(例如 )的类型,并且意味着它们可以从应用程序使用的依赖项注入容器中解析。
IOpenApiDocumentTransformer
IOpenApiOperationTransformer
IOpenApiSchemaTransformer
IConfiguration
下面是一个声明然后使用文档转换器的示例:
// Add a custom service to the DI container
builder.Services.AddTransient();
// Add services for generating OpenAPI documents and register a custom document transformer
builder.Services.AddOpenApi(options =>
{
options.AddDocumentTransformer();
});
// A custom implementation of IOpenApiDocumentTransformer that uses our custom service.
// The type is activated from the DI container, so can use other services in the application.
class MyDocumentTransformer(IMyService myService) : IOpenApiDocumentTransformer
{
public async Task TransformAsync(
OpenApiDocument document,
OpenApiDocumentTransformerContext context,
CancellationToken cancellationToken)
{
// Use myService to modify the document in some way...
}
}
作为这些转换器功能的另一个示例,我在这些抽象之上构建了一个自己的库,以便为我自己的 API
添加其他功能。OpenAPI Extensions for ASP.NET Core
库提供了许多转换器,可用于向 OpenAPI
文档添加其他元数据,例如支持为请求、响应和架构生成丰富的示例。
- OpenAPI Extensions for ASP.NET Core,https://github.com/martincostello/openapi-extensions
Feature Gaps / 特征差异
然而,作为第一个版本,与 NSwag
和 Swashbuckle
相比,开发人员对 OpenAPI
解决方案的期望存在一些功能差距。
无用户界面
与早期版本的 ASP.NET Core
中 .NET SDK
附带的应用程序模板相比,没有内置解决方案可以在生成的 OpenAPI
文档之上呈现用户界面。
我认为这在现阶段并不是一个重大差距,因为仍然可以通过继续使用 Swashbuckle.AspNetCore.SwaggerUI NuGet
包来轻松地将 Swagger UI
添加到您的应用程序中。此 NuGet
包独立于 Swashbuckle
的其余部分,因此可以与新的 OpenAPI
库一起使用,而不会因包含两个实现而出现任何问题或膨胀。从 Swashbuckle.AspNetCore
版本 6.6.2
开始,此软件包还支持本机 AoT
,因此也不会影响对本机 AoT
的支持。
无 XML 注释
对于 .NET 9
版本,不支持从代码中的 XML
文档向 OpenAPI
文档添加说明。这是计划在未来版本(可能是 .NET 10
)中提供的功能,但预计该功能的预览版将在此之前的某个时候提供。
如果这对您的应用程序至关重要,您可以研究创建自己的转换器,以便在那之前使用您的 XML
文档。
不支持 YAML 文档
虽然 Microsoft.OpenApi
库和 NSwag
都支持在 YAML
中生成 OpenAPI
文档(与 Swashbuckle
不同),但 Microsoft.AspNetCore.OpenApi
包目前仅支持在 JSON
中生成 OpenAPI
文档。此功能可以在将来的版本中添加。
这又是我添加到 OpenAPI Extensions for ASP.NET Core
库中的另一项功能,因此如果需要,您可以使用它来生成 YAML
文档。它通过应用程序中的一行代码启用:
app.MapOpenApiYaml();
- OpenAPI Extensions for ASP.NET Core,https://github.com/martincostello/openapi-extensions
与 NSwag 和 Swashbuckle 的比较
那么,新的 Microsoft.AspNetCore.OpenApi
包与现有的 NSwag
和 Swashbuckle
库相比如何呢?
虽然该库的目标不是实现与任一现有库 100%
的功能对等,但它确实提供了开发人员期望从 ASP.NET Core
应用程序的 OpenAPI
库中获得的大多数相同功能。如上所述,核心差距是对 XML
注释和内置用户界面的支持。
如果您想更详细地比较这三个库,可以查看此 GitHub 存储库
,该存储库实现了 Todo API
,并使用所有三个库为其公开了等效的 OpenAPI
文档。这应该可以让您很好地了解这三个库如何表达相同的概念,以及您作为应用程序开发人员如何使用它们。
例如,下面是添加 OpenAPI
文档并在所有三个实现中自定义 API
信息的代码。
你会注意到的一件事是,自定义文档的相同能力是通过类似的概念完成的,这些概念被命名为 transformers (ASP.NET Core)、处理器 (NSwag) 或过滤器 (Swashbuckle)
。
Microsoft.AspNetCore.OpenApi
public static IServiceCollection AddAspNetCoreOpenApi(this IServiceCollection services)
{
services.AddOpenApi(options =>
{
options.AddDocumentTransformer((document, _, _) =>
{
document.Info.Title = "Todo API";
document.Info.Description = "An API for managing Todo items.";
document.Info.Version = "v1";
return Task.CompletedTask;
});
options.AddOperationTransformer(new AddExamplesTransformer());
});
return services;
}
NSwag
public static IServiceCollection AddNSwagOpenApi(this IServiceCollection services)
{
services.AddOpenApiDocument(options =>
{
options.Title = "Todo API";
options.Description = "An API for managing Todo items.";
options.Version = "v1";
options.OperationProcessors.Add(new AddExamplesProcessor());
});
return services;
}
Swashbuckle
public static IServiceCollection AddSwashbuckleOpenApi(this IServiceCollection services)
{
services.AddSwaggerGen(options =>
{
var info = new OpenApiInfo
{
Title = "Todo API",
Description = "An API for managing Todo items.",
Version = "v1"
};
options.SwaggerDoc(info.Version, info);
options.OperationFilter();
});
return services;
}
Performance / 性能
我认为在这篇博文中要谈的最后一件事是性能。在我创建了比较这三种实现的存储库后,我认为对它们进行基准测试以比较它们在生成 OpenAPI
文档时的性能会很有趣。
- Preliminary Results with .NET 9 Preview 7
- .NET 9 预览版 7 的初步结果
在绕道设置持续基准测试过程(在此处阅读)之后,我使用 BenchmarkDotNet
为每个库设置了一些基准测试,以比较性能。当我第一次设置它们时,我的目标是 .NET 9 的官方预览版 7,在非常高的层面上,我得到了这些结果:
BenchmarkDotNet v0.14.0, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores
.NET SDK 9.0.100-preview.7.24406.8
[Host] : .NET 9.0.0 (9.0.24.40507), X64 RyuJIT AVX2
ShortRun : .NET 9.0.0 (9.0.24.40507), X64 RyuJIT AVX2
Job=ShortRun IterationCount=3 LaunchCount=1
WarmupCount=3
| Method | Mean | Error | StdDev | Gen0 | Gen1 | Gen2 | Allocated |
|------------ |----------:|----------:|----------:|---------:|---------:|---------:|----------:|
| AspNetCore | 10.988 ms | 13.319 ms | 0.7301 ms | 171.8750 | 140.6250 | 125.0000 | 6.02 MB |
| NSwag | 12.269 ms | 2.276 ms | 0.1247 ms | 15.6250 | - | - | 1.55 MB |
| Swashbuckle | 7.989 ms | 6.878 ms | 0.3770 ms | 15.6250 | - | - | 1.5 MB |
- 在此处阅读,https://blog.martincostello.com/continuous-benchmarks-on-a-budget/
- BenchmarkDotNet,https://github.com/dotnet/BenchmarkDotNet
从数据中可以看出,新的 OpenAPI
包在性能方面与 NSwag
大致排在第二位,Swashbuckle
领先几毫秒。然而,新的 ASP.NET Core OpenAPI
在内存使用方面远远落后,使用的内存几乎是其他两个库的 4
倍。您还可以从下图中从 .net9 预览版 7
随时间推移的多次运行中看到,与其他两个更稳定的库相比,OpenAPI
包的性能存在很大差异。
- ASP.NET Core results for .NET 9 preview 7
- NSwag results for .NET 9 preview 7
- Swashbuckle results for .NET 9 preview 7
不是特别好,但这些基准测试实际上有两个有趣的警告。
首先是 ASP.NET Core 9 预览版 7
中存在 一个错误,导致 OpenAPI
文档架构在各代之间不稳定 - 这导致了大量不必要的工作完成,并导致内存泄漏,最终导致 OpenAPI
生成完全停止工作。由于这个问题,我不得不通过 限制基准测试作为短期运行的迭代次数,否则基准测试将陷入停顿。这也是分配编号(第一个图表顶部的红线)出现差异的原因。[ShortRunJob]
第二个警告是,默认情况下,NSwag
会缓存它生成的 OpenAPI
文档,因此开箱即用,它只会生成一次 OpenAPI
文档。为了进行比较,我在 NSwag
中 禁用了缓存,以便在每个请求中完整生成文档。我们可以通过缓存所有三个来向相反的方向平衡竞争环境,但这对于性能比较/测试来说并不有趣,因为我们实际上只是对缓存😄进行基准测试。
Gotta Go Fast 🦔💨 / 必须走得快
手头有一些数据后,我查看了代码的确切作用,看看是否有任何明显的问题可以修复或改进以加快速度。在这个过程中,EventPipe Profiler
非常宝贵,可以在 BenchmarkDotNet
中启用它来捕获正在执行的代码的火焰图。使用 speedscope.app
我能够可视化正在执行的代码路径,并查看时间花在了哪里。有了这些信息,我能够识别出 OpenAPI
生成在哪些地方执行了不必要的工作并导致了性能问题。
- EventPipe Profiler,https://benchmarkdotnet.org/articles/features/event-pipe-profiler.html
- speedscope.app,https://www.speedscope.app/
- BenchmarkDotNet,https://benchmarkdotnet.org/
Dictionary Lookups 🕵️📖 / 字典查找
我发现的第一件事是代码似乎在该方法中花费了大量时间。进一步深入研究,我注意到它与索引器一起在代码中的许多地方被使用。这是 .NET
中已知的性能陷阱,此模式会导致双重查找,这可以通过改用该方法来避免。
Enumerable.All()
IDictionary<K, V>.Contains()
TryGetValue()
事实上,甚至还有一个 .NET
分析规则涵盖此方案:CA1854
。事实证明,这个分析器中存在一个错误,它没有捕获某些使用模式,这就是它以前没有被捕获的原因。
将代码更改为 use
是一个很容易的更改,但这并没有回答为什么首先要花费这么多时间的问题。事实证明,这是由于 OpenAPI
库为用于生成 OpenAPI
文档的各种类型的 IEqualityComparer<T>
实现的方式。
TryGetValue()
All()
实现了一些自定义相等比较器,用于帮助测试不同的 OpenAPI
架构“形状”是否彼此相等。这些对象在某些情况下包含数十个属性,其中一些属性本身就是字典或数组,它们可以创建一个大型对象图来遍历以计算相等性。
通过根据费用/不同的可能性对属性的计算方式进行一些重新排序,可以避免这些比较的很多成本,并且在大多数情况下可以使事情变得更快。
我打开了 一个拉取请求
来解决这两个项目,合并后,所有已识别的方法调用都退出了基准测试🔥中分析器跟踪的热路径。
- CA1854,https://learn.microsoft.com/zh-cn/dotnet/fundamentals/code-analysis/quality-rules/CA1854
- a pull request,https://github.com/dotnet/aspnetcore/pull/57208
Too Many Transformers 🤖 / 转换器太多
在修复不稳定的架构和上述更改后,我再次查看了基准测试运行的跟踪,并从数据中发现了另一个异常。查看数据,我注意到 转换器的创建频率太高
了。
这是由于变压器的生命周期和处置问题造成的,这意味着每个架构创建一次变压器,而不是每代 OpenAPI
文档创建一次。这不仅会产生额外工作的开销,还会对内存使用和垃圾回收产生影响。
- transformers were being created too often,https://github.com/dotnet/aspnetcore/issues/57211
.NET 9 RC.1 的最新结果
合并上述问题的更改后,我从他们的 CI
中针对 .NET 9
的最新每日版本重新运行基准测试,因为在撰写本文时,.NET 9 RC.1
尚未正式发布。我之前写过 关于使用每日构建的文章,所以如果你感兴趣,可以看看那篇博文。
与 .net9 预览版 7
相比,使用 .NET 9 CI
中最新版本的 .NET SDK
(在撰写本文时),情况得到了显著改善:9.0.100-rc.1.24452.12
BenchmarkDotNet v0.14.0, Ubuntu 22.04.4 LTS (Jammy Jellyfish)
AMD EPYC 7763, 1 CPU, 4 logical and 2 physical cores
.NET SDK 9.0.100-rc.1.24452.12
[Host] : .NET 9.0.0 (9.0.24.43107), X64 RyuJIT AVX2
DefaultJob : .NET 9.0.0 (9.0.24.43107), X64 RyuJIT AVX2
| Method | Mean | Error | StdDev | Median | Gen0 | Allocated |
|------------ |-----------:|---------:|----------:|-----------:|--------:|-----------:|
| AspNetCore | 981.9 us | 15.94 us | 30.34 us | 975.3 us | - | 326.64 KB |
| NSwag | 4,570.8 us | 60.82 us | 53.92 us | 4,556.4 us | 15.6250 | 1588.43 KB |
| Swashbuckle | 2,768.2 us | 52.00 us | 124.58 us | 2,721.2 us | 15.6250 | 1527 KB |
正如你所看到的,与之前的结果相比,OpenAPI
包现在是三个库中最快的。
新的 ASP.NET Core
包在时间和内存方面都大大击败了 NSwag
和 Swashbuckle
。⚡
事实上,它比最接近的竞争对手快了近 ~2.8
倍,内存消耗减少了 ~4.6
倍。
与 .net9预览版 7
中的自身相比,它现在的速度提高了 ~11
倍,分配的内存减少了 ~18
倍。这是一个巨大的进步!🚀
ASP.NET Core results for .NET 9 RC1
NSwag results for .NET 9 RC1
- Swashbuckle results for .NET 9 RC1
此处需要注意的注意事项:
- [ShortRunJob]不再使用,因此基准测试会运行更多迭代,因此更准确。这就是为什么第二系列图形中的误差线要小得多的原因。
- 包括
.NET 9 预览版 7
和.NET9 RC候选版本 1
之间的所有改进,而不仅仅是OpenAPI
的修复。这一点从图表上从左侧几个点开始的所有三个库的主要向下步骤中最为明显。这是基准测试项目从使用.NET 9 预览版 7
切换到每日.NET9 RC1
版本的地方。
与往常一样,性能与使用的环境相关,您的数据可能会有所不同。但是,在相对稳定的环境(在本例中为 GitHub Actions 的 Ubuntu 运行程序)下,图表显示多次运行的性能一致,并且在使用较新版本的 .NET 9
时有明显的改进。这里的有用数据是趋势,而不是绝对值。
延伸阅读
有关 Microsoft.AspNetCore.OpenApi
包中新功能的更多信息,请查看 YouTube 上的这些 ASP.NET 社区站立流。在这里,这项新功能背后的工程师 Safia Abdalla 解释了软件包中的新功能以及如何在您的应用程序中使用它们:
- .NET 9 中的 OpenAPI 更新,https://www.youtube.com/watch/XoMese9g8WQ
- .NET 9 中的 OpenAPI 更新(第 2 部分),https://www.youtube.com/watch/keK69Y5HqvY
ASP.NET Core 9
的软件包文档可在 [Microsoft Learn](https://learn.microsoft.com/zh-cn/aspnet/core/fundamentals/openapi/aspnetcore-openapi?view=aspnetcore-9.0&tabs=visual-studio%2Cminimal-apis)
中找到。
总结
总而言之,新的 [ASP.NET Core OpenAPI](https://www.nuget.org/packages/Microsoft.AspNetCore.OpenApi/9.0.0-rc.2.24474.3)
软件包是对 ASP.NET Core
生态系统的重要补充。它提供了一种现代且高性能的方式来为您的 ASP.NET Core
应用程序生成 OpenAPI
文档,以涵盖开发人员所需的核心使用案例。
虽然它可能还不如 NSwag
或 Swashbuckle
等现有库功能丰富,但它现在和将来能够更好地跟上 ASP.NET Core
的步伐变化,例如对原生 AoT
的支持,这为它的未来发展奠定了坚实的基础,例如未来对 OpenAPI 3.1
的支持。
如果开发人员对当前的实现感到满意,则无需从现有库切换到新的 OpenAPI
包,唯一令人信服的切换理由是您希望在本机 AoT
部署中生成 OpenAPI
文档。对于那些确实希望切换的用户(我的许多应用程序都有),由于 Swashbuckle.AspNetCore
的用户都是建立在同一个 OpenAPI.NET
基础之上的,因此迁移是最容易的。
如果您之前没有将 OpenAPI
文档添加到 API
中,并且正在编写新的 ASP.NET Core 9+
应用程序,我建议您尝试一下该库,看看它如何满足您的需求。这是开始使用 API
的 OpenAPI
文档的好方法!