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
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’sApiException
.{{>visibility}} class ApiException : CommonClient.Exceptions.ApiException
- Removed
ErrorCode
andErrorContent
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 receivesHeaders
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
fromCommonClient.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 intoCommonClient
and change the namespace. - Let the clients use our own
Multimap
. We need to modify the template ofApiException/ApiResponse/ClientUtils/RequestOptions/ApiClient/ClientUtils
to referenceCommonClient.Multimap
.{{>partial_header}} using CommonClient; // other using statements
- Add the
Headers
property toCommonClient.Extensions.ApiException
and change the base class toSystem.Exception
. - Let the clients use
CommonClient.Extensions.ApiException
directly. We do this by modifying the template ofXXXApi/ApiClient/Configuration
to referenceCommonClient.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
andApiException.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.
- Create a file called
- 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.