EFCoreのスキャフォールディングをカスタマイズする方法

# 概要
EFCoreでOracleDBをスキャフォールディングすると、なんか思っていたのと違う型になってクラスが生成されることがあります。例えばDB側で`NUMBER(1, 0)`みたいな感じで1桁の数字型を定義すると、なぜかC#ではboolに変換されます。しかもたちが悪いことに、これをそのまま使おうとするとboolをnumberに変換できないというエラーが出てしまい、SQLも実行できません（バージョンが古いDBだとダメっぽい？）。
これについて、Oracleの公式からはどの型がどの型に変換されるのかの一覧が出ています。  
https://docs.oracle.com/cd/F82042_01/odpnt/EFCoreREDataTypeMapping.html

boolに変換される他、byteやshortもあまり扱いたくないというような要件もあるかと思います。こういった場合、生成されたクラスを手で頑張ってなおしてもいいですが、できればスキャフォールディングされたものをそのまま使えるようにしておきたいです。今回はその方法を説明します。  
※今回はOracleDBについて扱いますが、他のDBでも同じ方法でカスタマイズ可能です。

# 選択肢
まず、スキャフォールディングのカスタマイズ方法としてT4を使った方法とEntityFrameworkCore.Designを使った方法があります。T4は生成されるクラスのテキストテンプレートそのものを編集できるので、実質なんでもできる感じではありますが、EFCoreのアップデートのたびにT4テンプレートが変わっていないかを確認し、変わっていたらその変更をマージするみたいなのはかなりつらいです。  
今回は現実的に保守がしやすい、EntityFrameworkCore.Designを使った方法を紹介します。

# やってみる
### 環境
* Visual Studio Professional 2022(64 bit) Version 17.13.2
* .NET 8
* EntityFrameworkCore 9

### 各パッケージのバージョン
プロジェクトには以下のパッケージを入れています。
* EFCore.NamingConventions - 9.0.0
* Microsoft.EntityFrameworkCore - 9.0.4
* Microsoft.EntityFrameworkCore.Design - 9.0.4
* Microsoft.EntityFrameworkCore.Relational - 9.0.4
* Microsoft.EntityFrameworkCore.Tools - 9.0.4
* Oracle.EntityFrameworkCore - 9.23.80

NamingConventionsはDBのテーブル名やカラム名をC#の命名規則っぽくしてくれるやつなので、入れなくても大丈夫ですが、今回の記事ではカラム名を元にいろいろやっている部分があるので、一応明記しています。

## Microsoft.EntityFrameworkCore.Designの参照を変える
`Microsoft.EntityFrameworkCore.Design`の参照方法を若干変えないと、この後実装するインターフェースなどにアクセスができないみたいなので、プロジェクトのcsprojファイル内の`Microsoft.EntityFrameworkCore.Design`の参照部分を以下のように変更します。
```xml
<PackageReference Include="Microsoft.EntityFrameworkCore.Design" Version="9.0.4">
  <PrivateAssets>all</PrivateAssets>
	<!-- Remove IncludeAssets to allow compiling against the assembly -->
	<!--<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>-->
</PackageReference>
```

## 型の変換を行う
今回、`NUMBER`がboolやbyte、shortなどに変換されてしまうのを阻止して、全てint型に変換されるようにしていきます。この時、生成されたコードに`[Column("カラム名",TypeName="NUMBER(1)")]`みたいな感じで`TypeName`があると、SQLに変換するときに勝手にbool型にされてしまいエラーになります。なので、`TypeName`を消しつつC#の型も変えるということをやっていきます。

### 型変換メソッド実装
まずは型変換を行うためのクラス、メソッドを実装していきましょう。Scaffolding時の型の変換に介入したい場合は`IRelationalTypeMappingSourcePlugin`というインターフェースを実装したクラスを作ります。

```csharp: CustomOracleTypeMappingSourcePlugin.cs
    public sealed class CustomOracleTypeMappingSourcePlugin : IRelationalTypeMappingSourcePlugin
    {
        public RelationalTypeMapping? FindMapping(in RelationalTypeMappingInfo mappingInfo)
        {
            var storeTypeName = mappingInfo.StoreTypeName?.ToUpperInvariant();
            var precision = mappingInfo.Precision;
            var scale = mappingInfo.Scale;
            var clrType = mappingInfo.ClrType;

            // NUMBER(0～5, 0)が勝手にbool, byte, shortに変換されるのを防ぐ
            if (mappingInfo.StoreTypeNameBase != null
                && mappingInfo.StoreTypeNameBase.Equals("NUMBER", StringComparison.OrdinalIgnoreCase)
                && precision < 5
                && (scale == 0 || scale == null)) // スケールが0または未指定 (整数)
            {
                return new OracleInt32TypeMapping("NUMBER").WithTypeMappingInfo(
                    new RelationalTypeMappingInfo(
                        "NUMBER",
                        mappingInfo.StoreTypeNameBase ?? "NUMBER",
                        mappingInfo.IsUnicode,
                        mappingInfo.Size,
                        mappingInfo.Precision,
                        mappingInfo.Scale));
            }
            
            return null;
        }
    }
```

`OracleInt32TypeMapping`だけでは`TypeName`を消すことができなかったので、`WithTypeMappingInfo`を使って上書きしています。
クラスはできましたが、これを書いただけでゃもちろん動かないので、スキャフォールディング時にこのクラスが使われるように設定をしていきます。

### IDesignTimeServicesを実装する
EFCoreのスキャフォールディングコマンド実行時に、同じプロジェクト内に`IDesignTimeServices`を実装したクラスがある場合、そのクラスの`ConfigureDesignTimeServices`というメソッドを呼び出してくれて、DIコンテナへのアクセスができるようになります。
先ほど作ったクラスを以下のようにDIコンテナに入れてあげてください。

```csharp: CustomOracleDesignTimeServices.cs
public sealed class CustomOracleDesignTimeServices : IDesignTimeServices
{
    public void ConfigureDesignTimeServices(IServiceCollection serviceCollection)
    {
        serviceCollection.AddSingleton<IRelationalTypeMappingSourcePlugin, CustomOracleTypeMappingSourcePlugin>();
    }
}
```

この状態でスキャフォールディングコマンドを実行すると、桁数の少ないNUMBERの型もC#では全てintに変換されるようになりました。これでSQLを投げても型の変換に関するエラーが出なくなったかと思います。
やりたいことはだいたいできるようになったかと思いますが、「マイグレーションの時はenumもうまく変換できて便利なのになぁ」というそこのあなたのために、スキャフォールディング処理に割り込んで型をenumにする方法も説明します。

## NUMBERのカラムを任意のenumに変換する
### enumを定義する
まず、何はともあれenumを定義します。
```csharp:ExampleFlgEnum.cs
public enum ExampleFlgEnum
{
    Enabled = 0,
    Disabled = 1,
    Deleted = 2,
}
```
これをDBのNUMBER型のカラムと紐づけていきます。

### RelationalScaffoldingModelFactoryを継承する
`RelationalScaffoldingModelFactory`というクラスを継承して処理を上書きすることで、モデルの型を好きにいじることができます。先ほどの`IRelationalTypeMappingSourcePlugin`でもできるとは思いますが、`RelationalTypeMapping`を返す必要があり、各enumに対応した`RelationalTypeMapping`を実装するのは大変そうな気がするので、こちらの方法を取ります。なお、`RelationalScaffoldingModelFactory`が見つからない場合、`Microsoft.EntityFrameworkCore.Design`の参照方法が変わっていない可能性が高いので、この記事の最初のほうの手順を飛ばしていないか確認してください。

継承して以下のようなクラスを実装しましょう。
```csharp: CustomScaffoldingModelFactory.cs
public sealed class CustomScaffoldingModelFactory : RelationalScaffoldingModelFactory
{
    public CustomScaffoldingModelFactory(
        IOperationReporter reporter,
        ICandidateNamingService candidateNamingService,
        IPluralizer pluralizer,
        ICSharpUtilities csharpUtilities,
        IScaffoldingTypeMapper scaffoldingTypeMapper,
        IModelRuntimeInitializer modelRuntimeInitializer)
        : base(
        reporter,
        candidateNamingService,
        pluralizer,
        csharpUtilities,
        scaffoldingTypeMapper,
        modelRuntimeInitializer)
    {
    }

    protected override TypeScaffoldingInfo GetTypeScaffoldingInfo(DatabaseColumn column)
    {
        // ExampleFlgの型をExampleFlgEnumに変換
        if ((column.Name.Equals("ExampleFlg", StringComparison.OrdinalIgnoreCase)
                || column.Name.Equals("EXAMPLE_FLG"))
            && (column.StoreType?.Equals("NUMBER(1)", StringComparison.OrdinalIgnoreCase) ?? false))
        {
            Type clrType = column.IsNullable ? typeof(ExampleFlgEnum?) : typeof(ExampleFlgEnum);
            return new TypeScaffoldingInfo(clrType, column.IsNullable, null, null, null, 1, 0);
        }

        return base.GetTypeScaffoldingInfo(column);
    }
}
```

今回の例ではカラム名を元に変換していますが、取り方はいろいろあると思うので、ユースケースに合った書き方をしてください。

### DIコンテナに登録する
もちろんこちらもDIコンテナに入れないと実行されませんので、以下のようにDIコンテナに入れてください。
```csharp: CustomOracleDesignTimeServices.cs
public sealed class CustomOracleDesignTimeServices : IDesignTimeServices
{
    public void ConfigureDesignTimeServices(IServiceCollection serviceCollection)
    {
        serviceCollection.AddSingleton<IRelationalTypeMappingSourcePlugin, CustomOracleTypeMappingSourcePlugin>();
        serviceCollection.AddSingleton<IScaffoldingModelFactory, CustomScaffoldingModelFactory>();
    }
}
```

これでスキャフォールディングを行うと、対象のカラムがenumの型になってクラスに変換されるようになります。

# まとめ
これらの方法を使うことである程度なんでもできるようになるかと思うので、Oracleのスキャフォールディングで困っている方はぜひ参考にしてみてください。このスキャフォールディングで生成されるクラス自体はEFCoreでしか使えないような形にはなっていないので、属性などをうまくつけてDapperなどで使うクラスとして生成することも可能かと思います。
よきC#ライフを...！