基於.NetCore開發部落格專案 StarBlog - (26) 整合Swagger介面文件

程式設計實驗室發表於2023-02-05

前言

這是StarBlog系列在2023年的第一篇更新?~

在之前的文章裡,我們已經完成了部分介面的開發,接下來需要使用 curl、Postman 這類工具對這些介面進行測試,但介面一多,每次測試都要一個個填入地址和對應引數會比較麻煩…

我們需要一種直觀的方式來彙總專案裡的所有介面,並且如果能直接在裡面除錯介面,那就更好了。

Swagger:誒嘿,說的不就是我嗎??

Swagger介紹

來一段官網的介紹

Simplify API development for users, teams, and enterprises with the Swagger open source and professional toolset.

翻譯:Swagger 是開源和專業的工具集,可以簡化使用者、團隊和企業的 API 開發。

一般來說,swagger用起來有兩部分,一個是 OpenAPI 一個是 SwaggerUI

在Swagger官網上,OpenAPI 介紹得天花亂墜?

The OpenAPI Specification, formerly known as the Swagger Specification, is the world’s standard for defining RESTful interfaces. The OAS enables developers to design a technology-agnostic API interface that forms the basis of their API development and consumption.

翻譯:OpenAPI 規範,以前稱為 Swagger 規範,是定義 RESTful 介面的世界標準。 OAS 使開發人員能夠設計一個與技術無關的 API 介面,該介面構成了他們 API 開發和使用的基礎。

簡單說 OpenAPI 是個標準,需要每種語言和框架自行實現一個工具,用來把專案裡的介面都整合起來,生成 swagger.json 檔案

然後 SwaggerUI 就是個網頁,讀取這個 swagger.json 就可以把所有介面以及引數顯示出來,還可以很方便除錯,效果如圖。

image

Swashbuckle.AspNetCore

前面說到每種框架都要自己實現一個工具來生成 swagger.json ,這個 Swashbuckle.AspNetCore 就是 .NetCore 平臺的實現,用就完事了。

專案主頁: https://github.com/domaindrivendev/Swashbuckle.AspNetCore

Tips:如果是建立 WebApi 專案,程式碼模板裡面預設就有 Swagger 了,不用手動新增。

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 就能看到介面文件了

效果大概這樣

image

擴充套件:關於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 介面文件頁面

可以看到右上角可以選擇介面分組了

image

搞定。

最佳化分組

前文對於 Swagger 分組的實現其實是一種硬編碼,不同分組的 Controller 上面需要加上 [ApiExplorerSettings(GroupName = "blog")] 特性,分組名全靠複製貼上,在專案比較小的情況下還好,如果分組多起來了,有幾百個介面的時候,估計人就麻了吧?

Q:“你剛才幹嘛不早說?”

A:“循序漸進嘛?”

A:“StarBlog專案也是最近才換到新版分組的?”

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的開發過程是吻合的?

參考資料

系列文章

相關文章