Note: see the link below for the English version of this article.

https://duongnt.com/openapi-generator-mustache-customize

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 MaritimumPecunia. 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
    }
}

đâ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ủa ApiException trong client.
    {{>visibility}} class ApiException : CommonClient.Exceptions.ApiException
    
  • Xóa các property ErrorCodeErrorContent 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ận Headers 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ỏi CommonClient.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 project CommonClient, 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ới CommonClient.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ào CommonClient.Extensions.ApiException và đổi lớp cha thành System.Exception.
  • Sửa các template để client sử dụng trực tiếp CommonClient.Extensions.ApiException. Ta cần thêm reference tới CommonClient.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.csApiException.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.
  • 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.

A software developer from Vietnam and is currently living in Japan.

One Thought on “Tùy biến OpenAPI Generator với Mustache template”

Leave a Reply