Note: phiên bản Tiếng Việt của bài này ở link dưới.

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

openapi-generator-mustache-customize

The clients generated by OpenAPI Generator can work out of the box, but there might be cases where the default code is not optimized. Today, we’ll add our own customization to the output of OpenAPI Generator using Mustache template. This article assumes some familiarity with how OpenAPI Generator works and with the sample code in my previous article, I suggest everyone check it out first.

Again, you can download all the sample code from the link below.

https://github.com/duongntbk/OpenAPIGeneratorDemo

An actual problem in production

This is the simplified version of a problem I had in one of my projects. In my previous article, we generated clients for the Maritimum and Pecunia API. The generated clients can handle all HTTP errors and wrap them in an exception type called ApiException. However, each client has their own version of ApiException. Maritimum has this version under the MaritimumClient.Client namespace; and Pecunia has this version under the PecuniaClient.Client namespace.

If we look at the source code, we can see that they are exactly the same, except for the namespace. But as far as the framework is concerned, these are two completely different classes. This is a problem when we want to use both clients at the same time. If we want to handle exceptions for both APIs, we have to write something like this.

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)
    {
        // Do something
    }
    // Do something else
}
catch (PecuniaClient.Client.ApiException ex)
{
    if (ex.ErrorCode == 404)
    {
        // Do something
    }
    // Do something else
}

We have to repeat all those catch blocks every time we want to handle API exceptions. And if we have multiple APIs, things can get ugly quickly. It would be nice if all clients throw the same exception.

Mustache template in OpenAPI Generator

Introducing Mustache template

Mustache template can be considered as the blueprint that OpenAPI Generator uses to generate all clients code. There are template files for all classes of all languages that OpenAPI Generator supports. You can find the full list here. Because we want to customize our C# .Net Core client, we will focus on the templates in csharp-netcore folder.

As an example, let’s look at the HttpMethod template.

{{>partial_header}}

namespace {{packageName}}.Client
{
    /// <summary>
    /// Http methods supported by swagger
    /// </summary>
    public enum HttpMethod
    {
        // A list of HTTP method
    }
}

And this is the generated code in 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
    {
        // The same list of HTTP Method
    }
}

As we can see, all the C# code is copied verbatim to the generated class, while the part inside {{}} are replaced with user provided values. For example, {{packageName}} becomes MaritimumClient.

Create our own Mustache template

Since Mustache template is the blueprint for OpenAPI Generator, we can change the way it generates the clients by customizing the templates. Let’s try this with the template of ApiException. First, we need to download the default template from here. Then we will add a property to 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; }

    //... more code
}

After that, we save this custom template into a folder called CustomTemplate and re-generate MaritimumClient by running the following command.

openapi-generator-cli generate -i Maritimum/doc/openapi.yaml -g csharp-netcore -t <path to CustomTemplate folder> -o ../MaritimumClient --package-name MaritimumClient

As we can see, the custom template folder is passed to the t argument. This command will generate the following code.

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; }

    //... more code
}

The TestProperty property is included in ApiException as we expected. Note that we only have to overwrite the templates we need, not all templates. For templates we don’t provide, OpenAPI Generator will use the default version. Aim with this knowledge, we will try to wrap ApiException of all clients into a common exception class.

Define a common ApiException class

We will define a common exception class in a separate package and let all clients inherit it. That common exception looks very similar to a generated ApiException. You can find it in the following link.

Perhaps you have noticed that our ApiException lacks the Headers property. This is because Headers uses a generated type called Multimap. We don’t want the common exception to depend on any client, that’s why we omit it for now. In a later section, we will find out how to add Headers back to our common exception.

Let ApiException inherit our common exception

The first step is adding the CommonClient package as a dependency of our clients. We do that by modifying the netcore_project template. You can find the customized template here. This is the interesting part.

<ItemGroup>
  <ProjectReference Include="..\CommonClient\CommonClient.csproj" />
</ItemGroup>

Notice that CommonClient is added as a project reference. This means the CommonClient folder must be in the same folder with our client projects. Obviously, in a real-life project, CommonClient will be published to a package repository. In that case, we need to modify the code above to use package reference.

<ItemGroup>
  <PackageReference Include="XXX.CommonClient" Version="x.y.z" />
  // other packages
</ItemGroup>

Then we can modify the template of ApiException, this is the result. Below are the changes we made.

  • Added CommonClient.Exceptions.ApiException as the base class of client’s ApiException.
    {{>visibility}} class ApiException : CommonClient.Exceptions.ApiException
    
  • Removed ErrorCode and ErrorContent because those properties are already defined inside the base class.
  • Modified the constructors to call the constructors in base class. As mentioned earlier, Headers does not exist in the base class. Because of this, the constructor which receives Headers looks like this.
    public ApiException(int errorCode, string message, object errorContent = null, Multimap<string, string> headers = null)
        : base(errorCode, message, errorContent)
    {
        this.Headers = headers;
    }
    

Again, we re-generate MaritimumClient and PecuniaClient by running the commands below.

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

One exception to rule them all

Now, we can simplify the code in the opening section to this.

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)
    {
        // Do something
    }
    // Do something else
}

No matter how many APIs we call, as long as they all use the templates above, we can use one catch block to handle all exceptions. Moreover, if we ever need to handle exceptions from one API in a special way, we can always catch just that exception.

try
{
    // code to call multiple APIs
}
catch (SpecialApi.Client.ApiException ex)
{
    // Do special things
}
catch (CommonClient.Exeptions.ApiException ex)
{
    // Do some normal things
}

Where to go from here?

The solution above works well enough, but there are still things we can improve.

  • We need to omit Headers from CommonClient.Exceptions.ApiException, it would be nice if we can include that property.
  • It’s debatable if wrapping all client exceptions in a common exception class is a clean solution. Perhaps client code should use CommonClient.Exceptions.ApiException directly?

We can archive all the goals above with customized templates. The actual solution is left as an exercise for the reader, but below are some hints.

  • Define our own Multimap class. This is an easy step, you can copy a generated class into CommonClient and change the namespace.
  • Let the clients use our own Multimap. We need to modify the template of ApiException/ApiResponse/ClientUtils/RequestOptions/ApiClient/ClientUtils to reference CommonClient.Multimap.
    {{>partial_header}}
    
    using CommonClient;
    // other using statements
    
  • Add the Headers property to CommonClient.Extensions.ApiException and change the base class to System.Exception.
  • Let the clients use CommonClient.Extensions.ApiException directly. We do this by modifying the template of XXXApi/ApiClient/Configuration to reference CommonClient.Extensions.ApiException.
    {{>partial_header}}
    
    using CommonClient.Exceptions;
    // other using statements
    
  • Put all customized templates into the CustomTemplate folder.
  • Tell OpenAPI Generator to skip generating Multimap.cs and ApiException.cs.
    • Create a file called .openapi-generator-ignore in the root of MaritimumClient output folder.
    • Add these lines into .openapi-generator-ignore.
      src/MaritimumClient/Client/ApiException.cs
      src/MaritimumClient/Client/Multimap.cs
      
    • Repeat the two steps above with PecuniaClient.
  • Re-generate all clients.

Conclusion

By using just a few simple template files, we have united all clients under one common exception. And although this article only focuses on C# .NET Core, the general principles also apply to all languages that OpenAPI Generator supports. With clever uses of Mustache template, we can customize every aspect of generated clients.

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

Leave a Reply