Entity Framework Core の In-Memory データベースに初期データを投入する

## はじめに

テストや環境を汚さないために In-Memory データベースを使用する際に初期データを投入する方法を調べました。

データベースコンテキストの`OnModelCreating`メソッドにフックすることで初期化します。
`OnModelCreating`はデータベースコンテキストが初期化される際に１度だけ呼ばれます。

`HasData`メソッドの初期化は、Entity Framework Coreによる`ID`の自動設定 が行われません。
そのため、自分で`ID`を取得する必要があります。

```csharp
public DbSet<Book> Books { get; set; }

protected override void OnModelCreating(ModelBuilder modelBuilder) =>
  modelBuilder.Entity<Book>().HasData(
    new Book { ID = 1, Name = "アンドロイドは電気羊の夢を見るか?" },
    new Book { ID = 2, Name = "幼年期の終り" },
    new Book { ID = 3, Name = "一九八四年" }
);
```

## サンプルアプリ In-Memory データベース を使った Web API

### プロジェクトの雛形の作成

```bash
# テンプレートには Web API を選択
$ dotnet new webapi -n SeedingInMemoryDb
$ cd SeedingInMemoryDb
# In-Memory データベースを使うためにライブラリをインストール
$ dotnet add Package Microsoft.EntityFrameworkCore.InMemory
```

### モデルとデータベースコンテキスト

```csharp:Models/Books.cs
public class Book {
  public int ID { get; set; }
  public string Name { get; set; }
}
```

```csharp
// using Microsoft.EntityFrameworkCore;
// using SeedingInMemoryDb.Models;

public class SampleInMemoryDbContext : DbContext {
  public SampleInMemoryDbContext(DbContextOptions options) : base(options) { }

  public DbSet<Book> Books { get; set; }

  protected override void OnModelCreating(ModelBuilder modelBuilder) =>
    modelBuilder.Entity<Book>().HasData(
      new Book { ID = 1, Name = "アンドロイドは電気羊の夢を見るか?" },
      new Book { ID = 2, Name = "幼年期の終り" },
      new Book { ID = 3, Name = "一九八四年" }
    );
}

```

### `Startup`クラス

In-Memory データベースを使う設定をします。
いつも通り`AddDbContext`する際にオプションから`UseInMemoryDatabase`メソッドを呼びます。
`UseInMemoryDatabase`メソッドの引数のデータベース名は必須みたいです。

```csharp:Startup.cs
// using Microsoft.EntityFrameworkCore;
// using Microsoft.Extensions.DependencyInjection;

public void ConfigureServices(IServiceCollection services) {
  services.AddDbContext<SampleInMemoryDbContext>(options => 
    options.UseInMemoryDatabase("sample_in_memory_db");
  );
  services.AddControllers();
}
```

### エントリーポイント

In-Memory データベースを構築し初期データを投入します。

`Program.cs`ではまだ DI コンテナが設定前であるため、データベースコンテキストを DI できません。
サービスプロバイダーからインスタンスを取得し`EnsureCreatedAsync`メソッドを呼んで DB を作成します。

```csharp:Program.cs
// using Microsoft.AspNetCore.Hosting;
// using Microsoft.Extensions.DependencyInjection;
// using Microsoft.Extensions.Hosting;
// using System.Threading.Tasks;

public class Program {
  public static async Task Main(string[] args) {
    IHost host = BuildHost(args);
    using IServiceScope scope = host.Services.CreateScope();
    IServiceProvider provider = scope.ServiceProvider;
    using var context = provider.GetRequiredService<SampleInMemoryDbContext>();
    await context.Database.EnsureCreatedAsync();
    host.Run();
  }

  public static IHost BuildHost(string[] args) =>
    Host.CreateDefaultBuilder(args)
        .ConfigureWebHostDefaults(web => web.UseStartup<Startup>()
        .Build();
}
```
### コントローラークラス

In-Memory データベース内の書籍を全件返すエンドポイント`/api/books`を作ってみます。

```csharp:Controllers/BooksController.cs
// using Microsoft.AspNetCore.Mvc;
// using Microsoft.EntityFrameworkCore;
// using SeedingInMemoryDb.Models;
// using System.Collections.Generic;
// using System.Threading.Tasks;

[Route("api/[controller]")]
public class BooksController : Controller {
  private readonly SampleInMemoryDbContext _db;
  public BooksController(SampleInMemoryDbContext db) => _db = db;

  [HttpGet]
  public async Task<ActionResult<IEnumerable<Book>>> Get() =>
    await _db.Books.ToListAsync();
}
```

### 実行結果

実行し`localhost:{ポート番号}/api/books`にアクセスします。
In-Memory データベースに初期データが投入され、そのデータを全件取得できることが確認できました。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/195024/d61a38ed-109e-18d2-b844-580609d0abce.png)


実行環境は以下の通りです。

- Windows X64
- Visual Studio 2019 (v16.4)
- .NET Core SDK 3.1.101
- C# 8.0