Swagger / OpenAPI 接口文档
大约 8 分钟约 2473 字
Swagger / OpenAPI 接口文档
简介
Swagger(更准确地说是 OpenAPI)不仅是"自动生成接口文档"的工具,更是前后端协作、接口测试、契约治理和 API 可发现性的基础设施。对于 ASP.NET Core 项目来说,真正有价值的不是把 /swagger 页面跑起来,而是让文档和代码同步、错误响应清晰、安全方案明确、版本管理稳定。
OpenAPI 规范与 Swagger 的关系
OpenAPI 规范(OAS):
- API 描述的标准格式(JSON/YAML)
- 定义路径、操作、参数、响应、安全方案等
- 版本:OAS 2.0(Swagger)、OAS 3.0、OAS 3.1
Swagger 生态:
- Swagger Editor — 在线编辑 OpenAPI 文档
- Swagger UI — 渲染可交互的 API 文档页面
- Swashbuckle — ASP.NET Core 的 OpenAPI 实现
工具链:
代码 → Swashbuckle → OpenAPI JSON → Swagger UI(人类阅读)
→ NSwag/AutoRest → 前端 SDK(自动生成)
→ Spectral/Lint → 契约验证
→ Postman/Insomnia → 接口调试特点
实现
基础接入与 XML 注释
# NuGet 包安装
dotnet add package Swashbuckle.AspNetCore
dotnet add package Swashbuckle.AspNetCore.Annotations<!-- 项目文件 — 启用 XML 文档生成 -->
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<!-- 启用 XML 文档文件生成 -->
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<!-- 忽略未注释的公共成员警告 -->
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
</Project>// ============================================
// Program.cs — Swagger 完整配置
// ============================================
using Microsoft.OpenApi.Models;
using System.Reflection;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddControllers();
builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen(options =>
{
// API 文档基本信息
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "SunnyFan API",
Version = "v1",
Description = "SunnyFan 知识库后端接口文档",
Contact = new OpenApiContact
{
Name = "SunnyFan",
Email = "sunnyfancore@163.com",
Url = new Uri("https://github.com/sunnyfan")
},
License = new OpenApiLicense
{
Name = "MIT",
Url = new Uri("https://opensource.org/licenses/MIT")
}
});
// 读取 XML 注释
var xmlFile = $"{Assembly.GetExecutingAssembly().GetName().Name}.xml";
var xmlPath = Path.Combine(AppContext.BaseDirectory, xmlFile);
options.IncludeXmlComments(xmlPath, includeControllerXmlComments: true);
// 启用注解支持([SwaggerOperation] 等)
options.EnableAnnotations();
// 自定义操作 ID(避免同名方法冲突)
options.CustomOperationIds(apiDesc =>
{
return apiDesc.ActionDescriptor.RouteValues?
.GetValueOrDefault("action")?.ToString()
?? apiDesc.HttpMethod;
});
// 按标签排序
options.OrderActionsBy((apiDesc) => $"{apiDesc.GroupName}_{apiDesc.HttpMethod}_{apiDesc.RelativePath}");
});
var app = builder.Build();
// 开发环境启用 Swagger
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "SunnyFan API v1");
options.RoutePrefix = "swagger"; // 访问路径:/swagger
options.DocumentTitle = "SunnyFan API 文档";
options.DisplayRequestDuration(); // 显示请求耗时
});
}
app.MapControllers();
app.Run();Controller / DTO 注释增强
/// <summary>
/// 用户管理接口
/// </summary>
[ApiController]
[Route("api/users")]
[Produces("application/json")]
public class UsersController : ControllerBase
{
private readonly IUserService _userService;
public UsersController(IUserService userService)
{
_userService = userService;
}
/// <summary>
/// 获取用户列表
/// </summary>
/// <remarks>
/// 示例请求:
///
/// GET /api/users?page=1&pageSize=10&keyword=zhang
///
/// </remarks>
/// <param name="page">页码,从 1 开始</param>
/// <param name="pageSize">每页数量,最大 100</param>
/// <param name="keyword">搜索关键词(可选)</param>
/// <returns>分页用户列表</returns>
/// <response code="200">成功返回用户列表</response>
/// <response code="400">参数错误</response>
/// <response code="401">未认证</response>
[HttpGet]
[ProducesResponseType(typeof(PagedResult<UserDto>), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(StatusCodes.Status401Unauthorized)]
public async Task<ActionResult<PagedResult<UserDto>>> GetAsync(
[FromQuery, Range(1, int.MaxValue)] int page = 1,
[FromQuery, Range(1, 100)] int pageSize = 10,
[FromQuery,MaxLength(50)] string? keyword = null)
{
var result = await _userService.GetPagedAsync(page, pageSize, keyword);
return Ok(result);
}
/// <summary>
/// 获取用户详情
/// </summary>
/// <param name="id">用户 ID</param>
/// <returns>用户详情</returns>
/// <response code="200">成功返回用户信息</response>
/// <response code="404">用户不存在</response>
[HttpGet("{id:int}")]
[ProducesResponseType(typeof(UserDto), StatusCodes.Status200OK)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status404NotFound)]
public async Task<ActionResult<UserDto>> GetByIdAsync(int id)
{
var user = await _userService.GetByIdAsync(id);
if (user == null) return NotFound(new { error = "用户不存在" });
return Ok(user);
}
/// <summary>
/// 创建用户
/// </summary>
/// <param name="request">创建用户请求体</param>
/// <returns>创建的用户信息</returns>
/// <response code="201">创建成功</response>
/// <response code="400">参数验证失败</response>
/// <response code="409">用户名已存在</response>
[HttpPost]
[ProducesResponseType(typeof(UserDto), StatusCodes.Status201Created)]
[ProducesResponseType(typeof(ValidationProblemDetails), StatusCodes.Status400BadRequest)]
[ProducesResponseType(typeof(ProblemDetails), StatusCodes.Status409Conflict)]
public async Task<ActionResult<UserDto>> CreateAsync([FromBody] CreateUserRequest request)
{
var user = await _userService.CreateAsync(request);
return CreatedAtAction(nameof(GetByIdAsync), new { id = user.Id }, user);
}
/// <summary>
/// 删除用户
/// </summary>
/// <param name="id">用户 ID</param>
/// <response code="204">删除成功</response>
/// <response code="404">用户不存在</response>
[HttpDelete("{id:int}")]
[ProducesResponseType(StatusCodes.Status204NoContent)]
[ProducesResponseType(StatusCodes.Status404NotFound)]
public async Task<IActionResult> DeleteAsync(int id)
{
await _userService.DeleteAsync(id);
return NoContent();
}
}/// <summary>
/// 创建用户请求
/// </summary>
public class CreateUserRequest
{
/// <summary>
/// 用户名(3-20 个字符)
/// </summary>
/// <example>zhangsan</example>
[Required(ErrorMessage = "用户名不能为空")]
[StringLength(20, MinimumLength = 3, ErrorMessage = "用户名长度为 3-20 个字符")]
public string UserName { get; set; } = string.Empty;
/// <summary>
/// 邮箱地址
/// </summary>
/// <example>zhangsan@example.com</example>
[Required(ErrorMessage = "邮箱不能为空")]
[EmailAddress(ErrorMessage = "邮箱格式不正确")]
public string Email { get; set; } = string.Empty;
/// <summary>
/// 密码(至少 8 位,包含大小写字母和数字)
/// </summary>
/// <example>Password123</example>
[Required]
[StringLength(100, MinimumLength = 8)]
[RegularExpression(@"^(?=.*[a-z])(?=.*[A-Z])(?=.*\d).+$",
ErrorMessage = "密码必须包含大小写字母和数字")]
public string Password { get; set; } = string.Empty;
/// <summary>
/// 手机号(可选)
/// </summary>
/// <example>13800138000</example>
[RegularExpression(@"^1[3-9]\d{9}$", ErrorMessage = "手机号格式不正确")]
public string? Phone { get; set; }
}
/// <summary>
/// 用户信息 DTO
/// </summary>
public class UserDto
{
/// <summary>
/// 用户 ID
/// </summary>
public long Id { get; set; }
/// <summary>
/// 用户名
/// </summary>
public string UserName { get; set; } = string.Empty;
/// <summary>
/// 邮箱
/// </summary>
public string Email { get; set; } = string.Empty;
/// <summary>
/// 创建时间
/// </summary>
public DateTimeOffset CreatedAt { get; set; }
}
/// <summary>
/// 分页结果
/// </summary>
/// <typeparam name="T">数据类型</typeparam>
public class PagedResult<T>
{
/// <summary>
/// 数据列表
/// </summary>
public List<T> Items { get; set; } = new();
/// <summary>
/// 总数
/// </summary>
public int Total { get; set; }
/// <summary>
/// 当前页
/// </summary>
public int Page { get; set; }
/// <summary>
/// 每页数量
/// </summary>
public int PageSize { get; set; }
}JWT 认证文档集成
// ============================================
// JWT 安全方案配置
// ============================================
builder.Services.AddSwaggerGen(options =>
{
// 定义安全方案
options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
{
Name = "Authorization",
In = ParameterLocation.Header,
Type = SecuritySchemeType.Http,
Scheme = "bearer",
BearerFormat = "JWT",
Description = "请输入 JWT Token(格式:Bearer {token})"
});
// 应用安全方案到所有接口
options.AddSecurityRequirement(new OpenApiSecurityRequirement
{
{
new OpenApiSecurityScheme
{
Reference = new OpenApiReference
{
Type = ReferenceType.SecurityScheme,
Id = "Bearer"
}
},
Array.Empty<string>()
}
});
// API Key 安全方案(可选)
options.AddSecurityDefinition("ApiKey", new OpenApiSecurityScheme
{
Name = "X-API-Key",
In = ParameterLocation.Header,
Type = SecuritySchemeType.ApiKey,
Description = "请输入 API Key"
});
});ProblemDetails 错误响应
// ============================================
// 全局错误响应配置 — 让 Swagger 显示标准错误格式
// ============================================
builder.Services.AddProblemDetails();
builder.Services.AddEndpointsApiExplorer();
// 在 Minimal API 中使用
app.MapGet("/api/errors", (IHostEnvironment env) =>
{
throw new InvalidOperationException("这是一个示例错误");
})
.ProducesProblem(StatusCodes.Status500InternalServerError);
// 自定义 ProblemDetails
app.MapGet("/api/not-found", () =>
{
return Results.Problem(
title: "资源未找到",
detail: "请求的用户不存在",
statusCode: 404,
instance: "/api/users/99999"
);
})
.ProducesProblem(StatusCodes.Status404NotFound);版本化文档
// ============================================
// 多版本文档配置
// ============================================
builder.Services.AddApiVersioning(options =>
{
options.DefaultApiVersion = new ApiVersion(1, 0);
options.AssumeDefaultVersionWhenUnspecified = true;
options.ReportApiVersions = true;
});
builder.Services.AddVersionedApiExplorer(options =>
{
options.GroupNameFormat = "'v'VVV";
options.SubstituteApiVersionInUrl = true;
});
builder.Services.AddSwaggerGen(options =>
{
options.SwaggerDoc("v1", new OpenApiInfo
{
Title = "SunnyFan API",
Version = "v1",
Description = "API v1 — 当前稳定版本"
});
options.SwaggerDoc("v2", new OpenApiInfo
{
Title = "SunnyFan API",
Version = "v2",
Description = "API v2 — 增强版(分页、筛选、聚合)"
});
});
app.UseSwaggerUI(options =>
{
options.SwaggerEndpoint("/swagger/v1/swagger.json", "API v1");
options.SwaggerEndpoint("/swagger/v2/swagger.json", "API v2");
// 默认展开所有操作
options.DocExpansion(DocExpansion.List);
});优点
缺点
总结
Swagger / OpenAPI 的真正价值在于"让接口成为可被治理的契约",而不是只生成一个页面。对 ASP.NET Core 项目来说,最值得投入的是:补齐 XML 注释、错误响应、认证方案、版本分组和 DTO 说明,让文档真正能支撑开发、测试和排障协作。
关键知识点
- Swagger UI 是界面,OpenAPI 才是契约本体。
- 好文档不止描述 200 成功响应,还要描述 400 / 401 / 403 / 404 / 409 / 500。
- 认证方式、版本策略和错误格式都应该体现在文档里。
- 文档自动生成只是基础,注释与规范补齐才是价值所在。
项目落地视角
- 前后端分离项目几乎都应提供 Swagger / OpenAPI 文档。
- 内部系统也建议用它做接口调试和联调基线。
- 多团队协作时,版本分组和业务域分组尤其重要。
- 生产环境是否暴露文档,要根据网关、内网、权限策略谨慎决定。
常见误区
- 只开 Swagger,不写注释、不管响应模型。
- 所有错误都返回一个模糊字符串,文档里也看不出差异。
- 文档和真实鉴权策略不一致,前端调试非常痛苦。
- 把 Swagger 当开发工具,而不是团队契约和治理入口。
进阶路线
- 结合 API 版本控制做多版本文档管理。
- 补充示例请求 / 响应和 ProblemDetails 错误模型。
- 将 OpenAPI 输出用于前端 SDK 生成和契约测试。
- 与鉴权、网关、限流和监控体系联动形成 API 治理平台。
适用场景
- RESTful API 项目。
- 前后端分离系统。
- 多团队协作接口项目。
- 需要接口调试、测试和契约治理的系统。
落地建议
- 每个对外接口都至少补齐 summary、参数说明、返回模型和错误码。
- 关键 DTO 补示例值和校验规则说明。
- 对 JWT / Cookie / API Key 等认证方式做显式文档说明。
- 将 Swagger 文档与版本发布、测试和网关治理一起考虑。
排错清单
- 文档页面打不开:先检查
UseSwagger/UseSwaggerUI是否启用。 - 注释没显示:检查 XML 文档输出和
IncludeXmlComments路径。 - JWT 调试不生效:检查 SecurityDefinition 和 SecurityRequirement 配置。
- 文档和实际返回不一致:检查 DTO、过滤器、异常处理中间件和版本分组。
