【Entity Framework Core 2.1～ 】エンティティクラスのプロパティは読み取り専用にしよう

## はじめに

この記事のエンティティクラスとは、`Entity Framework Core`において**データベースにマッピングされるクラス**のことを指します。

`Entity Framework Core 2.1`からエンティティクラスのプロパティを読み取り専用にできるようになりました。
これまで、`Entity Framework Core`でエンティティクラスを定義する場合、以下のように書き込みも`public`なプロパティが必要でした。

```csharp:書籍エンティティ
class Book {
  public int Id { get; set; }
  public string Title { get; set; }
  public string Author { get; set; }
}
```

`Entity Framework Core 2.1`からは以下のようにセッターを`private`にできるようになりました。

```csharp
class Book {
  public int Id { get; private set; }
  public string Title { get; private set; }
  public string Author { get; private set; }

  public Book(int id, string title, string author) {
    Id = id;
    Title = title;
    Author = author;
  }
}
```

インスタンスを外部から書き換えられないので、初期化はプロパティ初期化子[^1]でなくコンストラクターで明示的に行います。
プロパティが書き込み専用ならインスタンスの状態が不変になり、**内部のデータを外部からの意図せぬ変更から守る**ことができます。

※ C# 6 から、コンストラクターでのみ値を代入可能な`get`アクセサーだけのプロパティを定義できましたが、`Entity Framework Core`では`private set`がないとマッピングが行われません。
※ ただし、`OnModelCreating`のフックで明示的にマッピングすると`private set`も不要になります。後半に書きます。

## プロジェクトの作成

サンプルプロジェクトを作成しプロパティが読み取り専用なエンティティクラスを使用した CRUD 処理の説明したいと思います。

最終的なソースは以下のリポジトリにおきました。
**https://github.com/sano-suguru/readonly-model-props**


```sh
$ dotnet --version
2.1.403
# テンプレートは最低限のコンソールアプリを使用します。
$ dotnet new console -o "readonly-model-props"
$ cd ./readonly-model-props
# DBマイグレーションを省略するためインメモリ DB を使用します。
$ dotnet add package "Microsoft.EntityFrameworkCore.InMemory"
```

### エンティティクラス

エンティティクラスは 「はじめに」 と同じ書籍エンティティを使用します。
クラス外部からはプロパティが読み取り専用になっています。

```csharp:Book.cs
class Book {
  public int Id { get; private set; }
  public string Title { get; private set; }
  public string Author { get; private set; }

  public Book(int id, string title, string author) {
    Id = id;
    Title = title;
    Author = author;
  }
}
```

### データベースコンテキスト

データベースはマイグレーションを省略するためインメモリデータベースを使います。
今回は最初からシードデータを持たせておきます。

```csharp
using Microsoft.EntityFrameworkCore;

class InMemoryDbContext : DbContext {
  public DbSet<Book> Books { get; set; }

  protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) =>
    optionsBuilder.UseInMemoryDatabase(databaseName: "InMemoryDatabase");

  protected override void OnModelCreating(ModelBuilder modelBuilder) =>
    modelBuilder.Entity<Book>().HasData(
      new Book(id: 1, title: "Flutter入門", author: "掌田 津耶乃"),
      new Book(id: 2, title: "Nuxt.jsビギナーズガイド", author: "花谷 拓磨"),
      new Book(id: 3, title: "IntelliJ IDEAハンズオン", author: "山本 裕介")
    );
}
```

## プロパティが読み取り専用の CRUD 処理

プロパティが読み取り専用の場合、インスタンスに対し`book.Title = "FooBar"`のように書き換えることができません。
そのため、通常の`Entity Framework Core`の CRUD 処理と勝手が変わってくる部分があります。

### 抽出クエリ（`READ`）

プロパティが読み取り専用でも特別な処理はありません。


```csharp
using (var context = new InMemoryDbContext()) {
  var books = context.Books.AsNoTracking().AsEnumerable();
  foreach (var book in books) {
    Console.WriteLine($"{book.Id} {book.Title} {book.Author}");
  }
}
```

データベースのデータがすべて抽出されました。

```sh:結果
1 Flutter入門 掌田 津耶乃
2 Nuxt.jsビギナーズガイド 花谷 拓磨
3 IntelliJ IDEAハンズオン 山本 裕介
```

#### ※ `AsNoTracking`について
上記では`IQueryable<Book>`に対し`AsNoTracking`付いています。
通常、インスタンスは `Entity Framework Core`によって追跡され、エンティティ内で検出された変更はすべて`SaveChanges`メソッドを呼んだときデータベースに保存されます。
`AsNoTracking`によって追跡されない場合、変更追跡の情報を設定する必要がないのでクエリが高速化されます。

### 新規作成クエリ（`CREATE`）

プロパティ初期化子が使えないので、`Book`インスタンスの初期値はコンストラクタに渡されます。

```csharp
using (var context = new InMemoryDbContext()) {
  var newbook = new Book(
    id: 4,
    title: "C#プログラミングのイディオム/定石&パターン",
    author: "出井 秀行"
  );
  context.Books.Add(newbook);

  // 結果の確認
  foreach (var book in context.Books.AsNoTracking()) {
    Console.WriteLine($"{book.Id} {book.Title} {book.Author}");
  }
}
```

データベースに書籍が追加されているのが確認できます。

```sh:結果
1 Flutter入門 掌田 津耶乃
2 Nuxt.jsビギナーズガイド 花谷 拓磨
3 IntelliJ IDEAハンズオン 山本 裕介
4 C#プログラミングのイディオム/定石&パターン 出井 秀行
```

### 更新クエリ（`UPDATE`）

データベースから抽出した更新対象のインスタンスを直接書き換えることができないため、少し特殊です。
新規に`Book`のインスタンスを作成し、その際に更新するプロパティ以外を更新対象のプロパティで初期化します。
作成したインスタンスで更新をかけますが、`Entity Framework Core`では`Id`は自動で主キーになっているため、一致する`Id`のデータを更新されます。

```csharp
using (var context = new InMemoryDbContext()) {
  var target = context.Books
    .AsNoTracking()
    .Single(b => b.Id == 3);
  context.Books.Update(
    new Book(
      id: target.Id,
      title: target.Title,
      author: "今井 勝信"
    )
  );
  context.SaveChanges();

  // 更新の確認
  var updated = context.Books.AsNoTracking().Single(b => b.Id == 3);
  Console.WriteLine($"{updated.Id} {updated.Title} {updated.Author}");
}
```

対象（`Id == 3`が）のデータが更新されていることが確認できます。

```sh:結果
3 IntelliJ IDEAハンズオン 今井 勝信  # 元は 山本 裕介
```

### 削除クエリ（`DELETE`）

抽出クエリと同様、プロパティが読み取り専用でも特別な処理はありません。

```csharp
using (var context = new InMemoryDbContext()) {
  var target = context.Books.Single(b => b.Id == 1);
  context.Books.Remove(target);
  context.SaveChanges();

  foreach (var book in context.Books.AsNoTracking()) {
    Console.WriteLine($"{book.Id} {book.Title} {book.Author}");
  }
}
```

`ID == 1`のデータがデータベースから削除されているのが確認できます。

```sh:結果
2 Nuxt.jsビギナーズガイド 花谷 拓磨
3 IntelliJ IDEAハンズオン 今井 勝信
4 C#プログラミングのイディオム/定石&パターン 出井 秀行
```

## プロパティを内部からも読み取り専用にする

`public ~ { get; private set }`の場合、クラス内部からはデータを書き換えることが可能でしたが、`get`のみの場合はコンストラクタでの初期化のみ可能です。
「はじめに」で触れた`private set`もなく`get`アクセサのみプロパティを持つエンティティクラスを定義したいと思います。

まず、エンティティクラスのプロパティから`private set`を除きます。

```diff
   class Book {
-    public int Id { get; private set; }
-    public string Title { get; private set; }
-    public string Author { get; private set; }
+    public int Id { get; }
+    public string Title { get; }
+    public string Author { get; }

     public Book(int id, string title, string author) {
       Id = id;
       Title = title;
       Author = author;
     }
   }
```

このままでは`Entity Framework Core`でマッピングが行われないため、
データベースコンテキストの`OnModelCreating`で明示的にプロパティのマッピングを定義します。

```diff
   class InMemoryDbContext : DbContext {
     public DbSet<Book> Books { get; set; }

-    protected override void OnModelCreating(ModelBuilder modelBuilder) =>
+    protected override void OnModelCreating(ModelBuilder modelBuilder) {
+      modelBuilder.Entity<Book>(book => {
+        book.HasKey(nameof(Book.Id));
+        book.Property(e => e.Title);
+        book.Property(e => e.Author);
+      });
       modelBuilder.Entity<Book>().HasData(
         new Book(id: 1, title: "Flutter入門", author: "掌田 津耶乃"),
         new Book(id: 2, title: "Nuxt.jsビギナーズガイド", author: "花谷 拓磨"),
         new Book(id: 3, title: "IntelliJ IDEAハンズオン", author: "山本 裕介")
       );
+    }
   }
```

これでエンティティクラスのプロパティを`get`のみにすることができました。

### `Id`をコンストラクタから取り除く

今回、エンティティクラスではコンストラクタの引数に`Id`を初期値として渡せるようにしました。
本来`Entity Framework Core`では`Id`は自動でインクリメントされ採番されます。
自動で採番される`ID`にはコンストラクタから値を渡す必要がありません。

```csharp
class Book {
  public int Id { get; private set; } // private な setter を定義（EF Coreに使われる）
  public string Title { get; }
  public string Author { get; }

  public Book(string title, string author) {  // コンストラクタの引数に id を渡さない
    Title = title;
    Author = author;
  }
}
```

`DbContext.OnModelCreating`では`ID`のマッピングの定義は不要です。
（エンティティクラス側で`private set`があるため）

```csharp:InMemoryDbContextの抜粋
  protected override void OnModelCreating(ModelBuilder modelBuilder) =>
    modelBuilder.Entity<Book>(book => {
      // book.HasKey(nameof(Book.Id)); 不要
      book.Property(e => e.Title);
      book.Property(e => e.Author);
    });
```

`ID`が自動採番されているのを確認します。

```csharp
using (var context = new InMemoryDbContext()) {
  Book[] newbooks = {
    new Book(title: "Flutter入門", author: "掌田 津耶乃"),
    new Book(title: "Nuxt.jsビギナーズガイド", author: "花谷 拓磨"),
    new Book(title: "IntelliJ IDEAハンズオン", author: "山本 裕介")
  };
  context.Books.AddRange(newbooks);
  context.SaveChanges();

  foreach (var book in context.Books.AsNoTracking()) {
    Console.WriteLine($"{book.Id} {book.Title} {book.Author}");
  }
}
```

```:結果（IDが採番されているのが確認できる）
1 Flutter入門 掌田 津耶乃
2 Nuxt.jsビギナーズガイド 花谷 拓磨
3 IntelliJ IDEAハンズオン 山本 裕介
```


ただし、`Id`をコンストラクタから取り除いた場合、`OnModelCreating`のフックでシードデータを設定することはできません。
`modelBuilder.Entity<TEntity>().HasData`でシードデータ生成する際、`ID`は自動採番されないためです。

```:modelBuilder.Entity<TEntity>().HasDataで初期データをシーディングしようとした場合
Unhandled Exception: System.InvalidOperationException:
  The seed entity for entity type 'Book' cannot be added because there was no value provided for the required property 'Id'.
```

`ASP.NET Core MVC`や`Web API`では以下のように`Program.cs`でシードデータを採番するコードを自分で書くとよいと思います。

```csharp
public static void Main(string[] args) {
  var host = BuildWebHost(args);
  using (var scope = host.Services.CreateScope()) {
    var provider = scope.ServiceProvider;
    try {
      var context = provider.GetRequiredService<InMemoryDbContext>();
      context.Books.AddRange(new[] {
        new Book(title: "Flutter入門", author: "掌田 津耶乃"),
        new Book(title: "Nuxt.jsビギナーズガイド", author: "花谷 拓磨"),
      });
      Context.SaveChanges(); 
    } catch (Exception ex) {
      var logger = provider.GetRequiredService<ILogger<Program>>();
      logger.LogError(ex, "データベース初期化中にエラーが発生しました。");
    }
  }
  host.Run();
}
```


[^1]: セッターも`public`なら`new Book { Id = 1, Title = "Scala本", Author = "ゴンタロウ" }`のように初期化できる。