简介
Swagger (OpenAPI) 是一种用于定义和描述 RESTful API 的语言无关规范。在 .NET 项目中,我们通常使用 Swashbuckle.AspNetCore
这个库来自动根据您的 C# 代码(包括控制器、模型和 XML 注释)生成 Swagger 文档,并提供一个美观、可交互的 UI 界面,极大地提升了 API 的开发、测试和文档化效率。
1. 基础配置:在项目中启用 API 文档
1.1. 开启 XML 文档生成并抑制警告
为了让 Swagger 能够读取您代码中的 ///
注释,必须先让项目在构建时生成一个 XML 文档文件。
方法一:通过 Visual Studio 界面 (推荐)
- 打开项目属性 : 在"解决方案资源管理器"中,右键点击您的项目 -> 属性。
- 勾选文档文件 : 导航到 生成 (Build) -> 输出 (Output) ,勾选 "生成包含 API 文档的文件"。
- 删除警告 : 导航到 生成 (Build) (顶级菜单),在右侧找到 "错误和警告" 分组,在 "禁止显示特定警告" 输入框中添加
1591
。这个警告码代表"缺少对公共可见类型或成员的 XML 注释"。
方法二:手动编辑 .csproj
文件
打开您的项目 .csproj
文件,在 <PropertyGroup>
中添加以下两行:
xml
<PropertyGroup>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
1.2. 配置 Swagger 读取 XML 文件
在 Program.cs
中,告诉 Swashbuckle 去哪里找到并加载上一步生成的 XML 文件。
csharp
// In: Program.cs
using System.Reflection;
// ...
builder.Services.AddSwaggerGen(options =>
{
// 定义 XML 文件路径并包含它
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFilename);
options.IncludeXmlComments(xmlPath);
});
2. 核心用法:编写高质量的 API 注释
所有注释都必须以 ///
(三个斜杠) 开头。
2.1. 为 Controller 和 Action 添加注释
这是最常见的场景,用于描述您的 API 接口。
csharp
/// <summary>
/// V3版本API,提供文件处理、历史记录等高级功能。
/// </summary>
[ApiController]
[Route("v3")]
public class Image3Controller : ControllerBase
{
/// <summary>
/// 获取指定路径的高清原始图片或视频文件。
/// </summary>
/// <remarks>
/// ## 详细说明
/// 此终结点作为安全代理,用于从服务器本地文件系统读取媒体文件并将其作为文件流返回。
/// - 支持多种图片和视频格式。
/// - 路径参数必须经过 URL 编码。
/// </remarks>
/// <param name="path">媒体文件的绝对路径。</param>
/// <returns>一个表示媒体文件的文件流。</returns>
/// <response code="200">成功,返回文件流。</response>
/// <response code="400">请求无效,例如路径包含 ".." 等非法字符。</response>
/// <response code="404">未找到指定路径的文件。</response>
[HttpGet("image")]
public IActionResult GetImage([FromQuery] string path)
{
// ...
}
}
2.2. XML 注释标签详解
标签 | 作用 | 在 Swagger UI 中的位置 |
---|---|---|
<summary> |
核心摘要。对 API、模型或参数的简短、单行描述。 | API 的标题行;模型字段的旁边。 |
<remarks> |
详细说明。可以包含多行文本、列表甚至 Markdown,用于提供更丰富的上下文。 | API 展开后的 "Implementation Notes" 或描述区域。 |
<param name="..."> |
参数描述。用于描述一个方法的参数。 | "Parameters" 表格中的 "Description" 列。 |
<returns> |
返回值描述。对方法成功返回值的通用描述。 | "Responses" 部分的 "Description" 列。 |
<response code="..."> |
特定状态码的响应描述。可以为每个 HTTP 状态码(200, 404, 500 等)提供具体的描述。 | "Responses" 部分,按状态码分组显示。 |
<example> |
示例值。为模型属性提供一个示例值。 | "Schema" 视图和请求体示例中。 |
2.3. 为 Model (DTO) 添加注释
为数据传输对象(DTO)添加注释,能让前端开发者清晰地理解每个字段的含义。
csharp
/// <summary>
/// 定义一个文件复制请求的结构体。
/// </summary>
public class CopyRequest
{
/// <summary>
/// 复制操作的目标根路径。
/// </summary>
/// <example>E:\Photos\Archive</example>
public string To { get; set; }
/// <summary>
/// 本次复制的文件夹后缀名。
/// </summary>
/// <example>京都之旅</example>
public string Name { get; set; }
}
3. Swagger 的高级拓展
本节将深入探讨 Swashbuckle 提供的一些高级配置,让您的 API 文档更具交互性、可读性和专业性。
3.1. API 版本控制 (多场景详解)
在 Swagger UI 中通过下拉菜单管理 v1, v2, v3 等不同版本的 API 是一项核心需求。
场景 A & B: 每个 Controller 对应一个版本 / 多个 Controller 对应同一版本
这是最常见和最直接的场景,例如 Image3Controller
对应 v3
,或者未来有 ProductsV3Controller
和 UsersV3Controller
都对应 v3
。
-
实现方式 : 使用 基于路由前缀 的分组。
-
配置 : 在
AddSwaggerGen
中,我们使用DocInclusionPredicate
来检查每个 Controller 的[Route]
属性。csharpoptions.DocInclusionPredicate((docName, apiDesc) => { // ... (获取 controllerRoute 的逻辑) ... switch (docName) { case "v3": // 只要 Controller 的路由以 "v3" 开头,就将其归入 "v3" 文档 return controllerRoute != null && controllerRoute.StartsWith("v3"); // ... 其他版本 ... } });
-
优势: 配置简单,符合 RESTful 的 URL 版本化风格,能够自动、动态地包含所有符合命名规范的 Controller。
场景 C: 同一个 Controller 包含多个不同版本的接口
这是一个更高级的场景,要求版本控制的粒度精确到单个 Action (接口)。
-
实现方式 : 使用
[ApiExplorerSettings(GroupName = "...")]
特性。这是 ASP.NET Core 官方推荐的、用于在工具中对 API 进行分组的标准方式。 -
代码示例:
csharp[ApiController] [Route("api/data")] // Controller 路由可以不带版本号 public class MixedDataController : ControllerBase { /// <summary>获取旧版数据 (V1)</summary> [HttpGet("legacy")] [ApiExplorerSettings(GroupName = "v1")] // <-- 明确指定此接口属于 "v1" 组 public IActionResult GetDataV1() { return Ok("This is V1 data"); } /// <summary>获取新版数据 (V2)</summary> [HttpGet("current")] [ApiExplorerSettings(GroupName = "v2")] // <-- 明确指定此接口属于 "v2" 组 public IActionResult GetDataV2() { return Ok("This is V2 data"); } }
-
优势: 极度灵活,与 URL 结构完全解耦,是实现复杂版本策略的最佳实践。
3.2. 添加认证 (Authorization) 支持
在 Swagger UI 中添加"Authorize"按钮,方便测试需要 JWT Bearer Token 等认证的接口。
- 实现 : 在
AddSwaggerGen
中使用AddSecurityDefinition
定义安全方案,并用AddSecurityRequirement
将其全局应用。
3.3. 自定义模型的显示 (SchemaFilter
)
通过编程方式修改生成的模型结构 (Schema),例如添加只读标记。
- 实现 : 创建一个实现
ISchemaFilter
接口的类,并在AddSwaggerGen
中通过options.SchemaFilter<YourFilterClass>()
注册它。
3.4. 自定义 API 的行为 (OperationFilter
)
修改每个 API 终结点(Operation)的细节,最经典的用法是为文件上传接口 (IFormFile
) 自动生成一个专用的上传按钮。
- 实现 : 创建一个实现
IOperationFilter
接口的类,并在AddSwaggerGen
中通过options.OperationFilter<YourFilterClass>()
注册它。
3.5. 设置测试默认值
在 Swagger UI 的参数输入框中预填一个值,方便快速测试,但不影响代码的实际默认值。
- 实现 : 在 Controller 的 Action 方法的参数上,添加
[System.ComponentModel.DefaultValue("你的默认值")]
特性。
3.6. 优化枚举 (Enum) 显示
让 Swagger 将枚举显示为可读的字符串及其描述,而不是整数。
- 实现 :
- 字符串转换 : 在
Program.cs
中配置AddControllers().AddJsonOptions(...)
,添加JsonStringEnumConverter
。 - 添加描述 : 为枚举及其每个成员添加
/// <summary>
XML 注释。
- 字符串转换 : 在
4. 可复用的 SwaggerExtensions.cs
模块化模板
这个模板将所有 Swagger 配置抽离到一个文件中,并使用依赖注入实现了动态联动,方便您在任何新项目中复用。
步骤 1: 创建 Extensions/SwaggerExtensions.cs
文件
csharp
using Microsoft.AspNetCore.Mvc;
using Microsoft.OpenApi.Models;
using Swashbuckle.AspNetCore.SwaggerGen;
using System.ComponentModel;
using System.Reflection;
namespace YourProjectName.Extensions // <-- 修改为你的项目命名空间
{
/// <summary>
/// 用于在 DI 容器中传递 Swagger 版本配置的服务。
/// </summary>
internal class SwaggerVersioningOptions
{
public List<OpenApiInfo> Versions { get; } = new();
}
/// <summary>
/// 包含所有 Swagger 配置的扩展方法。
/// </summary>
public static class SwaggerExtensions
{
/// <summary>
/// 注册所有 Swagger 相关的服务。
/// </summary>
public static IServiceCollection AddSwaggerServices(this IServiceCollection services)
{
// 创建一个对象来保存版本信息,并将其注册为单例服务
var versioningOptions = new SwaggerVersioningOptions();
services.AddSingleton(versioningOptions);
services.AddSwaggerGen(options =>
{
// --- 按需启用或注释掉以下功能模块的调用 ---
// 模块1: 配置 API 的基本信息和版本控制
// 如果注释掉这行,UseSwaggerWithUI 会自动回退到单版本模式
AddVersioning(options, versioningOptions);
// 模块2: 配置 JWT Bearer Token 认证支持
// AddAuthorization(options);
// 模块3: 添加自定义过滤器 (例如:文件上传、只读属性等)
// AddCustomFilters(options);
// 模块4: 配置 XML 注释支持 (基础功能,强烈建议保留)
AddXmlComments(options);
});
return services;
}
/// <summary>
/// 配置 Swagger 和 SwaggerUI 中间件。
/// </summary>
public static WebApplication UseSwaggerWithUI(this WebApplication app)
{
// 从 DI 容器中获取版本配置
var versioningOptions = app.Services.GetRequiredService<SwaggerVersioningOptions>();
app.UseSwagger();
app.UseSwaggerUI(options =>
{
// 动态联动:如果定义了多个版本,则为每个版本创建 UI 入口
if (versioningOptions.Versions.Any())
{
foreach (var versionInfo in versioningOptions.Versions.OrderByDescending(v => v.Version))
{
options.SwaggerEndpoint($"/swagger/{versionInfo.Version}/swagger.json", versionInfo.Title);
}
}
else
{
// 回退方案:如果没有配置版本,则显示一个默认的单版本入口
options.SwaggerEndpoint("/swagger/v1/swagger.json", "API V1");
}
options.RoutePrefix = "swagger";
});
return app;
}
#region --- 私有配置方法 ---
/// <summary>
/// 配置 API 版本信息和分组规则。
/// </summary>
private static void AddVersioning(SwaggerGenOptions options, SwaggerVersioningOptions versioningOptions)
{
// 定义版本信息
var versions = new[]
{
new OpenApiInfo { Version = "v3", Title = "API V3", Description = "最新版本接口" },
new OpenApiInfo { Version = "v2", Title = "API V2", Description = "第二版接口" },
new OpenApiInfo { Version = "v1", Title = "API V1", Description = "旧版接口" }
};
foreach (var version in versions)
{
options.SwaggerDoc(version.Version, version);
versioningOptions.Versions.Add(version); // 将版本信息存入共享服务
}
options.DocInclusionPredicate((docName, apiDesc) =>
{
// 方案1: 基于 [ApiExplorerSettings(GroupName = "...")] 特性 (推荐)
if (apiDesc.TryGetMethodInfo(out var methodInfo))
{
var groupName = methodInfo.GetCustomAttributes<ApiExplorerSettingsAttribute>()
.Select(attr => attr.GroupName).FirstOrDefault();
if (groupName != null)
{
return groupName == docName;
}
}
// 方案2: 基于路由前缀作为回退 (适用于 Controller 级别的版本控制)
var controllerRoute = apiDesc.ActionDescriptor.EndpointMetadata
.OfType<RouteAttribute>()
.FirstOrDefault()?.Template;
return docName switch
{
"v3" => controllerRoute != null && controllerRoute.StartsWith("v3"),
"v2" => controllerRoute != null && controllerRoute.StartsWith("v2"),
"v1" => controllerRoute != null && !controllerRoute.StartsWith("v2") && !controllerRoute.StartsWith("v3"),
_ => false
};
});
}
/// <summary>
/// 配置 JWT Bearer Token 认证。
/// </summary>
private static void AddAuthorization(SwaggerGenOptions options)
{
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
Type = SecuritySchemeType.ApiKey,
Scheme = "Bearer",
BearerFormat = "JWT",
In = ParameterLocation.Header,
Description = "请输入 Bearer Token, 格式为: Bearer {你的Token}"
});
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference { Type = ReferenceType.SecurityScheme, Id = "Bearer" }
},
new string[] {}
}
});
}
/// <summary>
/// 添加自定义的 SchemaFilter 和 OperationFilter。
/// </summary>
private static void AddCustomFilters(SwaggerGenOptions options)
{
options.OperationFilter<FileUploadOperationFilter>();
options.SchemaFilter<ReadOnlySchemaFilter>();
}
/// <summary>
/// 配置 XML 注释文件的加载。
/// </summary>
private static void AddXmlComments(SwaggerGenOptions options)
{
var xmlFilename = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFilename);
if (File.Exists(xmlPath))
{
options.IncludeXmlComments(xmlPath);
}
}
#endregion
}
#region --- 自定义过滤器实现 ---
/// <summary>
/// 一个 OperationFilter, 用于为接受一个或多个 IFormFile 的 Action 生成文件上传 UI。
/// </summary>
public class FileUploadOperationFilter : IOperationFilter
{
public void Apply(OpenApiOperation operation, OperationFilterContext context)
{
var fileParams = context.MethodInfo.GetParameters()
.Where(p => p.ParameterType.IsAssignableTo(typeof(IFormFile)) ||
p.ParameterType.IsAssignableTo(typeof(IEnumerable<IFormFile>)));
if (!fileParams.Any()) return;
var schemaProperties = new Dictionary<string, OpenApiSchema>();
foreach (var param in fileParams)
{
var schema = new OpenApiSchema { Type = "string", Format = "binary" };
if (param.ParameterType.IsAssignableTo(typeof(IEnumerable<IFormFile>)))
{
schema = new OpenApiSchema { Type = "array", Items = schema };
}
schemaProperties[param.Name ?? "file"] = schema;
}
operation.RequestBody = new OpenApiRequestBody
{
Content =
{
["multipart/form-data"] = new OpenApiMediaType
{
Schema = new OpenApiSchema
{
Type = "object",
Properties = schemaProperties
}
}
}
};
}
}
/// <summary>
/// 一个 SchemaFilter, 用于为被 [ReadOnly(true)] 特性标记的属性添加 "readOnly" 标记。
/// </summary>
public class ReadOnlySchemaFilter : ISchemaFilter
{
public void Apply(OpenApiSchema schema, SchemaFilterContext context)
{
if (schema?.Properties == null) return;
var readOnlyProperties = context.Type.GetProperties()
.Where(prop => prop.GetCustomAttribute<ReadOnlyAttribute>()?.IsReadOnly == true);
foreach (var property in readOnlyProperties)
{
var propertyName = char.ToLowerInvariant(property.Name[0]) + property.Name.Substring(1);
if (schema.Properties.TryGetValue(propertyName, out var schemaProperty))
{
schemaProperty.ReadOnly = true;
}
}
}
}
#endregion
}
步骤 2: 在 Program.cs
中使用模板
csharp
using YourProjectName.Extensions; // <-- 1. 引入扩展方法的命名空间
using System.Text.Json.Serialization;
var builder = WebApplication.CreateBuilder(args);
// ... 其他服务 ...
builder.Services.AddControllers().AddJsonOptions(options =>
{
// 配置枚举以字符串形式显示
options.JsonSerializerOptions.Converters.Add(new JsonStringEnumConverter());
});
// 2. 一行代码完成所有 Swagger 服务注册
builder.Services.AddSwaggerServices();
var app = builder.Build();
if (app.Environment.IsDevelopment())
{
// 3. 一行代码完成所有 Swagger 中间件配置
app.UseSwaggerWithUI();
}
// ... 其他中间件 ...
app.Run();