﻿# 使用 .NET CLI 的 dotnet openapi 快速建立 Web API 用戶端函式庫的方法

在 Visual Studio 2019 裡面使用含有 OpenAPI 規格的 Web API 已經是十分便利，只要規格寫的好，Web API 用戶端函式庫只要一秒鐘就可以產生。但其實這些好用的功能背後都是靠 MSBuild 與 NSwag 做到，今天花了一整天把所有技術細節釐清，釐清之後對這整套作法是如此的豁然開朗，感覺很棒。這篇文章我就來寫寫今日的研究心得！

### 使用 `Microsoft.dotnet-openapi` 工具

微軟有發展一套 [Microsoft.dotnet-openapi](https://www.nuget.org/packages/Microsoft.dotnet-openapi) 工具，並且內建到 Visual Studio 2019 之中，但事實上你完全可以用命令列的方式執行。這也意味著你在 macOS 或 Linux 也可以利用這個工具快速產生專案所需的 Web API 用戶端函式庫。

1.  建立 Console 專案並透過 VSCode 開啟專案
    
    ```bash
    dotnet new console -n apiclient1
    code apiclient1
    ```
    
2.  安裝 [NSwag.ApiDescription.Client](https://www.nuget.org/packages/NSwag.ApiDescription.Client) 套件
    
    ```bash
    dotnet add package NSwag.ApiDescription.Client
    ```
    
    > 注意: 這個套件同時包含了 [NSwag.MSBuild](https://www.nuget.org/packages/NSwag.MSBuild/) 與 [Microsoft.Extensions.ApiDescription.Client](https://www.nuget.org/packages/Microsoft.Extensions.ApiDescription.Client/) 套件的相依性，所以會自動安裝起來。
    
3.  將 OpenAPI v3 規格文件加入專案
    
    先安裝 [Microsoft.dotnet-openapi](https://www.nuget.org/packages/Microsoft.dotnet-openapi) 這套 .NET 全域工具
    
    ```bash
    dotnet tool install -g Microsoft.dotnet-openapi
    ```
    
    將遠端 Web API 的 OpenAPI v3 規格加入專案
    
    ```bash
    dotnet openapi add url -p apiclient1.csproj https://localhost:5001/swagger/v1/swagger.json
    ```
    
    當你將 OpenAPI v3 規格文件加入專案後，專案內會新增一個 `swagger.json` 檔案，那是因為我們的 URL 結尾的檔案是 `swagger.json` 的關係。
    
    如果你無法從開發環境連到遠端的 Web API 以取得 OpenAPI 文件，你也可以人工取得該檔案，自己放到專案中，然後輸入以下命令加入檔案：
    
    ```bash
    dotnet openapi add file -p apiclient1.csproj swagger.json
    ```
    
    加入 OpenAPI 規格文件後，你的 `*.csproj` 專案檔中會添加一段 `<ItemGroup>` 項目，其內容如下：
    
    ```xml
    <ItemGroup>
      <OpenApiReference Include="swagger.json" SourceUrl="https://localhost:5001/swagger/v1/swagger.json" />
    </ItemGroup>
    ```
    
    > 注意: 上述命令會自動安裝 [NSwag.ApiDescription.Client](https://www.nuget.org/packages/NSwag.ApiDescription.Client) 與 [Newtonsoft.Json](https://www.nuget.org/packages/Newtonsoft.Json) 套件。不過自動安裝的 [NSwag.ApiDescription.Client](https://www.nuget.org/packages/NSwag.ApiDescription.Client) 與 [Newtonsoft.Json](https://www.nuget.org/packages/Newtonsoft.Json) 套件並不會套用 `<PrivateAssets>all</PrivateAssets>` 屬性，所以我才在上一個步驟先手動安裝了這個套件，手動安裝的設定才是對的。
    
4.  建置專案
    
    接著我們直接執行建置命令：
    
    ```bash
    dotnet build
    ```
    
    這個動作會自動產生一個 `obj\swaggerClient.cs` 的類別，而這個類別就是我們專案可以用的 C# 用戶端函式庫，裡面將包含我們在 OpenAPI 中定義的所有模型，以及所有 API 的操作方法(Operator)，全部都是強型別的定義。
    
5.  更新 OpenAPI v3 規格與重新產生原始碼
    
    當你的 Web API 有更新時，其 OpenAPI 規格也會有改變，這時我們的 Web API 用戶端函式庫就要連帶更新，以下是更新的命令：
    
    ```bash
    dotnet openapi refresh -p apiclient1.csproj https://localhost:5001/swagger/v1/swagger.json
    ```
    
    > 注意：更新完之後，要記得刪除 `obj` 目錄，並重新執行 `dotnet build` 才能重新產生新版的程式碼。
    

### 關於 `<OpenApiReference>` 的技術細節

基本上，你安裝的 [NSwag.ApiDescription.Client](https://www.nuget.org/packages/NSwag.ApiDescription.Client) 套件，包含了一個 [NSwag.MSBuild](https://www.nuget.org/packages/NSwag.MSBuild/) 套件，以及一個 [Microsoft.Extensions.ApiDescription.Client](https://www.nuget.org/packages/Microsoft.Extensions.ApiDescription.Client/) 套件，而這三個套都包含了一些 MSBuild 的屬性與目標定義。

1.  [NSwag.ApiDescription.Client](https://www.nuget.org/packages/NSwag.ApiDescription.Client)
    
    `NSwag.ApiDescription.Client.props` 屬性檔，定義了 `OpenApiReference.CodeGenerator` 這個屬性的預設值，你可以輕易的看出預設值為 `NSwagCSharp`，也就是說這套工具預設會產生 C# 的 Web API 用戶端函式庫。
    
    ```xml
    <?xml version="1.0" encoding="utf-8" standalone="no"?>
    <Project>
      <!-- Reset well-known metadata of the code generator item groups to make NSwag C# generator the default. -->
      <ItemDefinitionGroup>
        <OpenApiReference>
          <CodeGenerator>NSwagCSharp</CodeGenerator>
        </OpenApiReference>
        <OpenApiProjectReference>
          <CodeGenerator>NSwagCSharp</CodeGenerator>
        </OpenApiProjectReference>
      </ItemDefinitionGroup>
    </Project>
    ```
    
    `NSwag.ApiDescription.Client.targets` 目標檔案，定義了 `GenerateNSwagCSharp` 與 `GenerateNSwagTypeScript` 這兩個目標，可以讓你輕鬆透過 MSBuild 的定義 (也就是 `*.csproj` 專案檔)，就能夠輕鬆的產出 Web API 用戶端函式庫的程式碼。而所有會執行的命令與參數，也都完完整整的寫在這裡，你只要看的懂，就可以自行客製調整輸出的結果。
    
    ```xml
    <?xml version="1.0" encoding="utf-8" standalone="no"?>
    <Project>
      <PropertyGroup>
        <_NSwagCommand>$(NSwagExe)</_NSwagCommand>
        <_NSwagCommand
            Condition="'$(MSBuildRuntimeType)' == 'Core'">dotnet --roll-forward-on-no-candidate-fx 2 "$(NSwagDir_Core31)/dotnet-nswag.dll"</_NSwagCommand>
      </PropertyGroup>
    
      <!-- OpenApiReference support for C# -->
    
      <Target Name="GenerateNSwagCSharp">
        <ItemGroup>
          <!-- @(CurrentOpenApiReference) item group will never contain more than one item. -->
          <CurrentOpenApiReference>
            <Command>$(_NSwagCommand) openapi2csclient /className:%(ClassName) /namespace:%(Namespace)</Command>
          </CurrentOpenApiReference>
          <CurrentOpenApiReference>
            <Command Condition="! %(FirstForGenerator)">%(Command) /GenerateExceptionClasses:false</Command>
          </CurrentOpenApiReference>
          <CurrentOpenApiReference>
            <Command>%(Command) /input:"%(FullPath)" /output:"%(OutputPath)" %(Options)</Command>
          </CurrentOpenApiReference>
        </ItemGroup>
    
        <Message Importance="high" Text="%0AGenerateNSwagCSharp:" />
        <Message Importance="high" Text="  %(CurrentOpenApiReference.Command)" />
    
        <Exec Command="%(CurrentOpenApiReference.Command)" LogStandardErrorAsError="true" />
      </Target>
    
      <!-- OpenApiReference support for TypeScript -->
    
      <Target Name="GenerateNSwagTypeScript">
        <ItemGroup>
          <!-- @(CurrentOpenApiReference) item group will never contain more than one item. -->
          <CurrentOpenApiReference>
            <Command>$(_NSwagCommand) swagger2tsclient /className:%(ClassName) /namespace:%(Namespace)</Command>
          </CurrentOpenApiReference>
          <CurrentOpenApiReference>
            <Command>%(Command) /input:"%(FullPath)" /output:"%(OutputPath)" %(Options)</Command>
          </CurrentOpenApiReference>
        </ItemGroup>
    
        <Message Importance="high" Text="%0AGenerateNSwagTypeScript:" />
        <Message Importance="high" Text="  %(CurrentOpenApiReference.Command)" />
        <Exec Command="%(CurrentOpenApiReference.Command)" LogStandardErrorAsError="true" />
      </Target>
    </Project>
    ```
    
2.  [NSwag.MSBuild](https://www.nuget.org/packages/NSwag.MSBuild/)
    
    `NSwag.MSBuild.props` 屬性檔，裡面包含了 NSwag 執行檔的所在路徑。
    
    ```xml
    <?xml version="1.0" encoding="utf-8" ?>
    <Project xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
      <PropertyGroup>
        <NSwagExe>"$(MSBuildThisFileDirectory)../tools/Win/NSwag.exe"</NSwagExe>
        <NSwagExe_x86>"$(MSBuildThisFileDirectory)../tools/Win/NSwag.x86.exe"</NSwagExe_x86>
        <NSwagExe_Core21>dotnet "$(MSBuildThisFileDirectory)../tools/NetCore21/dotnet-nswag.dll"</NSwagExe_Core21>
        <NSwagExe_Core31>dotnet "$(MSBuildThisFileDirectory)../tools/NetCore31/dotnet-nswag.dll"</NSwagExe_Core31>
        <NSwagExe_Net50>dotnet "$(MSBuildThisFileDirectory)../tools/Net50/dotnet-nswag.dll"</NSwagExe_Net50>
    
        <NSwagDir>$(MSBuildThisFileDirectory)../tools/Win/</NSwagDir>
        <NSwagDir_Core21>$(MSBuildThisFileDirectory)../tools/NetCore21/</NSwagDir_Core21>
        <NSwagDir_Core31>$(MSBuildThisFileDirectory)../tools/NetCore31/</NSwagDir_Core31>
        <NSwagDir_Net50>$(MSBuildThisFileDirectory)../tools/Net50/</NSwagDir_Net50>
      </PropertyGroup>
    </Project>
    ```
    
3.  [Microsoft.Extensions.ApiDescription.Client](https://www.nuget.org/packages/Microsoft.Extensions.ApiDescription.Client/)
    
    `Microsoft.Extensions.ApiDescription.Client.props` 屬性檔，主要用來設定 `<OpenApiReference>` 與 `<OpenApiProjectReference>` 各屬性的預設值，這裡的設定有助於讓我們在執行 `MSBuild.exe` 的時候，從命令列直接設定屬性值！（利用 `/p:PropertyName=PropertyValue` 語法）
    
    `Microsoft.Extensions.ApiDescription.Client.targets` 目標檔，裡面有一堆微軟預先定義好的 **目標** (Target)，用來將需要產生程式碼或文件的動作整合起來。
    

簡單來說，當你的 `*.csproj` 專案檔使用了 `<OpenApiReference>` 項目，就會觸發在 `NSwag.ApiDescription.Client.targets` 目標檔案中的特定目標，你可以從內容組合出確切的執行命令與參數，其完整的命令如下：

```bash
dotnet --roll-forward-on-no-candidate-fx 2 "$(NSwagDir_Core31)/dotnet-nswag.dll" openapi2csclient /className:%(ClassName) /namespace:%(Namespace) /input:"%(FullPath)" /output:"%(OutputPath)" %(Options)
```

由此可知，透過 MSBuild 來執行 NSwag 命令，你可以在 `*.csproj` 檔案中可以設定的屬性名稱，大概就只有以下幾種：

-   `ClassName`
    
    透過 MSBuild 自動產生的 Web API 用戶端函式庫，其 **類別名稱** 預設為 **OpenAPI 規格的檔案名稱**加上 `Client` 結尾，所以如果檔名是 `swagger.json` 的話，其類別名稱就是 `swaggerClient`。因此，我們通常都會特別設定 `ClassName` 在 `<OpenApiReference>` 項目的屬性中。
    
    > 一般來說我們都會以 `Client` 當成類別名稱的尾碼，例如：`DuotifyServiceClient`。
    
-   `Namespace`
    
    透過 MSBuild 自動產生的 Web API 用戶端函式庫，其**類別**所使用的**命名空間**，就是當前專案的預設命名空間。如果你需要調整的話，可以透過這個屬性進行設定。
    
-   `OutputPath`
    
    透過 MSBuild 自動產生的 Web API 用戶端函式庫，其輸出的**實體檔案**會置於 `obj\` 加上 **OpenAPI 規格的檔案名稱** 再加上 `Client` 結尾，所以如果檔名是 `swagger.json` 的話，其實體檔案名稱就是 `obj/swaggerClient.cs`。
    
    > 一般來說，這個實體檔案會在建置的時候自動產生，所以檔名其實不是太重要，但如果想要將實體檔案保留下來，你可能會想取一個更容易理解的實體檔案名稱。
    
-   `Options`
    
    透過 MSBuild 自動產生的 Web API 用戶端函式庫，預設是透過執行 `NSwag` 命令列工具產生的，而 `NSwag` 命令列工具有數十個可以客製化輸出的選項，所有額外的選項你都可以透過這個屬性來進行設定！
    
    首先，你要先安裝 [NSwag.ConsoleCore](https://www.nuget.org/packages/NSwag.ConsoleCore) 全域工具：
    
    ```bash
    dotnet tool install -g NSwag.ConsoleCore
    ```
    
    然後透過以下命令查出所有可用的選項：
    
    ```bash
    nswag help openapi2csclient
    ```
    

### 分享幾個 `<OpenApiReference>` 設定範例

1.  預設範例
    
    ```xml
    <ItemGroup>
      <OpenApiReference Include="swagger.json" SourceUrl="https://localhost:5001/swagger/v1/swagger.json" />
    </ItemGroup>
    ```
    
2.  自訂類別名稱與輸出檔名，建立 interface 方便後續設定 DI
    
    ```xml
    <ItemGroup>
      <OpenApiReference Include="swagger.json" SourceUrl="https://localhost:5001/swagger/v1/swagger.json">
        <ClassName>DuotifyServiceClient</ClassName>
        <OutputPath>DuotifyServiceClient.cs</OutputPath>
        <Options>/GenerateClientInterfaces:true</Options>
      </OpenApiReference>
    </ItemGroup>
    ```
    
3.  自訂類別名稱與輸出檔名，建立 interface 方便後續設定 DI，改用 `System.Text.Json` 作為主要 JSON 處理函式
    
    ```xml
    <ItemGroup>
      <OpenApiReference Include="swagger.json" SourceUrl="https://localhost:5001/swagger/v1/swagger.json">
        <ClassName>DuotifyServiceClient</ClassName>
        <OutputPath>DuotifyServiceClient.cs</OutputPath>
        <Options>/GenerateClientInterfaces:true /JsonLibrary:SystemTextJson</Options>
      </OpenApiReference>
    </ItemGroup>
    ```
    
4.  自訂類別名稱與輸出檔名，從建構式關閉 BaseUrl 的注入、建立 interface 方便後續設定 DI
    
    ```xml
    <ItemGroup>
      <OpenApiReference Include="swagger.json" SourceUrl="https://localhost:5001/swagger/v1/swagger.json">
        <ClassName>DuotifyServiceClient</ClassName>
        <OutputPath>DuotifyServiceClient.cs</OutputPath>
        <Options>/UseBaseUrl:false /GenerateClientInterfaces:true</Options>
      </OpenApiReference>
    </ItemGroup>
    ```
    
    若使用 `/UseBaseUrl:false` 參數設定，意味著你必須從注入的 `HttpClient` 設定 `httpClient.BaseAddress` 屬性，否則將無法正確發出 HTTP 要求。
    
    以下是從 ASP.NET Core 中設定 `HttpClient` 服務的設定範例：
    
    ```csharp
    services.AddHttpClient<IDuotifyServiceClient, DuotifyServiceClient>(
        (provider, client) => {
        client.BaseAddress = new Uri(Configuration.GetValue(
            "DuotifyServiceBaseAddress", "https://example.com/"));
    });
    ```
    
    > 詳細用法請參見 [Use IHttpClientFactory to implement resilient HTTP requests](https://learn.microsoft.com/en-us/dotnet/architecture/microservices/implement-resilient-applications/use-httpclientfactory-to-implement-resilient-http-requests?WT.mc_id=DT-MVP-4015686#benefits-of-using-ihttpclientfactory) 文章。
    
5.  自訂類別名稱與輸出檔名，從建構式關閉 BaseUrl 的注入、從 Client 類別套用一個基底類別 (`BaseClient`)
    
    ```xml
    <ItemGroup>
      <OpenApiReference Include="swagger.json" SourceUrl="https://localhost:5001/swagger/v1/swagger.json">
        <ClassName>DuotifyServiceClient</ClassName>
        <OutputPath>DuotifyServiceClient.cs</OutputPath>
        <Options>/UseBaseUrl:false /ClientBaseClass:BaseClient</Options>
      </OpenApiReference>
    </ItemGroup>
    ```
    
6.  自訂類別名稱與輸出檔名，主要的 Client 類別套用一個基底類別 (Base Class) 並在建構式新增一個可額外傳入的 ClientConfiguration 類別
    
    ```xml
    <ItemGroup>
      <OpenApiReference Include="swagger.json" SourceUrl="https://localhost:5001/swagger/v1/swagger.json">
        <ClassName>DuotifyServiceClient</ClassName>
        <OutputPath>DuotifyServiceClient.cs</OutputPath>
        <Options>/ClientBaseClass:BaseClient /ConfigurationClass:ClientConfiguration</Options>
      </OpenApiReference>
    </ItemGroup>
    ```
    
    > 這招很適合透過 `ClientConfiguration` 用來修改與調整 Client 類別的初始狀態
    
7.  自動產生 TypeScript 類別與介面，使用 Angular 範本（隱含使用 HttpClient 最為發出 HTTP 的服務）
    
    ```xml
    <ItemGroup>
      <OpenApiReference Include="swagger.json">
        <SourceUrl>https://localhost:5001/swagger/v1/swagger.json</SourceUrl>
        <CodeGenerator>NSwagTypeScript</CodeGenerator>
        <ClassName>DuotifyServiceClient</ClassName>
        <OutputPath>DuotifyServiceClient.ts</OutputPath>
        <Options>/GenerateClientInterfaces:true /Template:Angular</Options>
      </OpenApiReference>
    </ItemGroup>
    ```
    
    Angular 必須在 `AppModule` 設定一組名為 `API_BASE_URL` 的 DI 才能讓函式庫正常運作：
    
    ```typescript
    @NgModule({
        imports: [ ... ],
        declarations: [ ... ],
        providers: [
            {
                provide: API_BASE_URL,
                useValue: environment.apiRoot
            },
            ...
        ]
        exports: [ ... ]
    })
    export class AppModule {}
    ```
    
8.  自動產生 TypeScript 類別與介面，使用 Fetch 作為發送 AJAX 的主要方法（Vue 或 React 可以考慮用這個）
    
    ```xml
    <ItemGroup>
      <OpenApiReference Include="swagger.json">
        <SourceUrl>https://localhost:5001/swagger/v1/swagger.json</SourceUrl>
        <CodeGenerator>NSwagTypeScript</CodeGenerator>
        <ClassName>DuotifyServiceClient</ClassName>
        <OutputPath>DuotifyServiceClient.ts</OutputPath>
        <Options>/GenerateClientInterfaces:true /Template:Fetch</Options>
      </OpenApiReference>
    </ItemGroup>
    ```
    
9.  自動產生 TypeScript 類別與介面，使用 `JQueryCallbacks` 作為發送 AJAX 的主要方法，**跨來源呼叫**時可一併設定 `withCredentials` 屬性
    
    ```xml
    <ItemGroup>
      <OpenApiReference Include="swagger.json">
        <SourceUrl>https://localhost:5001/swagger/v1/swagger.json</SourceUrl>
        <CodeGenerator>NSwagTypeScript</CodeGenerator>
        <ClassName>DuotifyServiceClient</ClassName>
        <OutputPath>DuotifyServiceClient.ts</OutputPath>
        <Options>/GenerateClientInterfaces:true /Template:Angular /WithCredentials:true</Options>
      </OpenApiReference>
    </ItemGroup>
    ```
    

### 刪除 `<OpenApiReference>` 設定

基本上有兩種方法可以刪除現有的 OpenAPI 參考：

1.  執行以下 `dotnet openapi remove` 命令
    
    ```bash
    dotnet openapi remove -p apiclient1.csproj swagger.json
    ```
    
2.  手動刪除 `apiclient1.csproj` 中包含 `<OpenApiReference>` 的 `<ItemGroup>`
    

> 請記得將 `obj` 目錄完整刪除，才不會有殘留的檔案導致編譯錯誤！

### 如何在執行 `dotnet clean` 的時候完整清除 `bin` 與 `obj` 目錄

使用 .NET CLI 執行 `dotnet clean` 的時候，只會清除 `dotnet build` 的過程中產生的檔案。簡單來說，就是很多檔案並不會在 `dotnet clean` 的時候被刪除，像是本篇文章所講的那些自動產生的原始碼，在更新或移除設定的時候都不會自動刪除。這很有可能會導致許多奇怪的問題發生！

這邊我也分享一個小技巧，你只要將以下片段加入到 `*.csproj` 專案檔中，就可以自動在 `dotnet clean` 的時候完整清除 `bin` 與 `obj` 目錄：

```xml
<Target Name="PostClean" AfterTargets="Clean">
  <RemoveDir Directories="$(BaseIntermediateOutputPath)" /> <!-- obj -->
  <RemoveDir Directories="$(BaseOutputPath)" /> <!-- bin -->
</Target>
```

如果只想清空 `obj` 就好的話，請用以下範例：

```xml
<Target Name="PostClean" AfterTargets="Clean">
  <RemoveDir Directories="$(BaseIntermediateOutputPath)" />
</Target>
```

> 參考：[dotnet clean command won't delete bin and obj folder that produced by dotnet publish · Issue #12304 · dotnet/docs](https://github.com/dotnet/docs/issues/12304#issuecomment-755672578)

### 相關連結

-   Microsoft Docs
    -   [Develop ASP.NET Core apps using OpenAPI | Microsoft Docs](https://learn.microsoft.com/en-us/aspnet/core/web-api/microsoft.dotnet-openapi?view=aspnetcore-5.0&WT.mc_id=DT-MVP-4015686)
    -   [MSBuild - Visual Studio | Microsoft Docs](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild?view=vs-2019&WT.mc_id=DT-MVP-4015686)
        -   [MSBuild Well-known Item Metadata - Visual Studio | Microsoft Docs](https://learn.microsoft.com/en-us/visualstudio/msbuild/msbuild-well-known-item-metadata?view=vs-2019&WT.mc_id=DT-MVP-4015686)
-   NSwag
    -   [NSwag.MSBuild · RicoSuter/NSwag Wiki](https://github.com/RicoSuter/NSwag/wiki/NSwag.MSBuild)
    -   [CommandLine · RicoSuter/NSwag Wiki](https://github.com/RicoSuter/NSwag/wiki/CommandLine)
        -   [NSwag.Commands\_Help.txt](https://gist.github.com/doggy8088/2a031ec0d4b54debfbe406546ea880e3)
    -   [c# - Generate NSwag client as part of the build - Stack Overflow](https://stackoverflow.com/questions/59393267/generate-nswag-client-as-part-of-the-build)
-   OpenApiReference
    -   [Using OpenAPI Auto-Generated Clients in ASP.NET Core](https://markheath.net/post/openapi-autogen-aspnetcore)
    -   [Using OpenApiReference To Generate Open API Client Code](http://stevetalkscode.co.uk/openapireference-commands)
