ASP.Net+Swagger+TypeScriptでEnumの表示名を自動生成する
Tags: aspdotnet, csharp, typscript, swagger
Published: 2024-01-25 08:53:11

---
title: ASP.Net+Swagger+TypeScriptでEnumの表示名を自動生成する
published: true
description: 
tags: aspdotnet,csharp,typscript,swagger
# cover_image: https://direct_url_to_image.jpg
# Use a ratio of 100:42 for best results.
# published_at: 2024-01-25 08:14 +0000
---

ASP.Netは[Swaggerをサポートしており](https://learn.microsoft.com/ja-jp/aspnet/core/tutorials/getting-started-with-swashbuckle?view=aspnetcore-7.0&viewFallbackFrom=aspnetcore-8.0&tabs=visual-studio)、Controllerを書くだけでopenapi schemaが自動生成されて便利です。

そして自動生成されたopenapi schemaから、openapi-generatorなどのツールを利用して、TypeScriptのコードを自動生成すれば、Single Page Applicationが作りやすくなります。

## レスポンスのPayloadにEnum型があった場合

しかし、レスポンスのPayloadにEnum型のプロパティがあった場合、あまり良いコードが得られません。

例えば、こんなC#のenumがあったとして、
```cs
public enum WeatherType
{
    Sunny = 0,
    Cloudy = 1,
    Rainy = 2,
}
```
普通にopenapi-generatorで自動生成すると、以下のTypeScriptのコードが得られます。
```ts
export const WeatherType = {
    NUMBER_0: 0,
    NUMBER_1: 1,
    NUMBER_2: 2
} as const;
export type WeatherType = typeof WeatherType[keyof typeof WeatherType];
```
EnumのメンバーがNUMBER_{数字} になってわかりにくいです。

## x-enum-varnames
メンバーのキー名を変更するには、openapi schemaにx-enum-varnamesという名前で配列を入れてやればよいです。
https://github.com/OpenAPITools/openapi-generator/issues/893#issuecomment-416617460

ASP.NetのSwaggerジェネレータ (今回はSwashbuckle)の場合、SchemaFilterという機能があり、これを使えば、出力されるopenapi schemaをいじることができます。

こんな感じのSchemaFilterを用意して
```cs
public class EnumSchemaFilter : ISchemaFilter
{
    public void Apply(OpenApiSchema model, SchemaFilterContext context)
    {
        if (context.Type.IsEnum)
        {
            var varNamesArray = new OpenApiArray();
            foreach (var memberVal in Enum.GetValues(context.Type))
            {
                var memberName = Enum.GetName(context.Type, memberVal);
                varNamesArray.Add(new OpenApiString(memberName));
            }

            model.Extensions.Add("x-enum-varnames", varNamesArray);
        }
    }
}
```

AddSwaggerGenで指定してやります
```cs
builder.Services.AddSwaggerGen(options =>
{
    options.SchemaFilter<EnumSchemaFilter>();
});
```

すると、openapi schemaにはx-enum-varnamesにメンバー名が入るようになります。
```json
"WeatherType": {
  "enum": [
    0,
    1,
    2
  ],
  "type": "integer",
  "format": "int32",
  "x-enum-varnames": [
    "Sunny",
    "Cloudy",
    "Rainy"
  ]
}
```

これを使ってTypeScriptを生成すると、キーが正しくメンバー名になります。
```ts
export const WeatherType = {
    Sunny: 0,
    Cloudy: 1,
    Rainy: 2
} as const;
export type WeatherType = typeof WeatherType[keyof typeof WeatherType];
```

## 表示名を変えたい
x-enum-varnamesを使えば、TypeScriptのオブジェクトのキーとして、Enumのメンバー名を使用できましたが、
実際にUIに値を表示したい時は、もっと違う文字列を表示したいかもしれません。

例えばメンバー名を `Sunny=` とした場合、`=` はTypeScriptのオブジェクトのキーとして使えないのでエラーになります。

また、各国の言語に翻訳した名前を表示したいかもしれません。

## EnumのAttributeを表示する

そこで、EnumにAttributeをつけて、それを表示するようにしてみます。
今回は `System.ComponentModel.DataAnnotations.DisplayAttribute` を使用します。

```cs
public enum WeatherType
{
    [Display(Name = "晴れ")]
    Sunny = 0,

    [Display(Name = "曇り")]
    Cloudy = 1,
    
    [Display(Name = "雨")]
    Rainy = 2,
}
```

openapi schemaのテキトーなキーに、この表示名をつけれればいいので、
EnumSchmeFilterを以下のように修正し、 `x-enum-displays` というスキーマを追加してみます。
```diff
public class EnumSchemaFilter : ISchemaFilter
{
    public void Apply(OpenApiSchema model, SchemaFilterContext context)
    {
        if (context.Type.IsEnum)
        {
            var varNamesArray = new OpenApiArray();
+           var displayArray = new OpenApiArray();
            foreach (var memberVal in Enum.GetValues(context.Type))
            {
                var memberName = Enum.GetName(context.Type, memberVal);
                varNamesArray.Add(new OpenApiString(memberName));
+               var obj = new OpenApiObject();
+               var display = (DisplayAttribute?)memberVal.GetType().GetMember(memberVal.ToString() ?? "").FirstOrDefault()?.GetCustomAttributes(typeof(DisplayAttribute), true).FirstOrDefault();
+               obj["value"] = new OpenApiInteger(Convert.ToInt32(memberVal));
+               obj["display"] = new OpenApiString(display?.Name);
+               displayArray.Add(obj);
            }

            model.Extensions.Add("x-enum-varnames", varNamesArray);
+           model.Extensions.Add("x-enum-displays", displayArray);
        }
    }
}
```

これで生成されたスキーマは以下のようになります。

```json
"WeatherType": {
  "enum": [
    0,
    1,
    2
  ],
  "type": "integer",
  "format": "int32",
  "x-enum-varnames": [
    "Sunny",
    "Cloudy",
    "Rainy"
  ],
  "x-enum-displays": [
    {
      "value": 0,
      "display": "晴れ"
    },
    {
      "value": 1,
      "display": "曇り"
    },
    {
      "value": 2,
      "display": "雨"
    }
  ]
}
```

あとはこれを使って、TypeScriptコードを生成してやればよいのですが、標準では今回作った `x-enum-displays` を読めないので、コード生成テンプレートを修正してやります。

テンプレートはここにあるので落としてきます
https://github.com/OpenAPITools/openapi-generator/tree/master/modules/openapi-generator/src/main/resources/typescript-fetch

次に、テンプレートの中にある `modelEnumInterfaces.mustache` に以下を追加します。

```
/*
customized
*/
{{^stringEnums}}
{{#vendorExtensions}}
export function Get{{classname}}DisplayName(v: {{classname}}) {
    switch(v) {
{{#x-enum-displays}}
        case {{value}}:
            return "{{display}}";
{{/x-enum-displays}}
        default:
            throw new Error(`value ${v} is not supported`);
    }
}
{{/vendorExtensions}}
{{/stringEnums}}
```

あとはopenapi generatorで生成する時に、 `-t` オプションで、先ほど修正したテンプレートのディレクトリを指定します。

テンプレートのカスタマイズは[このドキュメント](https://openapi-generator.tech/docs/templating/)を参考にしてください。

これで、生成されたTypeScriptには、enumから表示名を取得する関数が追加されます。
```ts
export const WeatherType = {
    Sunny: 0,
    Cloudy: 1,
    Rainy: 2
} as const;
export type WeatherType = typeof WeatherType[keyof typeof WeatherType];

/*
customized
*/
export function GetWeatherTypeDisplayName(v: WeatherType) {
    switch(v) {
        case 0:
            return "晴れ";
        case 1:
            return "曇り";
        case 2:
            return "雨";
        default:
            throw new Error(`value ${v} is not supported`);
    }
}
```

こんな感じで表示名を取得できます。
```ts
// 晴れ が返される
GetWeatherTypeDisplayName(WeatherType.Sunny)
```