Note: see the link below for the English version of this article.
https://duongnt.com/openapi-generator-mustache-customize
Chúng ta có thể sử dụng nguyên xi code do OpenAPI Generator sinh ra. Tuy nhiên trong một số tình huống, phần code đó chưa hẳn đã là tối ưu. Trong bài hôm nay, chúng ta sẽ tìm hiểu cách tùy biến OpenAPI Generator bằng cách sử dụng Mustache template. Đây là phần mở rộng của một bài viết trước đây về OpenAPI Generator, các bạn nên đọc qua bài đó trước khi đọc bài này.
Toàn bộ code trong bài nằm ở đường link bên dưới.
https://github.com/duongntbk/OpenAPIGeneratorDemo
Một vấn đề trong thực tế
Dưới đây là phiên bản đơn giản hóa của một vấn đề tôi gặp phải trong một dự án gần đây. Như đã nói trong bài trước, chúng ta sinh client cho các API Maritimum và Pecunia. Những client này có thể tự xử lý lỗi và bọc chúng trong một kiểu exception với tên gọi là ApiException
. Vấn đề là mỗi client lại có một phiên bản ApiException
riêng. Maritimum có bản này với namespace là MaritimumClient.Client
; còn Pecunia có bản này với namespace là Pecunia.Client
.
Từ mã nguồn, ta có thể thấy là 2 exception này giống hệt nhau chỉ khác namespace. Nhưng framework sẽ coi chúng là 2 exception hoàn toàn biệt lập. Điều này sẽ khiến ta gặp khó khăn khi dùng cả 2 client cùng một lúc. Nếu như ta muốn xử lý lỗi của cả 2 API, ta sẽ phải viết code như sau.
try
{
var portTask = portApi.PortsShowAsync(portUuid);
var accountTask = accountApi.AccountsShowAsync(accountUuid);
await Task.WhenAll(portTask, accountTask);
}
catch (MaritimumClient.Client.ApiException ex)
{
if (ex.ErrorCode == 404)
{
// Một vài xử lý
}
// Một vài xử lý khác
}
catch (PecuniaClient.Client.ApiException ex)
{
if (ex.ErrorCode == 404)
{
// Một vài xử lý
}
// Một vài xử lý khác
}
Ta sẽ phải lặp lại 2 đoạn code với lệnh catch
ở tất cả các chỗ cần xử lý lỗi của API. Nếu như số API càng nhiều thì code để xử lý lỗi sẽ càng bị trùng lặp. Nếu tất cả client đều dùng chung 1 exception thì ta sẽ đơn giản hóa được đoạn code ở trên.
Mustache template trong OpenAPI Generator
Giới thiệu Mustache template
Có thể coi Mustache template là bản thiết kế để OpenAPI Generator sinh ra code cho client. Mỗi lớp trong từng ngôn ngữ mà OpenAPI Generator hỗ trợ đều có 1 template tương ứng. Các bạn có thể tham khảo danh sách đầy đủ tại đây. Vì ta muốn tùy biến client viết bằng C# .NET Core nên ta sẽ tập trung vào các template trong folder csharp-netcore.
Ta sẽ lấy template của HttpMethod làm ví dụ.
{{>partial_header}}
namespace {{packageName}}.Client
{
/// <summary>
/// Http methods supported by swagger
/// </summary>
public enum HttpMethod
{
// Danh sách các HTTP Method
}
}
Và đây là code tương ứng của MaritimumClient.
/*
* Maritimum
*
* a sample API to return ships and ports information.
*
* The version of the OpenAPI document: v1
* Generated by: https://github.com/openapitools/openapi-generator.git
*/
namespace MaritimumClient.Client
{
/// <summary>
/// Http methods supported by swagger
/// </summary>
public enum HttpMethod
{
// Danh sách các HTTP Method đã nói ở trên
}
}
Có thể thấy là tất cả code C# được copy nguyên xi sang client, còn phần nằm trong {{}}
sẽ được thay thế bởi giá trị do người dùng cung cấp. Ví dụ: {{packageName}}
trở thành MaritimumClient
.
Tự viết Mustache template
Vì Mustache template là bản thiết kế mà OpenAPI Generator sử dụng nên ta có thể thay đổi đầu ra của nó bằng cách sửa template. Ta sẽ thử làm điều này với template của ApiException
. Đầu tiên ta cần tải template gốc từ đây. Sau đó ta thêm 1 property vào ApiException
.
{{>visibility}} class ApiException : Exception
{
/// <summary>
/// Property to demo Mustache template
/// </summary>
public string TestProperty { get; set; } = "Test property for Mustache template";
/// <summary>
/// Gets or sets the error code (HTTP status code)
/// </summary>
/// <value>The error code (HTTP status code).</value>
public int ErrorCode { get; set; }
//... nhiều code khác
}
Ở bước tiếp theo, ta lưu template này vào folder với tên gọi CustomTemplate
và sinh lại MaritimumClient.
openapi-generator-cli generate -i Maritimum/doc/openapi.yaml -g csharp-netcore -t <đường dẫn tới folder CustomTemplate> -o ../MaritimumClient --package-name MaritimumClient
Có thể thấy folder chứa template được truyền vào tham số t
. Lệnh trên cho kết quả như sau.
public class ApiException : Exception
{
/// <summary>
/// Property to demo Mustache template
/// </summary>
public string TestProperty { get; set; } = "Test property for Mustache template";
/// <summary>
/// Gets or sets the error code (HTTP status code)
/// </summary>
/// <value>The error code (HTTP status code).</value>
public int ErrorCode { get; set; }
//... nhiều code khác
}
Property với tên gọi TestProperty
đã được thêm vào ApiException
đúng như ta mong đợi. Chú ý là ta chỉ cần chỉnh sửa các template cần thiết chứ không phải sửa tất cả các template trong csharp-netcore. Với các template ta không thay đổi, OpenAPI Generator sẽ sử dụng bản gốc. Ta sẽ áp dụng điều này để khiến ApiException
của tất cả các client kế thừa một lớp exception chung.
Tự định nghĩa lớp ApiException
Ta sẽ định nghĩa một lớp exception trong một package riêng biệt và cho tất cả các client kế thừa exception đó. Lớp exception chung này rất giống với ApiException
do API Generator tạo ra, xin hãy tham khảo link này.
Có lẽ các bạn cũng để ý là lớp ApiException
ta tự định nghĩa không có property Headers
. Đó là do Headers
sử dụng Multimap
mà đây là một lớp được sinh tự động. Ta không muốn exception của ta phải phụ thuộc vào một client cụ thể nào, vì thế ta tạm lược bỏ property này. Trong một phần dưới, ta sẽ tìm hiểu cách bổ sung lại Headers
.
Khiến tất cả ApiException kế thừa một lớp exception chung
Bước đầu tiên ta cần làm là bổ sung package CommonClient
làm dependency của client. Ta cần phải sửa template netcore_project
. Các bạn có thể tham khảo bản sau thay đổi tại đây. Dưới đây là phần đáng chú ý.
<ItemGroup>
<ProjectReference Include="..\CommonClient\CommonClient.csproj" />
</ItemGroup>
Để ý là CommonClient
được bổ sung làm project reference. Điều này có nghĩa là folder CommonClient
cần được đặt trong cùng folder với client. Tất nhiên là trong thực tế chúng ta sẽ đẩy CommonClient
lên một package repository như Nuget. Lúc này, ta sẽ thay đổi code ở trên để bổ sung CommonClient
làm package reference.
<ItemGroup>
<PackageReference Include="XXX.CommonClient" Version="x.y.z" />
// other packages
</ItemGroup>
Sau đó, ta sửa template của ApiException
như dưới đây. Các điểm cần chú ý là như sau.
- Đặt
CommonClient.Exceptions.ApiException
làm lớp cha củaApiException
trong client.{{>visibility}} class ApiException : CommonClient.Exceptions.ApiException
- Xóa các property
ErrorCode
vàErrorContent
vì chúng đã được định nghĩa trong lớp cha. - Thay đổi cách các hàm khởi tạo gọi lớp cha. Như đã nói ở trên,
Headers
không tồn tại trong lớp cha. Vì thế hàm khởi tạo nhậnHeaders
sẽ có dạng như sau.public ApiException(int errorCode, string message, object errorContent = null, Multimap<string, string> headers = null) : base(errorCode, message, errorContent) { this.Headers = headers; }
Cuối cùng, chúng ta sinh lại MaritimumClient and PecuniaClient bằng các lệnh dưới.
openapi-generator-cli generate -i Maritimum/doc/openapi.yaml -g csharp-netcore -t CustomTemplate -o ../MaritimumClient --package-name MaritimumClient
openapi-generator-cli generate -i Pecunia/doc/openapi.yaml -g csharp-netcore -t CustomTemplate -o ../PecuniaClient --package-name PecuniaClient
Một exception chung cho tất cả các client
Giờ đây ta có thể đơn giản hóa đoạn code trong phần mở đầu.
try
{
var portTask = portApi.PortsShowAsync(portUuid);
var accountTask = accountApi.AccountsShowAsync(accountUuid);
await Task.WhenAll(portTask, accountTask);
}
catch (CommonClient.Exeptions.ApiException ex)
{
if (ex.ErrorCode == 404)
{
// Một vài xử lý
}
// Một vài xử lý khác
}
Cho dù ta có gọi bao nhiêu API đi chăng nữa, chỉ cần tất cả chúng đều dùng template ở trên thì ta có thể xử lý tất cả exception chỉ với 1 đoạn catch
. Hơn nữa, nếu ta cần xử lý exception từ một API nào đó một cách đặc biệt thì ta vẫn có thể viết code như dưới đây.
try
{
// code gọi nhiều APIs
}
catch (SpecialApi.Client.ApiException ex)
{
// Xử lý một cách đặc biệt
}
catch (CommonClient.Exeptions.ApiException ex)
{
// Xử lý thông thường
}
Một bài toán nâng cao
Dưới đây là một số điểm chưa tối ưu của giải pháp ở trên.
- Ta phải bỏ property
Headers
khỏiCommonClient.Exceptions.ApiException
. Nếu có property này thì ta sẽ biết được nhiều thông tin hơn về request đã gây ra lỗi. - Để tất cả exception của client kế thừa từ một lớp cha chưa chắc đã là giải pháp tốt. Có lẽ ta nên để các client sử dụng trực tiếp
CommonClient.Exceptions.ApiException
?
Tất cả các yêu cầu ở trên đều có thể được đáp ứng thông qua Mustache template. Tôi xin để vấn đề này làm bài tập về nhà cho người đọc, nếu gặp khó khăn các bạn có thể tham khảo các bước dưới đây.
- Tự định nghĩa lớp
Multimap
. Đây là bước đơn giản, các bạn có thể copy y nguyên lớp này vào projectCommonClient
, chỉ cần thay namespace. - Sửa các template để client sử dụng lớp
Multimap
ta vừa định nghĩa. Ta phải bổ sung reference tớiCommonClient.Multimap
vào các template sau:ApiException/ApiResponse/ClientUtils/RequestOptions/ApiClient/ClientUtils
.{{>partial_header}} using CommonClient; // các lệnh using khác
- Bổ sung property
Headers
vàoCommonClient.Extensions.ApiException
và đổi lớp cha thànhSystem.Exception
. - Sửa các template để client sử dụng trực tiếp
CommonClient.Extensions.ApiException
. Ta cần thêm reference tớiCommonClient.Extensions.ApiException
vào các template sau:XXXApi/ApiClient/Configuration
.{{>partial_header}} using CommonClient.Exceptions; // các lệnh using khác
- Copy tất cả các template ta vừa chỉnh sửa vào trong folder
CustomTemplate
. - Thiết lập OpenAP Generator để bỏ qua không tự sinh các file
Multimap.cs
vàApiException.cs
.- Tạo file với tên gọi
.openapi-generator-ignore
trong thư mục đầu ra của MaritimumClient. - Thêm 2 dòng sau vào
.openapi-generator-ignore
.src/MaritimumClient/Client/ApiException.cs src/MaritimumClient/Client/Multimap.cs
- Lặp lại 2 bước trên với PecuniaClient.
- Tạo file với tên gọi
- Sinh lại tất cả các client.
Kết thúc
Chỉ bằng việc chỉnh sửa vài file template đơn giản, chúng ta đã khiến tất cả các client dùng chung một lớp exception. Mặc dù bài hôm nay chỉ tập trung vào C# .NET Core, các kiến thức này có thể được áp dụng cho tất cả các ngôn ngữ khác mà OpenAPI Generator hỗ trợ. Bằng cách tận dụng Mustache template, ta có thể tùy biến mọi mặt của client mà OpenAPI Generator sinh ra.
One Thought on “Tùy biến OpenAPI Generator với Mustache template”