The Will Will Web

記載著 Will 在網路世界的學習心得與技術分享

如何在 ASP․NET Core 3 完整設定 NSwag 與 OpenAPI v3 文件

要設定好一份相對完整的 OpenAPI v3 文件,需要具備相當的知識才能深入理解,本篇文章我將介紹如何在 ASP․NET Core 3.1 環境下設定 NSwag 套件,並且介紹各種設定的詳細說明。

2021-06-02: 本文章範例已更新至 .NET 5 版本,但其實兩個版本的設定皆完全相同。

基本觀念建立

NSwag 是一套相當完整的 Swagger/OpenAPI 工具集,包含了 Swagger(v2)/OpenAPI(v3) 文件產生器,以及視覺化的互動式 Swagger UI 介面。使用 NSwag 時,不一定要先寫好 API 才能產生文件,如果你已經有現成的 OpenAPI 文件,一樣可以透過此工具產生必要的文件或用戶端程式碼(C# 或 TypeScript)。

不過,大部分時候,我們通常還是會先從開發 Web API 開始,然後透過 NSwag 產生 API 文件。在開始之前,你至少要知道幾個個專有名詞:

  1. Swagger

    早期 Swagger 是一家名叫 SmartBear Software 這家公司所發展的開源專案,該工具不但能夠透過 Swagger UI 工具快速一覽目前 .NET 專案的所有 RESTful APIs 清單,還能自動產生文件、用戶端程式碼(C#、TypeScript)、測試案例等等,由於做得太好了,所以在業界被大量採用。

    Swagger (software) - Wikipedia

  2. Swagger API

    這是 Swagger 工具的核心,早期 (v1) 是直接透過 Swagger 自行定義出的一份可以描述 RESTful APIs 的規格文件,其格式可以是 YAML 或 JSON 等。

  3. OpenAPI

    由於 Swagger 太多人使用,後來就成立了一個 Open API Initiative (OAI) 組織,用來訂定真正業界標準的 API 描述規範,也用更加透明的方式,讓大眾可以直接參與訂定規範,不再隸屬於特定公司下的一個產品。也因此,你常常會看到 Swagger/OpenAPI 諸如此類的寫法,但事實上可能大家在談的其實是同一套產品。建議未來大家盡量都使用 OpenAPI 為主要名稱。

  4. OpenAPI v2

    從 Swagger API 轉到 OpenAPI 的過程,第一個公開版本為 目前最新版本為 OpenAPI Specification v2.0,發佈於 2014 年 09 月 08 日。(GitHub)

  5. OpenAPI v3

    目前最新版本為 OpenAPI Specification v3.0.2,發佈於 2018 年 10 月 08 日。(GitHub)

  6. Document (文件)

    由於 NSwag 會先產生一份 Swagger/Open API specification 規格文件,預設是以 JSON 的格式輸出,網址路徑預設為:/swagger/v1/swagger.json

    這份文件的內容,原則上全都是由 AspNetCoreOpenApiDocumentGenerator 來負責產生,這當中有相當多可以設定的地方。(AspNetCoreOpenApiDocumentGenerator.cs)

  7. Operation (操作)

    這裡通常泛指每一個 API 操作,在 C# 裡面,其實就是 ASP․NET Core Web API 中控制器類別中的動作方法(Methods)。

注意:由於 NSwag 不支援 .NET Core 3 的 System.Text.Json,因為 System.Text.Json 並沒有揭露任何附加資訊以產生 Schemas,所以在 NSwag 內部其實是採用 Newtonsoft.Json 的 Schema 規則!相關討論請參見: Epic: Support System.Text.Json · Issue #2243 · RicoSuter/NSwag

安裝 NSwag 工具集

要在 ASP․NET Core 中使用 NSwag 的話,必須先安裝 NSwag.AspNetCore 套件,並且設定 NSwag middleware 才能使用。這個套件包含自動產生 Swagger/OpenAPI 規格的功能,可以自動掃描目前專案中所有 API 控制器中的 Actions,並產生必要的 swagger.jsonopenapi.json 規格文件。除此之外,也包含了 Swagger UI (v2 and v3) 與 ReDoc UI 頁面。

  1. 安裝 NSwag.AspNetCore 套件

    dotnet add package NSwag.AspNetCore
    
  2. 設定 Startup.ConfigureServicesStartup.Configure

    public class Startup
    {
        ...
    
        public void ConfigureServices(IServiceCollection services)
        {
            // Add OpenAPI v3 document
            services.AddOpenApiDocument();
    
            // Add Swagger v2 document
            // services.AddSwaggerDocument();
        }
    
        public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
        {
            ...
    
            app.UseOpenApi();       // serve OpenAPI/Swagger documents
    
            app.UseSwaggerUi3();    // serve Swagger UI
    
            app.UseReDoc(config =>  // serve ReDoc UI
            {
                // 這裡的 Path 用來設定 ReDoc UI 的路由 (網址路徑) (一定要以 / 斜線開頭)
                config.Path = "/redoc";
            });
        }
    }
    
  3. 瀏覽 Swagger 文件與與 Swagger UI 頁面

    基本上,在 Startup.ConfigureServicesStartup.Configure 不特別設定的情況下,以下網址路徑為預設值:

    • Swagger UI: https://localhost:5001/swagger/
    • Swagger 規格: https://localhost:5001/swagger/v1/swagger.json

    請注意:你在輸入 Swagger UI 網址時,請盡量使用 https://localhost:5001/swagger/ 這個網址,而不要使用像是 https://localhost:5001/swagger/index.html 這樣的網址。這是因為當你在一個站台下有多份 OpenAPI 文件的時候,透過 /swagger/ 會自動載入預設的文件,但 /swagger/index.html 就不會自動載入文件。

  4. 瀏覽 ReDoc UI 頁面

    預設 ReDoc UI 的網址其實是跟 Swagger UI 的網址是一樣的,因此會有點衝突,感覺一次只能使用一套。

    • ReDoc UI: https://localhost:5001/swagger/

    但我在上述步驟 2 的時候,特別設定了 app.UseReDoc()Path 屬性,重新設定路由為 /redoc,如此一來就不會跟 Swagger UI 的網址衝突,兩套可以同時使用。

    • ReDoc UI: https://localhost:5001/redoc/
  5. 使用 NSwag 工具集產生用戶端程式碼

啟用專案的 XML 註解功能

想在 ASP․NET Core 3.1 專案自動產生 XML 註解文件,必須要在你的 *.csproj 專案檔中設定以下屬性:

<PropertyGroup>
  <GenerateDocumentationFile>true</GenerateDocumentationFile>
  <NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>

當你設定了 <GenerateDocumentationFile>true</GenerateDocumentationFile> 之後,每次編譯專案都會出現大量的 CS1591 編譯器警告,建議可以設定 <NoWarn>$(NoWarn);1591</NoWarn> 關閉 CS1591 警告訊息。(Visual Studio Disabling Missing XML Comment Warning - Stack Overflow)

上述 <GenerateDocumentationFile>true</GenerateDocumentationFile> 這個設定等同於會將所有 *.cs 檔案中的 XML 註解都輸出到 bin\$(Configuration)\$(TargetFramework)\$(AssemblyName).xml 檔案中。

以下是在 Web API 中常見的 XML 註解用法:

  • 替每個 Operation 的回應類型狀態碼提供說明

    /// <summary>
    /// Creates an order.
    /// </summary>
    /// <param name="order"></param>
    /// <response code="201">Order created.</response>
    /// <response code="400">Order invalid.</response>
    [HttpPost]
    [ProducesResponseType(typeof(int), 201)]
    [ProducesResponseType(typeof(IDictionary<string, string>), 400)]
    public IActionResult CreateOrder()
    {
        return new CreatedResult("/orders/1", 1);
    }
    
  • 如果回應的內容無法決定類型,例如下載檔案,你可以這樣設定:

    [ProducesResponseType(typeof(IActionResult), 200)]
    public IActionResult DownloadFile(string name)
    {
        ....
    }
    
  • 如果確定不會回應任何內 (HTTP 204 或 HTTP 404),你可以這樣設定:

    [ProducesResponseType(typeof(void), 204)]
    public IActionResult DownloadFile(string name)
    {
        ....
    }
    
  • 如果有預期的例外發生,可以這樣設定:(N.B. 不建議直接拋出完整的例外內容)

    [HttpPost]
    [ProducesResponseType(typeof(int), 200)]
    [ProducesResponseType(typeof(LocationNotFoundException), 500)]
    public async Task<ActionResult> Create([FromBody]Person person)
    {
        try
        {
            var location = await _geoService.FindLocationAsync(person.Location);
            person.LocationLatitude = location.Latitude;
            person.LocationLongitude = location.Longitude;
        }
        catch (LocationNotFoundException locationNotFoundException)
        {
            return StatusCode(500, locationNotFoundException);
        }
    
        await _dataContext.SaveChangesAsync();
        return Ok(person.Id);
    }
    
  • 變更預設處理 null 回應的行為

    基本上,在 .NET 裡面,所有回應 IActionResultActionResult<T> 類型都是可以允許回傳 null 的。但是在 Web API 裡面,預設所有 API 回應都是不允許空值(Not Null)出現。

    當你希望 ASP.NET Core 預設可以接受回傳 null 空值,可以在 services.AddOpenApiDocument() 裡面做出以下設定:

    config.DefaultResponseReferenceTypeNullHandling = NJsonSchema.Generation.ReferenceTypeNullHandling.Null;
    

    你也可以透過 XML 註解來設定只有 Controller 內的其中一個 Action 允許輸出空值:

    /// <response code="200" nullable="true">The order.</response>
    [HttpGet("~/claims")]
    public ActionResult<IEnumerable<Claim>> GetClaims()
    {
        return Ok(User.Claims.Select(p => new { p.Type, p.Value }));
    }
    

注意:雖然 C# 有非常多種 XML 註解的表示法,但並非所有 XML 註解都可以正常顯示在 Swagger UI 中。

替 Controller/Action/Parameter 加上額外屬性 (Attributes)

你可以在 Controller 或 Action 上面設定一些額外的 Attributes,以改變 OpenAPI 規格文件的定義,這些定義除了可以變更 API 文件的顯示方式外,也可以幫助用戶端程式碼產生器產生出更加易懂的程式碼。

  • [OpenApiOperation("GetClaims")]

    用來指定特定一個 API 的操作名稱操作名稱會用用在 API 用戶端程式碼產生器中,操作名稱會自動成為方法名稱

  • [OpenApiIgnore]

    將特定 API Action 排除在文件之外。

  • [OpenApiTags("foo", "bar")]

    定義操作標籤。參見 Specify the operation tags

    當使用 https://localhost:5001/swagger/ 的時候,這裡所定義的 Tags 會變成第一層分類的名稱,如果一個 API 有兩個 Tags,那麼該 API Method 就會重複出現在兩個標籤下。

  • [OpenApiExtensionData("ABC", "123")]

    外加一些 Metadata 到 openapi.json 定義中。(可用於用戶端程式碼產生器))

  • [OpenApiBodyParameter("text/json")]

    當沒有用 [FromBody] 設定接入參數,使用程式設計方式讀取 POST Body 的時候,可以用這個 Attribute 來宣告傳入的資料類型。(少用)

  • [NotNull][CanBeNull]

    可以直接定義在 DTO 類別的屬性、操作參數、回傳型別。

    所有 DTO 的屬性,只要是參考型別都是 CanBeNull 的!

    回傳型別的標示方法,是在操作方法上寫以下標註,預設所有操作方法都是 NotNull 的:

    [return: NJsonSchema.Annotations.CanBeNull]
    
  • [BindRequired]

    只能定義在操作方法的參數列上,明確宣告這個參數必須被執行資料繫結,也就是參數必填的意思。

    如果用自訂的 ViewModel 來傳參數,預設值就是必填。

    更多 Provided NJsonSchema attributes

  • [OpenApiFile]

    只要你的任何一個 Operation (操作) 使用了以下任何一種型別,預設就會被視為「檔案上傳」的欄位:

    • IFormFile
    • HttpPostedFile
    • HttpPostedFileBase

    如果你想將自訂類別標示為檔案上傳,可以透過 [OpenApiFile] 來標示:

    public void MyOperation([OpenApiFile] MyClass myParameter)
    {
        ...
    }
    

    請記得引用 NSwag.Annotations 命名空間。

同時支援 OpenAPI v3.0.0Swagger v2.0 文件

目前 NSwag 同時支援 OpenAPI v3.0.0Swagger v2.0 兩個版本,當你在 Startup.ConfigureServices 設定時,選擇其中一個版本即可,建議選擇新版。

雖然同時註冊兩個版本很少見,但如果你要這麼做的話,必須區分不同的 DocumentName 才行:

services.AddOpenApiDocument(document => document.DocumentName = "a");
services.AddSwaggerDocument(document => document.DocumentName = "b");

而且設定 app.UseSwaggerUi3()app.UseReDoc() 的時候也要指定相對應的 DocumentName 屬性,而且 Swagger UI 與 ReDoc UI 的路由位址也必須區隔開來,所以要設定 Path 屬性:

app.UseOpenApi(config => { config.DocumentName = "a"; });
app.UseSwaggerUi3(config =>
{
    config.Path = "/swagger_a";
    config.DocumentPath = "/swagger/a/swagger.json";
});


app.UseOpenApi(config => { config.DocumentName = "b"; });
app.UseSwaggerUi3(config =>
{
    config.Path = "/swagger_b";
    config.DocumentPath = "/swagger/b/swagger.json";
});

在 Swagger UI 加入文件名稱、標題、版本、簡介

當你在 Startup.ConfigureServices 設定時,可以加入幾個重要的設定,分別說明如下:

  1. config.DocumentName

    這個設定相當重要,預設值雖然為 v1,但其用意並非「版本號」的意思,而是文件名稱,該文件名稱會顯示在網址列上,因為 app.UseOpenApi() 預設的路由網址為 /swagger/{documentName}/swagger.json,所以預設網址就是 /swagger/v1/swagger.json

    如果你改變了 DocumentName (文件名稱) 屬性,網址就會改變。

  2. config.Version

    想要設定或變更顯示在 OpenAPI 文件上的 API 版本資訊,可以自行指定版本號碼。

  3. config.Title

    設定文件的顯示標題。

  4. config.Description

    設定文件簡要說明。

以下是設定範例:

// add OpenAPI v3 document
services.AddOpenApiDocument(config =>
{
    // 設定文件名稱 (重要) (預設值: v1)
    config.DocumentName = "v2";

    // 設定文件或 API 版本資訊
    config.Version = "0.0.1";

    // 設定文件標題 (當顯示 Swagger/ReDoc UI 的時候會顯示在畫面上)
    config.Title = "JwtAuthDemo";

    // 設定文件簡要說明
    config.Description = "This is a JWT authentication/authorization sample app";
});

image

微調 OpenAPI 規格文件

我們在定義 OpenAPI 規格時,主要有兩個目的:

  1. 自動產生完整且清楚的 API 文件 (Swagger UI)
  2. 自動產生用戶端程式碼 (API Client code)

而這兩件事,都是由 OpenAPI Specification 為核心,因此你只要建立出一份完整的 OpenAPI 規格文件,就可以達成以上兩個目的。

我們用 Swagger UI 顯示 API 文件的時候,我們可能會希望依據不同的環境顯示不同的 API 文件資訊,例如文件標題可以標示「測試環境」或「預備環境」、變更 API 的基底網址(BaseUrl)等等。

在 NSwag 裡面,你在設定 app.UseOpenApi() 的時候,可以調整一個 PostProcess 參數,該參數型別為 Action<OpenApiDocument, Microsoft.AspNetCore.Http.HttpRequest>,可以讓你在 services.AddOpenApiDocument(); 設定服務後,還可以動態調整文件內容。

請注意:這裡的 PostProcess 參數,當使用 NSwag CLIMSBuild 的時候不會生效,僅適用於 Swagger UI 或 ReDoc UI 瀏覽環境下使用。

  • 以下是一份完整的設定範例:

    app.UseOpenApi(config =>
    {
        // 這裡的 Path 用來設定 OpenAPI 文件的路由 (網址路徑) (一定要以 / 斜線開頭)
        config.Path = "/swagger/v2/swagger.json";
    
        // 這裡的 DocumentName 必須跟 services.AddOpenApiDocument() 的時候設定的 DocumentName 一致!
        config.DocumentName = "v2";
    
        config.PostProcess = (document, http) =>
        {
            if (env.IsDevelopment())
            {
                document.Info.Title += " (開發環境)";
                document.Info.Version += "-dev";
                document.Info.Description += "當 API 有問題時,請聯繫 Will 保哥的技術交流中心 粉絲團,我們有專業顧問可以協助解決困難!";
                document.Info.Contact = new NSwag.OpenApiContact
                {
                    Name = "Will Huang",
                    Email = "doggy.huang@gmail.com",
                    Url = "https://twitter.com/Will_Huang"
                };
            }
            else
            {
                document.Info.TermsOfService = "https://go.microsoft.com/fwlink/?LinkID=206977";
    
                document.Info.Contact = new NSwag.OpenApiContact
                {
                    Name = "Will Huang",
                    Email = "doggy.huang@gmail.com",
                    Url = "https://twitter.com/Will_Huang"
                };
            }
    
            document.Info.License = new NSwag.OpenApiLicense
            {
                Name = "The MIT License",
                Url = "https://opensource.org/licenses/MIT"
            };
        };
    });
    
  • 以下是各種不同的設定範例

設定身分驗證與授權的 API 文件

在開發 API 的時候,身分認證與授權,不外乎就是 OAuth2API KeyJWT 等不同做法,請參考連結進行設定即可。

  1. 以下是 JWT 驗證授權的設定範例:

    services.AddOpenApiDocument(config =>
    {
        var apiScheme = new OpenApiSecurityScheme()
        {
            Type = OpenApiSecuritySchemeType.ApiKey,
            Name = "Authorization",
            In = OpenApiSecurityApiKeyLocation.Header,
            Description = "Copy this into the value field: Bearer {token}"
        };
    
        config.AddSecurity("Bearer", Enumerable.Empty<string>(), apiScheme);
    
        config.OperationProcessors.Add(
            new AspNetCoreOperationSecurityScopeProcessor("Bearer"));
    });
    

    請務必將 config.AddSecurity()new AspNetCoreOperationSecurityScopeProcessor() 都設定為一樣的安全名稱(Bearer),如此一來,所有專案內只要有套用 [Authorize] 屬性(Attribute)的 API action 都會自動套用 Bearer 的安全設定,在線上發送 API 要求時,就會自動送出 Bearer Token。

    上述設定並不是很好理解技術細節,我是透過反覆閱讀與理解 AspNetCoreOperationSecurityScopeProcessor.cs 原始碼才知道的。

  2. 設定好之後,你的 Swagger UI 會有點改變,文件的右上角會出現一個 Authorize 按鈕,而且每個 API 右邊也會出現一個「鎖頭」的圖示:

    image

  3. 這裡特別要提醒的地方,就是當你按下 Authorize 按鈕後,要在對話框內設定 Token 到 Swagger UI 時,必須自己手動輸入 Bearer 開頭,加一個空白字元,然後再貼上你的 JWT Token,這樣才能正確設定!

    image

2021-06-02 更新:因為上述設定在 Swagger UI 設定 Authorize 的 Token 時,還需要手動加上 Bearer 才能正常運作,以下我提供另一種更好的範例。

  1. 以下是 JWT 驗證授權的設定範例:

    以下範例中,這個 OpenApiSecurityScheme 物件請勿加上 NameIn 屬性,否則產生出來的 OpenAPI Spec 格式會有錯誤!

    services.AddOpenApiDocument(config =>
    {
        // 這個 OpenApiSecurityScheme 物件請勿加上 Name 與 In 屬性,否則產生出來的 OpenAPI Spec 格式會有錯誤!
        var apiScheme = new OpenApiSecurityScheme()
        {
            Type = OpenApiSecuritySchemeType.Http,
            Scheme = JwtBearerDefaults.AuthenticationScheme,
            BearerFormat = "JWT", // for documentation purposes (OpenAPI only)
            Description = "Copy JWT Token into the value field: {token}"
        };
    
        // 這裡會同時註冊 SecurityDefinition (.components.securitySchemes) 與 SecurityRequirement (.security)
        config.AddSecurity("Bearer", Enumerable.Empty<string>(), apiScheme);
    
        // 這段是為了將 "Bearer" 加入到 OpenAPI Spec 裡 Operator 的 security (Security requirements) 中
        config.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor());
    });
    

    如果只想在 Swagger UI 針對特定有套用 [Authorize] 的 API 才出現鎖頭的話,可以改用以下方法宣告,這樣你就更容易看出哪些 API 才是需要通過 JWT 驗證與授權的:

    services.AddOpenApiDocument(config =>
    {
        // 這個 OpenApiSecurityScheme 物件請勿加上 Name 與 In 屬性,否則產生出來的 OpenAPI Spec 格式會有錯誤!
        var apiScheme = new OpenApiSecurityScheme()
        {
            Type = OpenApiSecuritySchemeType.Http,
            Scheme = JwtBearerDefaults.AuthenticationScheme,
            BearerFormat = "JWT", // for documentation purposes (OpenAPI only)
            Description = "Copy JWT Token into the value field: {token}"
        };
    
        // 這裡會同時註冊 SecurityDefinition (.components.securitySchemes) 與 SecurityRequirement (.security)
        config.AddSecurity("Bearer", apiScheme);
    
        // 這段是為了將 "Bearer" 加入到 OpenAPI Spec 裡 Operator 的 security (Security requirements) 中
        config.OperationProcessors.Add(new AspNetCoreOperationSecurityScopeProcessor());
    });
    

    這裡的 new AspNetCoreOperationSecurityScopeProcessor() 預設安全名稱就是 Bearer,因此不用特別設定。但記得 config.AddSecurity() 的第一個參數一定要設定成 Bearer 才正確。

  2. 設定好之後,你的 Swagger UI 會有點改變,文件的右上角會出現一個 Authorize 按鈕,而且每個 API 右邊也會出現一個「鎖頭」的圖示:

    image

  3. 這裡特別要提醒的地方,就是當你按下 Authorize 按鈕後,要在對話框內設定 Token 到 Swagger UI 時,不需要自己手動輸入 Bearer 開頭,直接貼上你的 JWT Token 就可以了!🔥

完整原始碼

我特別製作了一個 ASP․NET Core 3.1 範例專案 (2021-06-02: 目前專案已經升級到 .NET 5.0 版),套用本篇文章所提到的大部分用法,應該是相當具有參考價值。

相關連結

留言評論