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


はじめに

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

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

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

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

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

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

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

Models/Books.cs
public class Book {
  public int ID { get; set; }
  public string Name { get; set; }
}
// 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メソッドの引数のデータベース名は必須みたいです。

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 を作成します。

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を作ってみます。

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 データベースに初期データが投入され、そのデータを全件取得できることが確認できました。

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

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