.NET Swagger 配置与拓展指南

简介

Swagger (OpenAPI) 是一种用于定义和描述 RESTful API 的语言无关规范。在 .NET 项目中,我们通常使用 Swashbuckle.AspNetCore 这个库来自动根据您的 C# 代码(包括控制器、模型和 XML 注释)生成 Swagger 文档,并提供一个美观、可交互的 UI 界面,极大地提升了 API 的开发、测试和文档化效率。


1. 基础配置:在项目中启用 API 文档

1.1. 开启 XML 文档生成并抑制警告

为了让 Swagger 能够读取您代码中的 /// 注释,必须先让项目在构建时生成一个 XML 文档文件。

方法一:通过 Visual Studio 界面 (推荐)

  1. 打开项目属性 : 在"解决方案资源管理器"中,右键点击您的项目 -> 属性
  2. 勾选文档文件 : 导航到 生成 (Build) -> 输出 (Output) ,勾选 "生成包含 API 文档的文件"
  3. 删除警告 : 导航到 生成 (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,或者未来有 ProductsV3ControllerUsersV3Controller 都对应 v3

  • 实现方式 : 使用 基于路由前缀 的分组。

  • 配置 : 在 AddSwaggerGen 中,我们使用 DocInclusionPredicate 来检查每个 Controller 的 [Route] 属性。

    csharp 复制代码
    options.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 将枚举显示为可读的字符串及其描述,而不是整数。

  • 实现 :
    1. 字符串转换 : 在 Program.cs 中配置 AddControllers().AddJsonOptions(...),添加 JsonStringEnumConverter
    2. 添加描述 : 为枚举及其每个成员添加 /// <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();

5. 官方文档

相关推荐
ChinaRainbowSea4 小时前
11. Spring AI + ELT
java·人工智能·后端·spring·ai编程
不会写DN4 小时前
用户头像文件存储功能是如何实现的?
java·linux·后端·golang·node.js·github
盖世英雄酱581364 小时前
FullGC排查,居然是它!
java·后端
Jagger_4 小时前
SOLID原则中的依赖倒置原则(DIP):构建可维护软件架构的关键
后端
Penge6664 小时前
为什么 Go 中值类型有时无法实现接口?—— 从指针接收器说起
后端
用户90555842148054 小时前
Milvus源码分析:向量查询(Search)
后端
间彧4 小时前
Java HashMap:链表工作原理与红黑树转换
后端
亚雷5 小时前
深入浅出达梦共享存储集群数据同步
数据库·后端·程序员
作伴5 小时前
多租户架构如何设计多数据源
后端