在 Visual Studio 2019 裡面使用含有 OpenAPI 規格的 Web API 已經是十分便利,只要規格寫的好,Web API 用戶端函式庫只要一秒鐘就可以產生。但其實這些好用的功能背後都是靠 MSBuild 與 NSwag 做到,今天花了一整天把所有技術細節釐清,釐清之後對這整套作法是如此的豁然開朗,感覺很棒。這篇文章我就來寫寫今日的研究心得!
使用 Microsoft.dotnet-openapi
工具
微軟有發展一套 Microsoft.dotnet-openapi 工具,並且內建到 Visual Studio 2019 之中,但事實上你完全可以用命令列的方式執行。這也意味著你在 macOS 或 Linux 也可以利用這個工具快速產生專案所需的 Web API 用戶端函式庫。
-
建立 Console 專案並透過 VSCode 開啟專案
dotnet new console -n apiclient1
code apiclient1
-
安裝 NSwag.ApiDescription.Client 套件
dotnet add package NSwag.ApiDescription.Client
注意: 這個套件同時包含了 NSwag.MSBuild 與 Microsoft.Extensions.ApiDescription.Client 套件的相依性,所以會自動安裝起來。
-
將 OpenAPI v3 規格文件加入專案
先安裝 Microsoft.dotnet-openapi 這套 .NET 全域工具
dotnet tool install -g Microsoft.dotnet-openapi
將遠端 Web API 的 OpenAPI v3 規格加入專案
dotnet openapi add url -p apiclient1.csproj https://localhost:5001/swagger/v1/swagger.json
當你將 OpenAPI v3 規格文件加入專案後,專案內會新增一個 swagger.json
檔案,那是因為我們的 URL 結尾的檔案是 swagger.json
的關係。
如果你無法從開發環境連到遠端的 Web API 以取得 OpenAPI 文件,你也可以人工取得該檔案,自己放到專案中,然後輸入以下命令加入檔案:
dotnet openapi add file -p apiclient1.csproj swagger.json
加入 OpenAPI 規格文件後,你的 *.csproj
專案檔中會添加一段 <ItemGroup>
項目,其內容如下:
<ItemGroup>
<OpenApiReference Include="swagger.json" SourceUrl="https://localhost:5001/swagger/v1/swagger.json" />
</ItemGroup>
注意: 上述命令會自動安裝 NSwag.ApiDescription.Client 與 Newtonsoft.Json 套件。不過自動安裝的 NSwag.ApiDescription.Client 與 Newtonsoft.Json 套件並不會套用 <PrivateAssets>all</PrivateAssets>
屬性,所以我才在上一個步驟先手動安裝了這個套件,手動安裝的設定才是對的。
-
建置專案
接著我們直接執行建置命令:
dotnet build
這個動作會自動產生一個 obj\swaggerClient.cs
的類別,而這個類別就是我們專案可以用的 C# 用戶端函式庫,裡面將包含我們在 OpenAPI 中定義的所有模型,以及所有 API 的操作方法(Operator),全部都是強型別的定義。
-
更新 OpenAPI v3 規格與重新產生原始碼
當你的 Web API 有更新時,其 OpenAPI 規格也會有改變,這時我們的 Web API 用戶端函式庫就要連帶更新,以下是更新的命令:
dotnet openapi refresh -p apiclient1.csproj https://localhost:5001/swagger/v1/swagger.json
注意:更新完之後,要記得刪除 obj
目錄,並重新執行 dotnet build
才能重新產生新版的程式碼。
關於 <OpenApiReference>
的技術細節
基本上,你安裝的 NSwag.ApiDescription.Client 套件,包含了一個 NSwag.MSBuild 套件,以及一個 Microsoft.Extensions.ApiDescription.Client 套件,而這三個套都包含了一些 MSBuild 的屬性與目標定義。
-
NSwag.ApiDescription.Client
NSwag.ApiDescription.Client.props
屬性檔,定義了 OpenApiReference.CodeGenerator
這個屬性的預設值,你可以輕易的看出預設值為 NSwagCSharp
,也就是說這套工具預設會產生 C# 的 Web API 用戶端函式庫。
<?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 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>
-
NSwag.MSBuild
NSwag.MSBuild.props
屬性檔,裡面包含了 NSwag 執行檔的所在路徑。
<?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>
-
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
目標檔案中的特定目標,你可以從內容組合出確切的執行命令與參數,其完整的命令如下:
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 全域工具:
dotnet tool install -g NSwag.ConsoleCore
然後透過以下命令查出所有可用的選項:
nswag help openapi2csclient
分享幾個 <OpenApiReference>
設定範例
-
預設範例
<ItemGroup>
<OpenApiReference Include="swagger.json" SourceUrl="https://localhost:5001/swagger/v1/swagger.json" />
</ItemGroup>
-
自訂類別名稱與輸出檔名,建立 interface 方便後續設定 DI
<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>
-
自訂類別名稱與輸出檔名,建立 interface 方便後續設定 DI,改用 System.Text.Json
作為主要 JSON 處理函式
<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>
-
自訂類別名稱與輸出檔名,從建構式關閉 BaseUrl 的注入、建立 interface 方便後續設定 DI
<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
服務的設定範例:
services.AddHttpClient<IDuotifyServiceClient, DuotifyServiceClient>(
(provider, client) => {
client.BaseAddress = new Uri(Configuration.GetValue(
"DuotifyServiceBaseAddress", "https://example.com/"));
});
詳細用法請參見 Use IHttpClientFactory to implement resilient HTTP requests 文章。
-
自訂類別名稱與輸出檔名,從建構式關閉 BaseUrl 的注入、從 Client 類別套用一個基底類別 (BaseClient
)
<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>
-
自訂類別名稱與輸出檔名,主要的 Client 類別套用一個基底類別 (Base Class) 並在建構式新增一個可額外傳入的 ClientConfiguration 類別
<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 類別的初始狀態
-
自動產生 TypeScript 類別與介面,使用 Angular 範本(隱含使用 HttpClient 最為發出 HTTP 的服務)
<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 才能讓函式庫正常運作:
@NgModule({
imports: [ ... ],
declarations: [ ... ],
providers: [
{
provide: API_BASE_URL,
useValue: environment.apiRoot
},
...
]
exports: [ ... ]
})
export class AppModule {}
-
自動產生 TypeScript 類別與介面,使用 Fetch 作為發送 AJAX 的主要方法(Vue 或 React 可以考慮用這個)
<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>
-
自動產生 TypeScript 類別與介面,使用 JQueryCallbacks
作為發送 AJAX 的主要方法,跨來源呼叫時可一併設定 withCredentials
屬性
<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 參考:
-
執行以下 dotnet openapi remove
命令
dotnet openapi remove -p apiclient1.csproj swagger.json
-
手動刪除 apiclient1.csproj
中包含 <OpenApiReference>
的 <ItemGroup>
請記得將 obj
目錄完整刪除,才不會有殘留的檔案導致編譯錯誤!
如何在執行 dotnet clean
的時候完整清除 bin
與 obj
目錄
使用 .NET CLI 執行 dotnet clean
的時候,只會清除 dotnet build
的過程中產生的檔案。簡單來說,就是很多檔案並不會在 dotnet clean
的時候被刪除,像是本篇文章所講的那些自動產生的原始碼,在更新或移除設定的時候都不會自動刪除。這很有可能會導致許多奇怪的問題發生!
這邊我也分享一個小技巧,你只要將以下片段加入到 *.csproj
專案檔中,就可以自動在 dotnet clean
的時候完整清除 bin
與 obj
目錄:
<Target Name="PostClean" AfterTargets="Clean">
<RemoveDir Directories="$(BaseIntermediateOutputPath)" /> <!-- obj -->
<RemoveDir Directories="$(BaseOutputPath)" /> <!-- bin -->
</Target>
如果只想清空 obj
就好的話,請用以下範例:
<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
相關連結
- Microsoft Docs
- NSwag
- OpenApiReference