前言

这是StarBlog系列在2023年的第一篇更新😃~

在之前的文章里,我们已经完成了部分接口的开发,接下来需要使用 curl、Postman 这类工具对这些接口进行测试,但接口一多,每次测试都要一个个填入地址和对应参数会比较麻烦…

我们需要一种直观的方式来汇总项目里的所有接口,并且如果能直接在里面调试接口,那就更好了。

Swagger介绍

来一段官网的介绍

一般来说,swagger用起来有两部分,一个是 OpenAPI 一个是 SwaggerUI

在Swagger官网上,OpenAPI 介绍得天花乱坠🙂

简单说 OpenAPI 是个标准,需要每种语言和框架自行实现一个工具,用来把项目里的接口都整合起来,生成 swagger.json 文件

然后 SwaggerUI 就是个网页,读取这个 swagger.json 就可以把所有接口以及参数显示出来,还可以很方便调试,效果如图。

基于.NetCore开发博客项目 StarBlog - (26) 集成Swagger接口文档-LMLPHP

Swashbuckle.AspNetCore

前面说到每种框架都要自己实现一个工具来生成 swagger.json ,这个 Swashbuckle.AspNetCore 就是 .NetCore 平台的实现,用就完事了。

项目主页: https://github.com/domaindrivendev/Swashbuckle.AspNetCore

StarBlog项目一开始是使用MVC模板,所以没有自带Swagger,需要手动添加。

直接使用nuget添加 Swashbuckle.AspNetCore 这个包就完事了。

这个包功能很多,内置了 SwaggerUI 这个官方界面,还有一个 ReDoc 的纯静态接口文档网页(这个 ReDoc 只能看接口不能调试)。

初步使用

为了保证 Program.cs 代码整洁,我们在 StarBlog.Web/Extensions 里面创建 ConfigureSwagger

public static class ConfigureSwagger {
  public static void AddSwagger(this IServiceCollection services) {
    services.AddSwaggerGen(options => {
      options.SwaggerDoc("v1", new OpenApiInfo { Version = "v1", Title = "APIs"});

      // 在接口文档上显示 XML 注释
      var filePath = Path.Combine(System.AppContext.BaseDirectory, $"{typeof(Program).Assembly.GetName().Name}.xml");
      options.IncludeXmlComments(filePath, true);
    });
  }
  
  public static void UseSwaggerPkg(this IApplicationBuilder app) {
    app.UseSwagger();
    app.UseSwaggerUI(options => {
      options.RoutePrefix = "api-docs/swagger";
      options.SwaggerEndpoint("/swagger/v1/swagger.json", "APIs");
    });
    app.UseReDoc(options => {
      options.RoutePrefix = "api-docs/redoc";
      options.SpecUrl = "/swagger/v1/swagger.json";
    });
  }
}

上面代码可以看到有三步

  • AddSwaggerGen - 对应前文说的生成 swagger.json
  • UseSwagger - 让浏览器可以访问到 /swagger/v1/swagger.json 这类路径
  • UseSwaggerUI - 提供 SwaggerUI 的网页访问

然后回到 Program.cs 里面,分别注册服务和添加中间件就好了。

// 注册服务
builder.Services.AddSwagger();
// 添加中间件
app.UseSwaggerPkg();

现在启动项目,访问 http://[本地地址]/api-docs/swagger 就能看到接口文档了

效果大概这样

基于.NetCore开发博客项目 StarBlog - (26) 集成Swagger接口文档-LMLPHP

扩展:关于XML注释

C# 的代码注释可以导出XML,然后显示在 swagger 文档上

注意需要手动在 .csproj 项目配置里面开启,才会输出XML文档

<!--  输出XML  -->
<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

但是开启XML之后,IDE很蠢的要求我们所有public成员都写上注释,很烦,加上 <NoWarn>$(NoWarn);1591</NoWarn> 这行就可以关掉这个警告。

在 Swagger 里加载XML文档,既可以用本文前面写的方式

var filePath = Path.Combine(System.AppContext.BaseDirectory, $"{typeof(Program).Assembly.GetName().Name}.xml");
options.IncludeXmlComments(filePath, true);

还可以用第二种,加载目录里的全部XML

var xmlFiles = Directory.GetFiles(AppContext.BaseDirectory, "*.xml");
foreach (var file in xmlFiles) {
  options.IncludeXmlComments(file, true);
}

具体用哪种,都行吧,看心情~

扩展:关于 AddEndpointsApiExplorer

AddSwagger 扩展方法这里可能有同学会有疑问

为啥创建 .Net6 项目后默认是这两行代码

builder.Services.AddEndpointsApiExplorer();
builder.Services.AddSwaggerGen();

而我这里只有一行代码

services.AddSwaggerGen();

先说结论:AddEndpointsApiExplorer 是为了支持 Minimal Api 的。

因为 StarBlog 项目使用的是MVC模板,在 Program.cs 的最开始可以看到这行代码,添加控制器和视图

builder.Services.AddControllersWithViews();

翻一下这个框架的源码,可以看到这个方法的套娃是这样的

AddControllersWithViews() -> AddControllersWithViewsCore() -> AddControllersCore()

而在 AddControllersCore 里面,又调用了 AddApiExplorer

private static IMvcCoreBuilder AddControllersCore(IServiceCollection services) {
  // This method excludes all of the view-related services by default.
  var builder = services
    .AddMvcCore()
    .AddApiExplorer()
    .AddAuthorization()
    .AddCors()
    .AddDataAnnotations()
    .AddFormatterMappings();

  if (MetadataUpdater.IsSupported) {
    services.TryAddEnumerable(
      ServiceDescriptor.Singleton<IActionDescriptorChangeProvider, HotReloadService>());
  }

  return builder;
}

就是说正常的项目已经有 ApiExplorer 这个东西了,但是 Minimal Api 项目没有,所以本项目不需要 builder.Services.AddEndpointsApiExplorer(); 这行代码。

详情可以阅读参考资料的第一个链接。

接口分组

接口文档有了,但项目里接口太多了,几十个接口全挤在一个页面上,找都找得眼花了😑

这时候可以给接口分个组

先来给 StarBlog 项目里面的接口分个类,根据不同用途,大致分成这五类:

  • admin - 管理员相关接口
  • common - 通用公共接口
  • auth - 授权接口
  • blog - 博客管理接口
  • test - 测试接口

还是在上面那个 ConfigureSwagger.cs 文件

修改 AddSwagger 方法,把这几个分组添加进去

services.AddSwaggerGen(options => {
  options.SwaggerDoc("admin", new OpenApiInfo {
    Version = "v1",
    Title = "Admin APIs",
    Description = "管理员相关接口"
  });
  options.SwaggerDoc("common", new OpenApiInfo {
    Version = "v1",
    Title = "Common APIs",
    Description = "通用公共接口"
  });
  options.SwaggerDoc("auth", new OpenApiInfo {
    Version = "v1",
    Title = "Auth APIs",
    Description = "授权接口"
  });
  options.SwaggerDoc("blog", new OpenApiInfo {
    Version = "v1",
    Title = "Blog APIs",
    Description = "博客管理接口"
  });
  options.SwaggerDoc("test", new OpenApiInfo {
    Version = "v1",
    Title = "Test APIs",
    Description = "测试接口"
  });
});

这样就会生成五个 swagger.json 文件,路径分别是

  • /swagger/admin/swagger.json
  • /swagger/common/swagger.json
  • /swagger/auth/swagger.json
  • /swagger/blog/swagger.json
  • /swagger/test/swagger.json

所以下面的 UseSwaggerPkg 方法也要对应修改

public static void UseSwaggerPkg(this IApplicationBuilder app) {
  app.UseSwagger();
  app.UseSwaggerUI(options => {
    options.RoutePrefix = "api-docs/swagger";
    options.SwaggerEndpoint("/swagger/admin/swagger.json", "Admin");
    options.SwaggerEndpoint("/swagger/blog/swagger.json", "Blog");
    options.SwaggerEndpoint("/swagger/auth/swagger.json", "Auth");
    options.SwaggerEndpoint("/swagger/common/swagger.json", "Common");
    options.SwaggerEndpoint("/swagger/test/swagger.json", "Test");
  });
}

接下来,要让 Swagger 知道每个接口都是属于哪个分组的。

具体方法是在 Controller 上添加 ApiExplorerSettings 特性。

比如 BlogController 是属于 blog 分组,在 class 定义前面添加一行代码

[ApiExplorerSettings(GroupName = "blog")]
public class BlogController : ControllerBase {
  // ...
}

其他的 Controller 也是类似的操作,具体分组跟 StarBlog.Web/Apis 下的目录结构一样,这里就不赘述了。

实现效果

做完之后,打开 swagger 接口文档页面

可以看到右上角可以选择接口分组了

基于.NetCore开发博客项目 StarBlog - (26) 集成Swagger接口文档-LMLPHP

搞定。

优化分组

前文对于 Swagger 分组的实现其实是一种硬编码,不同分组的 Controller 上面需要加上 [ApiExplorerSettings(GroupName = "blog")] 特性,分组名全靠复制粘贴,在项目比较小的情况下还好,如果分组多起来了,有几百个接口的时候,估计人就麻了吧😂

StarBlog.Web/Models 里添加个新的类 SwaggerGroup

public class SwaggerGroup {
    /// <summary>
    /// 组名称(同时用于做URL前缀)
    /// </summary>
    public string Name { get; set; }

    public string? Title { get; set; }
    public string? Description { get; set; }

    public SwaggerGroup(string name, string? title = null, string? description = null) {
        Name = name;
        Title = title;
        Description = description;
    }

    /// <summary>
    /// 生成 <see cref="Microsoft.OpenApi.Models.OpenApiInfo"/>
    /// </summary>
    public OpenApiInfo ToOpenApiInfo(string version = "1.0") {
        var item = new OpenApiInfo();
        Title ??= Name;
        Description ??= Name;
        return new OpenApiInfo { Title = Title, Description = Description, Version = version };
    }
}

然后改造一下 StarBlog.Web/Extensions/ConfigureSwagger.cs

在这个文件里面添加个新的类,这样就不会硬编码了😃

public static class ApiGroups {
  public const string Admin = "admin";
  public const string Auth = "auth";
  public const string Common = "common";
  public const string Blog = "blog";
  public const string Test = "test";
}

ConfigureSwagger 里添加一些代码,创建 SwaggerGroup 列表

public static class ConfigureSwagger {
  public static readonly List<SwaggerGroup> Groups = new() {
    new SwaggerGroup(ApiGroups.Admin, "Admin APIs", "管理员相关接口"),
    new SwaggerGroup(ApiGroups.Auth, "Auth APIs", "授权接口"),
    new SwaggerGroup(ApiGroups.Common, "Common APIs", "通用公共接口"),
    new SwaggerGroup(ApiGroups.Blog, "Blog APIs", "博客管理接口"),
    new SwaggerGroup(ApiGroups.Test, "Test APIs", "测试接口")
  };
}

然后把后面的 AddSwagger 方法改成这样,那一坨东西,现在一行代码就代替了😀

public static void AddSwagger(this IServiceCollection services) {
  services.AddSwaggerGen(options => {
    Groups.ForEach(group => options.SwaggerDoc(group.Name, group.ToOpenApiInfo()));

    // XML注释
    var filePath = Path.Combine(AppContext.BaseDirectory, $"{typeof(Program).Assembly.GetName().Name}.xml");
    options.IncludeXmlComments(filePath, true);
  });
}

接着是 UseSwaggerPkg 方法,简单😎

public static void UseSwaggerPkg(this IApplicationBuilder app) {
  app.UseSwagger();
  app.UseSwaggerUI(opt => {
    opt.RoutePrefix = "api-docs/swagger";
    // 分组
    Groups.ForEach(group => opt.SwaggerEndpoint($"/swagger/{group.Name}/swagger.json", group.Name));
  });
}

Controller里面也对应修改成这样

[ApiExplorerSettings(GroupName = ApiGroups.Blog)]
public class BlogController : ControllerBase {
}

完美🆒🥳

小结

“Swagger之大,一锅炖不下”

关于Swagger还有其他的用法,但需要一些前置知识,因此本文不会把StarBlog项目中关于Swagger的部分全部介绍完

等把相关的前置知识写完,再来完善对应的用法~

这也跟StarBlog的开发过程是吻合的😀

参考资料

系列文章

02-05 20:24