Entity Framework Coreで文字列カラムとEnumを双方向にマッピングする


この記事で使用しているコードは以下にあります。
https://github.com/noobow34/MappingStringToEnumInEntityFrameworkCore
何も考えずそのまま実行すれば動きます。
SQLiteを使用しているのでデータベースのファイルは存在しなくても勝手に作成されます。
実際に作成されたSQLiteのデータベースで結果を確認してみてください。

また、本記事ではC#と表記しますが、基本的にはVB.NETでも同様です。

やりたいこと

タイトルのとおりです。
想定としては、よくある"〇〇コード"のような決まった値しか取らないDB上の文字列カラム(charやvarchar)に対して
エンティティクラス側(C#側)でもEnumを使って取りうる値を限定してやるということです。

今回例として、USERテーブルのGENDERカラム(性別)は男性'M'、女性'F'のみが設定され得るとします。

C#のEnumは内部的にはintなので、Javaと違って値が文字列(string)となるEnumは言語の標準機能では使用することはできません。
また、エンティティクラス側でそのプロパティの型をEnumにする場合、文字列との変換をどのように行うのかを定義する必要があります。
以上より、今回の目的を達成するためには大きく2つのステップが必要です。

1.文字列のEnumを定義する
Enumのメンバが文字列(string)の値を持つ必要があります。
また、string⇔Enumの双方向の変換方法が必要になります。
Enum→string:データベースの文字列カラムに保存するので必要になる
string→Enum:データベースから取得してきた文字列をEnumとしてエンティティクラスのプロパティに格納するときに必要になる
2.文字列カラムをエンティティクラス側上でEnumで扱う(Enumにマッピングする)

1.文字列のEnumを定義する

C#の言語標準機能では文字列のEnumを定義することはできませんが、
アトリビュートと拡張メソッドを使って実現する方法があります。
例えば以下の記事が例です。
C# > enumに文字列を割り当てる。
ですが、自分でアトリビュートや拡張メソッドを定義しなくても、ライブラリがあります。
今回これを使います。nugetでプロジェクトに取り込んでください。
EnumStringValues

以下のようにStringValueアトリビュートでEnumに対応する文字列を定義します。

GenderEnum.cs
//~~~略 全体はGitHubからGenderEnum.csをみてください
public enum GenderEnum
{
    [StringValue("M")]
    Male,
    [StringValue("F")]
    Female
}
//~~~略

そうすると、Enumの.GetStringValue()で文字列が取得できるようになります。

GenderEnum.Male.GetStringValue(); //"M"が返ってくる
GenderEnum.Female.GetStringValue(); //"F"が返ってくる

逆に文字列(今回でいうと"F","M")をEnumにするには.ParseToEnum<T>()を使用します。

"M".ParseToEnum<GenderEnum>(); //GenderEnum.Maleが返ってくる
"F".ParseToEnum<GenderEnum>(); //GenderEnum.FeMaleが返ってくる
//
string gender = "M";
gender.ParseToEnum<GenderEnum>(); //当然、定数ではなくて変数でもOK

これで、string⇔Enumの双方向の変換が実現できました。

2.文字列カラムをエンティティクラス上でEnumで扱う(Enumにマッピングする)

まず、エンティティクラス上では当然ですが該当のプロパティの型をEnumにします。

User.cs
//~~~略 全体はGitHubからUser.csをみてください
[Column("GENDER")]
public GenderEnum? Gender { get; set; } //プロパティの型をEnumにしておく
//~~~略

では
・データベースから取得してきた値をどのようにEnumに変換してプロパティに格納するのか
・Enumからどのように文字列に変換してデータベースに登録するのか
をどうやって定義するのでしょうか?
値の変換(Value Conversion) を使用します。
DbContextのOnModelCreatingで以下のように定義します。

TestDbContext.cs
//~~~略 全体はGitHubからTestDbContext.csをみてください
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
    //Conversionを使用してEnumとの対応関係を定義
    modelBuilder.Entity<User>() //Userエンティティの
       .Property(u => u.Gender) //Genderプロパティに
       .HasConversion //値の変換を設定
       //EnumをGetStringValueしたものがDBに登録される
       (g => g.GetStringValue()
       //DBから取得した値をParseToEnumしたものがEnumとしてプロパティに格納される
       , g => ((string)g).ParseToEnum<GenderEnum>());
}
//~~~略

これで、めでたくEnumのプロパティを使用してデータベースと双方向でやり取り(登録、取得)ができます。

実行してみる

Program.cs
//~~~略 全体はGitHubからProgram.csをみてください
//登録
using (var context = new TestDbContext(options))
{
    context.Users.Add(new User { Name = "Bob", Gender = GenderEnum.Male }); //Maleは'M'として登録される
    context.Users.Add(new User { Name = "Elizabeth", Gender = GenderEnum.Female }); //Femaleは'F'として登録される
    context.SaveChanges();
}

//取得
using (var context = new TestDbContext(options))
{
    foreach (var user in context.Users)
    {
        Console.WriteLine($"Id:{user.Id}");
        Console.WriteLine($"Name:{user.Name}");
        Console.WriteLine($"Gender(Enum):{user.Gender}");
        Console.WriteLine($"Gender(String):{user.Gender.GetStringValue()}");
        Console.WriteLine("---------------------------");
    }
}

実行結果
Enumとstringの変換が正しく行われているのがわかります。

Id:1
Name:Bob
Gender(Enum):Male
Gender(String):M
---------------------------
Id:2
Name:Elizabeth
Gender(Enum):Female
Gender(String):F
---------------------------

データベース上では'M','F'で登録されているのがわかります。