ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

.NET Core集成Swagger:从安装配置到XML注释自动生成接口文档

2026/10/2 3:12:40 拓冰建站 浏览量
.NET Core集成Swagger:从安装配置到XML注释自动生成接口文档 1. API文档之痛从手工维护到Swagger自动生成1.1 手工维护接口文档的真实困境先说说我为什么会对这个话题这么上心。前几年接手的项目里后端是有完整接口文档的Word文档几十页写了每个接口的地址、参数、返回示例。刚接手时还挺欣慰觉得这项目文档齐全。可真到了联调阶段就发现文档里的返回字段跟代码里实际返回的完全对不上——有人改了返回结构只加了注释没动文档有人新增了接口忘了往文档里补更别提那种文档里写着A代码里已经改成B前端照着文档调了半天发现报错的经典场面。这种问题的根源其实不在团队执行力而是手工文档天生就存在一个致命的弱点它会过期。代码是活的文档是死的。代码每提交一次文档的可靠性就打一次折。到后期前端同学干脆不看文档了直接跑过来问那个某某接口现在返回啥字段后端同学放下手头的活去翻代码。联调效率低不说新人上手更是灾难——光是把文档和代码对应起来就要花好几天。后来引入了Swagger这个问题基本就根治了。因为Swagger的核心思路是让接口文档从代码里直接长出来。你在控制器里写的方法签名、路由特性、参数Model、返回类型Swagger把这些全部读出来实时生成一份交互式文档。前端拿到的不再是一份可能会过期的Word而是跟当前运行版本完全一致的接口说明书。1.2 Swagger不是单一工具而是一套规范生态很多刚接触的人会把Swagger等同于那个蓝色背景的UI页面这个理解不算错但不全对。Swagger其实分好几层最底层是OpenAPI规范以前叫Swagger Spec这是一份JSON/YAML格式的接口描述文件里面按paths、definitions、parameters这些维度把整个API讲得一清二楚。第二层是基于这份描述文件的各种工具其中就有我们最常用的Swagger UI——那个可以直接在浏览器里展开接口、填写参数、点击Try it out发请求的交互页面。.NET Core里用的官方组件是Swashbuckle.AspNetCore它做的事情可以简化成三步启动时扫描你程序集中的Controller和Action把它们翻译成一份OpenAPI JSON文档然后加载这份JSON渲染成UI页面最后把UI页面挂在某个路径下供浏览器访问。整个过程完全自动化不需要你额外去写一行接口描述。我第一次在ASP.NET Core项目里跑通Swagger时最大的感受就是天亮了。前端同学再也不需要跑来问字段后端同学也不用隔三差五去同步文档。更爽的是Swagger UI上每一个接口都可以直接在页面上发起真实请求——输入参数、点执行、看响应调试阶段很多问题我自己在Swagger里就能复现根本不用把前后端拉到一起排查。2. 最小集成实战三行代码让Swagger跑起来2.1 安装Swashbuckle.AspNetCore包版本的选择思路在.NET Core时代集成Swagger已经非常简单几乎不需要手动配置OpenAPI的JSON结构。第一步是给项目安装NuGet包。在类库项目的依赖上右键管理NuGet程序包搜索Swashbuckle.AspNetCore直接安装最新稳定版即可。这里有个版本选择上的细节值得说。如果你用的是.NET 6以上的版本直接用最新版Swashbuckle.AspNetCore完全没问题它可以支持到OpenAPI 3.0。但如果你的项目还停留在.NET Core 3.1或者.NET 5建议选装6.x或5.x等对应时期的版本不要盲追新版否则可能出现程序集加载不兼容或者运行时行为不一致的问题。怎么确认自己装的是哪个版本打开.csproj文件看PackageReference节点就行。ItemGroup PackageReference IncludeSwashbuckle.AspNetCore Version6.5.0 / /ItemGroup提示Swashbuckle.AspNetCore是一个聚合包它已经包含了Swashbuckle.AspNetCore.SwaggerGen生成文档、Swashbuckle.AspNetCore.SwaggerUI渲染界面、Swashbuckle.AspNetCore.Newtonsoft可选JSON支持等子包正常项目装这一个就够了不需要手动拆着装。2.2 Program.cs里的标准三件套安装完成后改动集中在Program.cs。这是ASP.NET Core 6引入的最小托管模型跟以前Startup.cs时代写法不完全一样但逻辑是等价的。第一步在builder.Services区域注册Swagger生成器var builder WebApplication.CreateBuilder(args); builder.Services.AddControllers(); // 注册Swagger生成器 builder.Services.AddEndpointsApiExplorer(); builder.Services.AddSwaggerGen(); var app builder.Build();第二步在请求管道里启用Swagger中间件和UIif (app.Environment.IsDevelopment()) { app.UseSwagger(); app.UseSwaggerUI(); } app.UseAuthorization(); app.MapControllers(); app.Run();AddSwaggerGen()负责收集API元数据并生成OpenAPI文档UseSwagger()负责把生成的JSON文档暴露出来默认路由是/swagger/v1/swagger.jsonUseSwaggerUI()负责把交互页面挂到/swagger路径下。三者缺一不可少了任何一个要么看不到页面要么页面里没有任何接口数据。2.3 跑起来之后怎么验证一切正常按下F5运行项目浏览器地址栏手动输入http://localhost:端口号/swagger正常情况下你会看到经典的Swagger UI首页顶部显示Swagger UI - v1之类的标题下方按Controller分组的接口列表。验证的时候不能只看页面有没有出来要看三个关键点。第一接口是否完整列出——每个Controller下的每个Action都应该出现数量和代码里对应得上。第二点击任意一个接口展开里面应该能看到Parameters区域如果有参数的话和Responses区域。第三点右上角的Try it out按钮输入参数后点Execute页面会返回HTTP状态码和响应体——这说明Swagger不光能看还能真的发请求。这里顺便说一下第一次跑的时候最容易遇到的坑启动报错500 Internal Server Error或者页面出来了但所有接口都显示No operations defined in spec。前者八成是Swagger版本与运行时版本不匹配后者要么是AddEndpointsApiExplorer()没写要么是API没有添加控制器路由特性。把Controller改成标准的特性路由[Route(api/[controller])]能解决绝大多数No operations问题。3. 让接口注释自动生成XML文档完整配置3.1 注释文件从哪来三步开启XML生成很多开发者做到上面那步就收工了觉得自己会了Swagger。但真实项目中Swagger最大价值在于注释自动生成——把C#代码里的///注释直接变成前端能看到的接口说明。没有这一步Swagger页面上所有接口都光秃秃的只显示路径参数没有说明返回码没有描述前端依然要靠猜。要让注释生效得先让编译器生成XML注释文档。在项目文件里按下图配置Project SdkMicrosoft.NET.Sdk.Web PropertyGroup TargetFrameworknet8.0/TargetFramework GenerateDocumentationFiletrue/GenerateDocumentationFile NoWarn$(NoWarn);1591/NoWarn /PropertyGroup /ProjectGenerateDocumentationFile设为true后编译时VS会扫描代码里的XML注释并生成一个项目名.xml文件通常输出在bin/Debug/net8.0/目录下。NoWarn;1591的意思是代码里有未添加注释的方法时不要报警告——如果你没有加这一行每次编译会刷出一大片CS1591警告看着心烦但对实际运行没影响。很多人到了这一步就卡住了XML文件确实生成了但Swagger页面上依然没有注释。原因很简单——你还得告诉Swagger去读这个XML文件。在AddSwaggerGen里补上builder.Services.AddSwaggerGen(options { // 找到生成的XML注释文件名 var xmlFile ${Assembly.GetExecutingAssembly().GetName().Name}.xml; var xmlPath Path.Combine(AppContext.BaseDirectory, xmlFile); options.IncludeXmlComments(xmlPath); });Assembly.GetExecutingAssembly()获取当前程序集的名字拼上.xml就是注释文件的默认名。AppContext.BaseDirectory是运行时基目录即编译输出目录。这种方式比硬编码路径更稳妥不管项目发布到哪都能正确定位XML文件。3.2 XML注释的书写规范三斜杠注释的魔法XML注释文件生成出来后里面会自动提取你代码中所有///开头注释的文本。Swagger读取后会按以下规则映射到UI的对应位置C#代码位置注释写法Swagger UI中的显示位置Controller类上方/// summary接口分组名下方的描述区域不默认显示需额外配置用Action方法上方/// summary接口名称下方的大段描述文字Action方法上方/// remarks接口详情中额外展示的说明区域方法的普通参数/// param nameid参数列表里该参数旁边的说明Action返回值上方/// returns接口详情Responses区域配合状态码显示举个完整例子/// summary /// 根据ID查询用户信息 /// /summary /// param nameid用户主键ID大于0/param /// returns返回指定ID对应的用户详情若不存在则返回null/returns [HttpGet({id})] [ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)] [ProducesResponseType(StatusCodes.Status404NotFound)] public async TaskActionResultUserDto GetById(int id) { // ... }在Swagger UI里展开这个接口你会看到Parameters区域显示id (integer) – 用户主键ID大于0Responses区域显示200和404两种状态码接口名下面有一行根据ID查询用户信息的描述文字。前端拿到这个文档几乎不需要后端再口头解释任何东西。3.3 模型与Dto的字段注释前端最刚需的部分接口注释只是第一层真正让前端感激涕零的是Model字段的自动说明。没有这层注释Swagger页面上展示的请求JSON体长这样{ id: 0, userName: string, roleId: 0, createTime: 2024-01-01T00:00:00 }加上注释之后变成{ id: 0, // 用户ID主键 userName: string, // 登录账号唯一 roleId: 0, // 角色ID关联角色表 createTime: 2024-01-01T00:00:00 // 创建时间默认当前时间 }这个效果不需要改任何配置只需要给你的Dto类加上XML注释/// summary /// 创建用户请求模型 /// /summary public class CreateUserRequest { /// summary /// 登录账号系统内唯一长度为4-20个字符 /// /summary [Required] [StringLength(20, MinimumLength 4)] public string UserName { get; set; } /// summary /// 角色ID需预先在角色表中存在 /// /summary public int RoleId { get; set; } }但要注意一个细节如果请求Model被放在了独立的类库项目里比如Application.Dto这种层光配置Web项目的XML文件是不够的——Swagger只加载当前程序集的XML。这时候需要再把类库生成的XML文件也Include进来builder.Services.AddSwaggerGen(options { var xmlFile ${Assembly.GetExecutingAssembly().GetName().Name}.xml; var xmlPath Path.Combine(AppContext.BaseDirectory, xmlFile); options.IncludeXmlComments(xmlPath); // 如果Model在Common.Entities等类库中额外引入该类库的XML var dtoXmlPath Path.Combine(AppContext.BaseDirectory, Common.Entities.xml); if (File.Exists(dtoXmlPath)) { options.IncludeXmlComments(dtoXmlPath); } });如果忘了这步会出现一种诡异的状况接口的参数个数对但字段一点说明都没有你翻遍了Controller的注释也找不到原因。其实问题不在Controller而在那个沉默的Model类库。3.4 DataAnnotations对注释的补充作用除了XML注释ASP.NET Core的模型验证特性DataAnnotations也会影响Swagger的表现。[Required]、[StringLength]、[Range]这些特性会被Swagger读取在UI上标注参数是否必填。我自己的做法是双保险XML注释负责描述这个字段是什么意思DataAnnotations负责描述这个字段有什么约束。两者结合前端看到的参数说明几乎能替代后端口头沟通/// summary /// 用户姓名 /// /summary [Required(ErrorMessage 用户姓名不能为空)] [StringLength(50, ErrorMessage 用户姓名长度不能超过50)] public string Name { get; set; }这段代码生成的Swagger参数信息里会同时出现用户姓名这个注释、必填标记、长度限制。前端照着这个文档去写校验规则准确率非常高。4. 生产环境要解决的问题分组、安全与UI定制4.1 按模块或版本拆分文档分组项目一变大所有Controller挤在一个Swagger页面里就变得不方便了。几十个接口混在一起前端想找订单相关的接口得一个一个翻。我的经验是按领域或按版本拆分成多个分组每个分组有自己的标签页互不干扰。Swagger分组在AddSwaggerGen里配置核心是DocInclusionPredicate和多个SwaggerDoc的组合。这里直接给一个常见的多版本分组方案builder.Services.AddSwaggerGen(options { options.SwaggerDoc(v1, new OpenApiInfo { Version v1, Title 用户中心API, Description 面向Web前端的用户相关接口, }); options.SwaggerDoc(v2, new OpenApiInfo { Version v2, Title 订单中心API, Description 面向移动端的订单相关接口, }); // 按Controller的命名空间或特性来分组 options.DocInclusionPredicate((docName, apiDescription) { // 比如根据Controller名称前缀分组 if (docName v1) return apiDescription.RelativePath.StartsWith(user); if (docName v2) return apiDescription.RelativePath.StartsWith(order); return false; }); });这里有一点特别说明DocInclusionPredicate的匹配逻辑要跟你的[Route(api/[controller])]配合。我上面的判断方式是看路由前缀实际项目中也可以自定义一个Attribute标记在Controller上然后在这里通过apiDescription.ActionDescriptor.EndpointMetadata去判断。分组粒度怎么定取决于你们前端的组织方式——我遇到过有公司按前端团队来分组一个团队一个Swagger入口合作边界非常清晰。在UI端配置终端节点app.UseSwaggerUI(options { options.SwaggerEndpoint(/swagger/v1/swagger.json, 用户中心V1); options.SwaggerEndpoint(/swagger/v2/swagger.json, 订单中心V2); });这样Swagger UI顶部会多一个下拉框可以切换查看不同分组的文档。4.2 接口鉴权让Swagger UI能直接携带Token调试很多系统用了JWT认证。没配置Swagger的鉴权信息之前你只能在Swagger页面里看接口一调用就返回401。实测下来的正确做法是在AddSwaggerGen里把安全定义加上options.AddSecurityDefinition(Bearer, new OpenApiSecurityScheme { Description JWT授权头格式Bearer {token}, Name Authorization, In ParameterLocation.Header, Type SecuritySchemeType.ApiKey, Scheme Bearer }); options.AddSecurityRequirement(new OpenApiSecurityRequirement { { new OpenApiSecurityScheme { Reference new OpenApiReference { Type ReferenceType.SecurityScheme, Id Bearer } }, new Liststring() } });配置完之后Swagger UI右上角会出现一个Authorize按钮。点进去填写Token之后在这个页面里发的每个请求都会自动带上Authorization请求头前端联调的时候不用再拿Postman单独设置Header效率提升非常明显。重点提示Type SecuritySchemeType.ApiKey配合Name Authorization只是很多团队采用的常见写法它本质上是告诉Swagger这个接口需要带Authorization头。如果你的项目用的是OAuth2或者Cookie认证安全定义的类型和参数完全不一样需要按实际方案调整。不要看见网上代码直接抄要明白这行配置表达的是什么意思。4.3 环境隔离Swagger该不该在生产环境开放这个话题在团队里永远有争论。有的团队喜欢在生产环境也开放Swagger方便排查线上问题有的团队出于安全考虑一律只在开发/测试环境开放。我的立场很明确Swagger是个调试利器但它同时会把你项目的所有接口结构、参数、字段全部暴露给任何能访问到这个地址的人这无异于把API设计蓝图贴在了大门上。推荐的折中方案是生产环境不开放但留一个内部调试通道。通过环境变量或者配置文件控制开关var isSwaggerEnabled builder.Configuration.GetValuebool(EnableSwagger); if (isSwaggerEnabled) { app.UseSwagger(); app.UseSwaggerUI(); }然后在appsettings.Production.json里设EnableSwagger: false在appsettings.Development.json里设true。这样既能保证安全又保留了按需开启的能力。如果你确实需要生产环境用Swagger排查问题也至少应该套一层IP白名单或内网访问控制而不是裸奔在公网上。4.4 UI主题给Swagger换一层皮Swagger默认的UI风格偏开发者工具黑乎乎的头部加上密密麻麻的接口列表给前端看问题不大但如果你要把这个文档发给非技术角色比如项目经理、产品经理评审界面就略显粗糙了。官方其实支持替换UI主题。最简单的方式是从NuGet装一个Swashbuckle.AspNetCore.SwaggerUI的扩展包或者用第三方的UI库比如SwaggerUIBundle的定制CSS。不过我的建议是一般团队没必要在这一步花太多时间功能优先。如果你真想定制核心思路是给app.UseSwaggerUI(options { options.InjectStylesheet(/swagger-ui/custom.css); })注入一份自定义CSS把背景色、字体、Logo换一换就够了。真正提升体验的还是4.1节说的分组和4.2节的鉴权配置UI皮相是锦上添花不是雪中送炭。5. 踩坑实录我实际项目里遇到的五类问题5.1 注释文件生效了但Controller注释就是不出来这种情况最常见的场景是XML文件明明存在、IncludeXmlComments也写了、Controller上方也用///写了summary但页面上Controller名下方永远是空的。排查链路的第一个节点是打开bin目录下的XML文件看看里面到底有没有生成Controller的注释节点。如果你用的是ASP.NET Core WebAPI模板Controllers目录默认不会生成XML节点——因为文档注释默认只覆盖public类型和成员而Controller类如果没加public关键字编译器就认为它不需要进文档。我遇到过一次就是因为Controller写成了internal class WeatherControllerSwagger的UI能从路由上找到它但XML里没有它的注释节点。解法很简单把Controller改成public class重新编译XML里就有了对应的member nameT:命名空间.控制器节点Swagger页面上的描述立刻出现。5.2 发布到服务器后Swagger页面404本地开发一切正常发布到IIS或者Docker后访问/swagger直接404。这通常不是Swagger配置的问题而是静态文件中间件或者虚拟路径的问题。在IIS部署场景下如果站点有虚拟目录UseSwaggerUI的SwaggerEndpoint路径要拼上虚拟目录名app.UseSwaggerUI(options { options.SwaggerEndpoint(/myapp/swagger/v1/swagger.json, 用户中心V1); });如果是Docker部署要注意容器内监听端口与外部映射端口的区别Swagger UI在尝试加载JSON的时候用的是相对路径一般不会出问题但如果你在页面上手动改过URL容易把不该加的端口段加进去。这类问题的排查思路永远是从Swagger的JS控制台看起——按F12查看网络请求看swagger.json请求返回了什么状态码。404了就往路由前缀方向查500了就往程序集加载方向查。5.3 动态加载的程序集里接口注释丢失如果你的项目用了插件化架构通过Assembly.Load动态加载业务模块的程序集那么IncludeXmlComments默认配置对动态程序集是不生效的——因为你在AddSwaggerGen时写的XML路径是基于Web项目的程序集名动态程序集的XML注释文件不会被加载。处理办法是在加载动态程序集的代码位置把对应程序集的XML路径也加入到Swagger的注释加载列表中。可以在AddSwaggerGen的action里遍历某个已知目录下的所有XML文件var pluginXmlFiles Directory.GetFiles(pluginDirectory, *.xml); foreach (var file in pluginXmlFiles) { options.IncludeXmlComments(file); }注意要过滤掉那些不是注释文件、但恰好以.xml结尾的配置文件比如log4net.xml否则Swagger在读文件时会抛异常。5.4 Swagger暴露让整个API裸奔的风险这个坑不是遇到的而是我见过太多团队踩上去。Swagger UI是个很方便的工具但它同时也是攻击者的情报中心。攻击者拿到你的接口列表、字段定义、鉴权方式之后能低成本的构造出针对性的渗透测试请求。前面4.3节说了环境隔离这里再强调几个实操要点。如果你真的要在非开发环境开放Swagger至少做到第一不要把Swagger挂在根目录换一个非常规的路径比如/swg-doc减少被扫描到的概率第二加上鉴权可以是基础认证Basic Auth或者一个简单的IP白名单中间件第三如果用的是API网关在网关层面直接屏蔽对Swagger相关路径的转发。记住一点Swagger UI是开发调试工具不是生产功能。它的便利与风险并存别让这份便利变成事故的源头。5.5 路由冲突导致Swagger页面重复展示同名接口.NET Core中如果同一个路由匹配到了多个Action启动时会直接抛异常AmbiguousMatchExceptionSwagger页面上也会展示重复的路径节点。这个问题在Swagger集成前可能被忽略因为RESTful API有时候会用HTTP方法区分不同的Action但Swagger生成文档时如果方法特性[HttpGet]、[HttpPost]缺少明确的[Route]约束就可能出现两个Action都响应GET /api/user的情况。解决方案是给每个Action都写明子路由[HttpGet({id:int})] public async TaskActionResultUserDto GetById(int id) [HttpGet({name})] public async TaskActionResultListUserDto GetByName(string name)这样/api/user/1和/api/user/zhangsan在Swagger里就是两个不同的路径不会冲突。如果你希望更规范化也可以直接为不同操作设计不同路由前缀比如[Route(api/user/profile)]。5.6 UI界面能打开但接口列表空白的隐蔽原因最后再补充一个隐蔽问题Swagger UI能打开、界面也正常但下方显示No operations defined in spec。前面说过检查AddEndpointsApiExplorer是最常见的解法但还有一个容易忽略的场景你把Controller写成了普通类而不是继承ControllerBase或者类上没有加[ApiController]特性。[ApiController]这个特性有两个作用一是开启API行为特性自动ModelState验证、自动绑定等二是标记该类是一个API控制器Swagger在做扫描的时候对它的识别会更积极。如果项目里混用了老式MVC控制器和WebAPI控制器建议用[ApiController]把API控制器显式标记出来Swagger生成会更稳定。另外提醒一下如果你用.NET 7以上版本并且启用了Microsoft.AspNetCore.OpenApi包它和Swashbuckle存在一定的功能重叠两者都开时偶发文档异常。项目里二选一就行混用没必要。6. 从能用走向好用几个值得做的增强实践6.1 在Swagger里直接调试文件上传接口前后端联调时文件上传接口永远是沟通成本最高的类型——前端不知道要传什么格式后端不知道要怎么接。结合Swagger你可以用IFormFile作为参数类型/// summary /// 上传用户头像 /// /summary /// param namefile图片文件支持jpg/png/param /// returns上传成功后的文件URL/returns [HttpPost(upload-avatar)] [RequestSizeLimit(5 * 1024 * 1024)] public async TaskActionResultstring UploadAvatar(IFormFile file) { // ... }Swagger UI对IFormFile类型会自动渲染成一个文件选择框。测试时直接选文件、点Execute就能看到真实的上传结果。这个体验比Postman构造form-data请求好得多也方便后端自测。6.2 用示例值让请求JSON开箱即用前端在Swagger上测试接口时经常对着一个空JSON结构不知道怎么填。你可以通过给Model的属性加[Example]Swashbuckle的注释扩展或者给AddSwaggerGen配置SchemaFilter来注入示例值。最轻量的做法是加一个名为Swashbuckle.AspNetCore.Annotations的扩展包在Action上直接用特性[SwaggerRequestExample(typeof(CreateUserRequest), typeof(CreateUserRequestExample))] [SwaggerResponseExample(StatusCodes.Status200OK, typeof(UserDtoExample))] public async TaskActionResultUserDto Create(CreateUserRequest request)示例值的作用是降低联调门槛前端在页面里一点就能自动填入一份合法的请求体改几个字段就能调通。对复杂嵌套的请求模型这个配置带来的效率提升非常显著。6.3 把Swagger导出给别的工具用Swagger不只是在浏览器里看。因为它在运行时会产出一份标准的swagger.json你还可以把这个JSON导出后导入到Apifox、Postman、YApi等平台让接口文档在各个工具间流转。导出方式很简单浏览器打开/swagger/v1/swagger.json复制全部内容在目标工具中选择导入OpenAPI/Swagger格式即可。我团队的实际流程是这样的日常开发调试用Swagger UI每周五把swagger.json导出同步到API协作平台给不常写代码的测试同学用。整个过程不需要额外开发纯靠Swagger的标准格式能力。7. 写在最后我用Swagger四年后的几条心得先纠正一个心态问题Swagger不是给你自己看的是给前端、测试、新同事看的。所以配置的时候多站在使用者的角度想——他们需要什么他们需要准确的字段说明需要能一键填好的示例值需要不需要反复问就能自己跑的鉴权配置。我早期配置Swagger时只图自己看得方便分组、注释、示例都没弄结果前端依然天天来问。后来花了一个下午把注释补全、分组理顺、鉴权加上之后一个月都没人来找我问接口的事。再提醒一个操作习惯每次写完接口先不要急着发给前端自己在Swagger UI里先点一遍。手点的过程中你会自然发现很多问题——参数类型不对、校验规则没生效、返回结构跟预期不一致。这比前端来报告Bug再修复要划算得多。最后说说我踩过最深刻的一次坑吧。有一次升级Swashbuckle.AspNetCore版本从5.x升到6.x结果所有接口的注释全部消失了。排查了半天才发现新版本要求IncludeXmlComments必须在AddSwaggerGen里显式声明旧版本会自动扫描同目录XML文件的行为在新版被移除了。这种升级带来的行为变更非常隐蔽所以升级第三方包之后一定要先跑一轮接口文档的自检流程别以为版本升级就只是版本号变了一下。Swagger这个工具功能边界其实很清晰——它就是做好接口描述这件事。正因为它简单可靠才值得每个.NET Core项目标配。至于怎么把它用好让注释自动生成、让文档保持新鲜、让前端用得顺手就在今天写到的这些细节里。