说实话我一开始对API文档这类事情没什么热情直到接了一个接口文档全靠Word维护的老项目才明白自动生成OpenAPI文档有多香。正好 .NET 9 把OpenAPI从第三方插件升级成了官方能力文档生成本身不用再纠结选什么框架真正纠结的反而是UI皮肤继续用大家都熟的Swagger还是跟风换成Scalar UI于是我用一个全新的Minimal API项目从0到1跑了一遍把“官方OpenAPI Swagger UI”和“官方OpenAPI Scalar UI”两条路都走完记录下所有配置、对比和坑这篇文章就是完整攻略。无论你是刚接触 .NET 9还是正想把老项目从Swashbuckle迁出来都应该能在这里找到可以直接抄走的答案。1. 为什么 .NET 9 的OpenAPI值得重新认识1.1 官方入场生态的底层逻辑变了在 .NET 9 之前ASP.NET Core 项目想生成一份像样的API文档基本只有两条路Swashbuckle.AspNetCore 或者 NSwag。这两种方案其实都不是微软官方出品虽然用得很广泛但总要面对同一个尴尬——框架升级一代第三方库就得跟着碰运气偶尔还会因为API变化打几个补丁。.NET 9 把Microsoft.AspNetCore.OpenApi这个包放到了官方位子上我终于可以只写一套服务注册框架自动扫描、映射、生成/openapi/v1.json这样的标准文档。注意它只负责“生成OpenAPI文档”并不强制你绑定某个UISwagger UI、Scalar UI、Redoc甚至自己写个前端展示都行。这个设计很聪明生成和展示解耦UI选择回归用户需求而不是被某个中间件捆绑。一套标准文档能同时喂给不同的UI这个事比想象中影响大。以前为了换UI往往要跟着换一整套生成器代码改不少。现在官方生成器固定了UI只是一个壳切换成本几乎为零。1.2 Swashbuckle代码真的需要立刻拆掉吗很多老项目目前的写法是这样的builder.Services.AddSwaggerGen(); app.UseSwagger(); app.UseSwaggerUI();这套组合拳在 .NET 9 里依然能跑Swashbuckle 并没有退役。但你要知道它同时承载了三件事生成OpenAPI文档、托管Swagger中间件、渲染UI页面。如果你只想要文档给Scalar UI用Swashbuckle 会多做不少无用功。我的建议是老项目不要急着推翻重写。可以先在Program.cs里同时注册官方OpenAPI和Swashbuckle给两者不同的文档路径灰度跑一段时间。等Scalar UI或自定义UI验证通过后再把Swashbuckle从依赖里摘出去。我在迁移一个订单服务时就是按这个节奏走的线上没有出现过一秒文档不可用。真正需要留意的有三个迁移差异点过滤器写法不同官方用IDocumentTransformer和IOpenApiOperationTransformerSwashbuckle 用的是IOperationFilter文档元数据不再靠 XML 注释自动拼装需要自己配置DocumentTransformer认证方案描述方式也有差异Swagger 的SecurityScheme在官方API里要用OpenApiSecurityScheme手动声明。这些一旦踩到排查起来都比较隐蔽建议提前列好对照表。2. Swagger UI 与 Scalar UI 的正面PK我把两个都装上试了一周2.1 开箱即用的第一印象Swagger UI 长什么样做 .NET 开发的应该都熟蓝色标题栏、接口列表折叠、点开后有Try it out按钮整体给人一种“这很开发者”的感觉。第一次打开Scalar UI我甚至以为自己打开了个现代化API网关产品页面左侧目录、右侧详情深色主题默认就挺好看操作按钮和代码块的排版明显更跟手。这不是谁劣谁优的问题而是产品定位差异。Swagger UI 追求的是在尽量简单的页面里把所有OpenAPI能力都暴露出来所以信息密度高、层级多Scalar UI 更在意阅读体验和交互反馈把常用操作做得更顺滑视觉上更像是给外部开发者看的产品文档。我拿同一个/openapi/v1.json分别喂给两个UIScalar 的响应展示和错误提示明显更直观尤其是字段多的大请求体折叠和层级渲染更清晰。Swagger 的响应体则是一整块JSON看得久了容易眼花。2.2 主题定制与品牌化能力Swagger UI 不是不能定制但默认的定制方式偏“硬核”要么改CSS变量要么往页面里注入自定义CSS/JS。如果只是想换个公司Logo和主色调工作量还能接受想把整个页面布局改成产品文档那种风格你得和它默认的HTML结构搏斗一轮。Scalar UI 在这一点上走得更远它把主题做成了配置项app.MapScalarApiReference(options { options.WithTitle(订单服务 OpenAPI); options.WithTheme(ScalarTheme.Mars); options.WithDarkModeToggle(true); options.WithDefaultHttpClient(ScalarTarget.CSharp, ScalarClient.HttpClient); });像Mars、Solarized这些内置主题一行代码就能切换不需要懂前端就能把文档风格和团队技术博客统一起来。对于要对外提供API文档的团队来说这个特性节省的不只是开发时间还有设计评审里反复抠细节的沟通成本。2.3 依赖大小和性能有没有差别我出于好奇对比了两个UI的静态资源体积。swagger-ui-dist整体包含CSS、JS和一堆字体压缩后传输量通常在数百KB级别Scalar.AspNetCore 把前端资源内嵌进 NuGet 包首次加载也会拉取对应的 JS/CSS 资源。但说句公道话对API文档这种低频访问页面几百KB并不会造成明显卡顿。真正会让文档页变慢的是把几十个接口、几十个Schema全部膨胀进一个OpenAPI JSON里。如果存在超长描述、大量示例UI再优化也没用。所以我更建议花时间控制文档体积而不是纠结UI静态资源那几十KB差距。2.4 关键能力对照表对比项Swagger UIScalar UI官方支持不是官方行为但生态最老牌独立开源项目更新活跃接入方式独立UI包或Swashbuckle聚合包独立 NuGet 包配置量小默认视觉信息密度高老派开发工具风现代化双栏布局默认支持暗色主题定制需要自定义CSS/JS内置多主题配置即切换Try It Out基础可用交互扎实更顺滑响应展示更友好代码示例生成不原生提供内建多语言代码片段学习资料大量老教程可参考资料相对少但官方文档清晰适合受众团队内部联调、老项目接盘对外开发者文档、新项目这张表不是用来证明谁一定赢而是帮你按场景选。像我们团队内部用的管理端APISwagger UI完全够但给外部客户看的订单查询APIScalar UI更拿得出手。3. 从0到1接入官方OpenAPISwagger和Scalar的完整落地过程3.1 环境准备和项目骨架先说明我的实验环境.NET 9 SDKWindows 11一个空壳Web项目。命令行操作如下dotnet --version # 9.0.x dotnet new web -n OpenApiDemo -f net9.0 cd OpenApiDemo dotnet add package Microsoft.AspNetCore.OpenApi dotnet add package Scalar.AspNetCore dotnet add package Swashbuckle.AspNetCore.SwaggerUI这里有个容易踩的坑如果只装Swashbuckle.AspNetCore.SwaggerUI它只负责渲染UI不会生成OpenAPI JSON所以必须配合官方的MapOpenApi()使用。如果你习惯装整套Swashbuckle.AspNetCore也可以但会用不上它的生成器纯属增加依赖。3.2 用官方OpenAPI生成文档在Program.cs里写最简配置using Microsoft.AspNetCore.OpenApi; var builder WebApplication.CreateBuilder(args); builder.Services.AddOpenApi(); var app builder.Build(); app.MapOpenApi(); app.MapGet(/orders/{id}, (int id) Results.Ok(new { Id id, ProductName 鼠标, Price 99.9 })); app.Run();运行起来后访问http://localhost:5000/openapi/v1.json就能看到标准的OpenAPI JSON。这时候还没有任何可视化页面但整个API的结构、路径、参数、响应Schema都已经在里面了。我给文档加个标题和描述这一步在给前端或外部团队用时非常关键builder.Services.AddOpenApi(options { options.AddDocumentTransformer((document, context, cancellationToken) { document.Info.Title 订单服务 API; document.Info.Version v1; document.Info.Description 包含订单查询、创建、取消等接口。; return Task.CompletedTask; }); });AddDocumentTransformer是 .NET 9 官方OpenAPI的核心扩展点有点类似以前Swashbuckle的DocumentFilter。官方默认的Info内容很简陋不配置的话UI页面上就只有一个标题v1对外不好看。3.3 接入Scalar UI添加完包以后在Program.cs里加一行using Scalar.AspNetCore; // ... 省略其他 app.MapScalarApiReference();默认访问路径是/scalar/v1页面会自动找/openapi/v1.json这个地址。如果项目路径做了统一前缀比如部署在https://api.example.com/docs下需要确认Scalar生成的URL引用是否正确。我实际操作时最喜欢的是它自动生成的代码片段。打开任意接口右侧会展示cURL、C#、JavaScript等示例前端同事拿去就能直接调试比Swagger的“手动拼参数再执行”要省事不少。3.4 接入Swagger UI只安装Swashbuckle.AspNetCore.SwaggerUI后在Program.cs里配置using Swashbuckle.AspNetCore.SwaggerUI; // ... 省略其他 app.UseSwaggerUI(options { options.SwaggerEndpoint(/openapi/v1.json, 订单服务 v1); });打开/swagger就能看到熟悉的界面。注意这里没有调用app.UseSwagger()因为我们用的是官方生成的JSON不需要Swashbuckle再生成一份。如果你同时把整个Swashbuckle包都装了又调用了UseSwagger()很可能出现/swagger/v1/swagger.json和/openapi/v1.json两个文档地址共存的情况。不是不能用但会让团队搞不清到底该以哪个为准。我建议一个项目只保留一个生成器UI随便换。3.5 两种UI共存和一键切换我实际验证过MapScalarApiReference()和UseSwaggerUI()可以同时存在互不干扰app.MapOpenApi(); app.UseSwaggerUI(options { options.SwaggerEndpoint(/openapi/v1.json, v1); }); app.MapScalarApiReference();这个组合适用于项目里有人用Swagger习惯、有人想尝鲜Scalar的过渡期。切换成本约等于零因为数据源是同一个/openapi/v1.json。4. 用OpenAPI文档驱动接口测试和客户端生成4.1 Swagger的Try It Out到底够不够用Swagger UI 的Try it out是很多团队引入接口文档的原因不用打开Postman浏览器里就能填参数、发请求、看响应。对于简单接口这个体验非常顺畅。但它也有让我不太舒服的地方。一是请求体一旦是深层嵌套对象手填JSON容易漏字段Swagger校验报错不够友好二是响应展示基本是纯文本JSON没有状态码和响应头分开展示排查问题时要开开发者工具才能看到完整信息。好在这些都是细节内部联调完全能忍。4.2 Scalar在接口测试上做了什么不一样Scalar UI 的请求发送器明显是照着现代API客户端做的响应状态码和耗时直观展示响应体自动高亮还支持收起/展开长JSON字段。更友好的地方是它默认展示“代码示例”不管你前端用什么技术栈基本都能找到对应的请求片段。我在给一个Vue项目联调时前端同事第一次用Scalar就说比Swagger顺手。原因是看了cURL和Fetch示例就可以直接复制到浏览器控制台验证不需要理解OAuth2授权跳转那一套复杂的UI流程。4.3 从OpenAPI文档生成SDK有多香OpenAPI文档最大的隐藏价值其实是喂给代码生成器自动生成强类型客户端。我常用的有两条一是 Kiotadotnet tool install --global Microsoft.OpenApi.Kiota kiota generate -d openapi.json -l CSharp -c HttpClient -o ./GeneratedClient -n Demo.Client二是 NSwagdotnet tool install --global NSwag.ConsoleCore nswag openapi2csclient /input:openapi.json /classname:OrderClient /namespace:Demo.Client /output:OrderClient.cs生成完以后C#代码里直接new OrderClient(httpClient)就能调用接口省掉大量手写HttpClient样板代码。这类工具最依赖的是文档质量。如果你的接口没有明确标注nullable、响应状态码、错误结构生成出来的SDK就会反复出现类型坑。所以我会建议先用官方OpenAPI把文档打磨好再让生成器去读文档。5. 上线前必须处理的OpenAPI安全配置别让文档变成攻击地图5.1 先问自己这个文档需要公网可见吗我见过不少团队把Swagger UI打开到生产环境理由是“方便排查问题”。这是非常危险的习惯。公开的/swagger/index.html、/openapi/v1.json、/scalar/v1等于把你的接口路径、参数、模型字段、枚举值全部送给扫描器。攻击者根本不用猜接口照着文档一点一点测试就行。最简单的防御是分环境控制if (app.Environment.IsDevelopment()) { app.MapOpenApi(); app.MapScalarApiReference(); app.UseSwaggerUI(options { options.SwaggerEndpoint(/openapi/v1.json, v1); }); }如果你的服务会部署到多个环境建议用配置项控制而不是硬编码IsDevelopmentvar exposeOpenApi builder.Configuration.GetValuebool(OpenApi:Expose); if (exposeOpenApi) { app.MapOpenApi(); app.MapScalarApiReference(); app.UseSwaggerUI(options { options.SwaggerEndpoint(/openapi/v1.json, v1); }); }这样就只剩appsettings.Development.json里配置OpenApi:Expose true生产环境不配置或显式false。5.2 “未授权访问”漏洞的常见入口与处置思路很多安全扫描器会例行扫/swagger/index.html、/v1/swagger.json、/openapi/v1.json一旦命中就报“Swagger API 未授权访问漏洞”。这个漏洞原理其实很简单文档入口没有做任何身份认证任何匿名用户都能拉到完整接口定义。我在排查一个旧系统时遇到过类似问题处置链路大概是这样的先确认暴露面访问公网域名下的/swagger和/openapi/v1.json确认是否真的可访问。检查反向代理规则看是服务本身开的口子还是网关裸转进来的。临时方案在代理层把这两个路径匹配到内网IP或加IP白名单。根治方案按环境开关关闭文档暴露同时让运维监控公网访问日志里对.json和/swagger的探测。复查重新跑一次路径扫描确保返回不再是200。这里我不展开具体的利用方式因为有价值的是防御思路。OpenAPI文档本身是开发效率工具不是必须暴露给所有人的资源。如果团队确实需要远程查看文档正确做法是把它放到内部开发者平台或者加上SSO身份认证。5.3 在OpenAPI文档里隐藏敏感接口和字段有时候我们不能一刀切关闭文档因为联调还要用。那至少要把敏感接口和内部字段藏起来。官方OpenAPI支持ShouldInclude过滤器builder.Services.AddOpenApi(options { options.ShouldInclude api { // 不以 /debug 开头的接口才进文档 return !api.RelativePath?.StartsWith(/debug/) ?? true; }; });对于Schema里的敏感字段比如Password、IdCard、InternalRemark可以用文档Transformer动态移除options.AddSchemaTransformer((schema, context, cancellationToken) { if (schema.Properties is null) return Task.CompletedTask; foreach (var key in schema.Properties.Keys.ToList()) { if (key.Contains(password, StringComparison.OrdinalIgnoreCase) || key.Contains(internal, StringComparison.OrdinalIgnoreCase)) { schema.Properties.Remove(key); } } return Task.CompletedTask; });这样处理之后文档描述的信息是“给外部协作方看的最小子集”既不影响联调又不至于把内部数据库字段暴露干净。5.4 记录一次线上文档泄露的排查链路我印象很深的一次是客户发来一份扫描报告里面标红了一个/swagger/index.html路径。当时我们查了很多地方都找不到是谁把这个服务暴露出去的最后发现是Nginx配置里写了一条location / { proxy_pass http://service; }的泛匹配规则把所有子路径都转发了出去。那一次之后我给自己定了几条规矩文档路径尽量用非默认路径比如/api-docs/v1.json降低被扫描器直接命中的概率。在网关层统一拦截/swagger、/scalar、/openapi、/api-docs这些模式无论背后服务怎么配置公网都进不来。服务里再设一道开关双重保险。每次上新前用漏洞扫描器或简单脚本过一遍公开路径确认文档没有裸奔。这些操作不复杂但在真实项目里能救你很多次。6. 实测结论与我的选型建议6.1 什么场景下继续选Swagger UI如果你的团队已经用Swagger用了很多年大家肌肉记忆都形成了我建议不要为了追新而换。Swagger UI虽然不够现代化但胜在稳定、认知成本低、资料齐全。尤其是做企业内部后台管理系统文档是给后端和测试看的Swagger UI完全能完成任务。还有一类场景适合Swagger项目还停留在Swashbuckle生成文档的旧架构中短时间内没有精力迁移。这时候安装Swashbuckle.AspNetCore.SwaggerUI对接官方OpenAPI至少能降低对旧生成器的依赖UI过渡期也更平滑。6.2 什么场景下值得换成Scalar UI新项目是Scalar UI最好的舞台。尤其当你需要把API文档开放给前端团队、外部开发者或者作为产品的一部分Scalar UI能给使用者更好的第一印象。它的暗色主题、多语言代码片段、响应展示细节都更接近商业级API文档平台。如果你正在做一个对外提供API的服务我甚至建议把Scalar UI作为默认入口再配一个Swagger UI作为备用因为有些老客户可能只认Swagger的交互方式。6.3 踩过几次坑之后我现在的固定做法现在的固定做法是官方AddOpenApi()生成文档Scalar UI作为主UISwagger UI按环境开关作为辅助。安全上强制分环境公开环境一律不暴露文档内网环境走网关白名单。每次发布后我会手动访问一次/openapi/v1.json确认生成器没被新代码打崩。如果你只想一条建议带走别再纠结Swagger和Scalar谁更强先统一用.NET 9官方OpenAPI生成文档然后按你团队的使用习惯选UI。生成和展示分离以后这个选择题已经没那么重要了重要的是文档必须和代码同步、并且永远不该是黑客的免费情报。