Migrationやデザインタイムの挙動を別アセンブリに閉じ込める

「Migration用のコードがアプリ実行時に含まれるのが嫌だ。ツール実行時だけ存在すればいい」というニーズもごく普通にあろうかと思います。 EF Coreはそのあたりのオプションがちゃんと用意されています。大丈夫だ問題ない。が、結構わかりにくい挙動をしますのでメモを残します。

ソリューション構成は以下とします。

WebApplication1
  WebApplication1 (ASP.NET Coreプロジェクト)
  AppCore (MyDbContextのあるアセンブリ)
  AppCore.Migrations (MyDbContextのMigrationのコードの生成先)

MyDbContextからパラメータのないコンストラクタを抹殺する

さきにASP.NET固有の問題を片付けます。 ASP.NETからEF Coreを使用する場合、AddDbContextPool<MyDbContext>でPoolを使って性能を稼ぎたいはずですが、Poolを使う場合はDbContextのパラメータのないデフォルトコンストラクタは使用できません。
DBファーストでscaffoldを使ってMyDbContextを作成する場合はデフォルトコンストラクタが勝手に作成されるのでこれを手で消す必要があります。(DbContextOptions<MyDbContext>を受け取るコンストラクタのみとします)

ところが、このデフォルトコンストラクタの削除によってMigrationにエラーがでるようになります。

Migrationを実行するプロジェクト(dotnet ef migrations addを実行するプロジェクト)ではMigrationさんがMyDbContextをnewしようとします。
このとき、ASP.NETのようにpublic static IHostBuilder CreateHostBuilder(string[] args)が存在するプロジェクトではDI経由でMyDbContextを生成してもらえるので、DbContextOptions引数をもつコンストラクタを読んでもらえるのですが、
Migration実行プロジェクトにCreateHostBuilderがない場合は、MigrationさんはデフォルトコンストラクタでMyDbContextをnewしようとします。Poolのために消しちゃうのでエラーとなります。

この問題を解決するため、Migrationを実行するプロジェクトにCreateHostBuilderがない場合は、IDesignTimeDbContextFactory<TDbContext>を実装したクラスを作る必要があります。

using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Design;

public class DesignTimeTestDbContextFactory : IDesignTimeDbContextFactory<MyDbContext>
{
    public MyDbContext CreateDbContext(string[] args)
    {
        var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
        optionsBuilder.UseSqlServer("Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=testdb;");
        return new MyDbContext(optionsBuilder.Options);
    }
}

これでMyDbContext側には必要なコンストラクタのみを残すことができて整います。 また、上記のinterfaceはMicrosoft.EntityFrameworkCore.Designパッケージにありますが、このパッケージはMigrationを実行するプロジェクトには必ず必要なものです。入れておきましょう。
今回の例ソリューション構成だと、AppCore.Migrationsプロジェクトにパッケージを入れます。

MigrationsAssemblyを指定する

Migrationの既定の動作は、「MigrationファイルはMyDbContextが存在するアセンブリに出力する」というものです。
このルールからそれる場合、オプションでその旨を伝える必要があります。
これは、Migrationを実行するプロジェクトがどこであれ、MyDbContextと異なるアセンブリにMigrationファイルを出力するならば必要です。

public class DesignTimeTestDbContextFactory : IDesignTimeDbContextFactory<MyDbContext>
{
    public MyDbContext CreateDbContext(string[] args)
    {
        var optionsBuilder = new DbContextOptionsBuilder<MyDbContext>();
        optionsBuilder.UseSqlServer(
            "Data Source=(localdb)\\MSSQLLocalDB;Initial Catalog=testdb;",
            b => b.MigrationsAssembly("AppCore.Migrations"));
        return new MyDbContext(optionsBuilder.Options);
    }
}

Migraionファイルが作成されるプロジェクトに、MyDbContextをもつプロジェクトへの参照を持たせる

Migrationで作成されるC#ソースコードはMyDbContextへの参照があるのでコンパイルを通すために単に必要です。 例のソリューション構成だと、AppCore.DesignがAppCoreを参照します。

Migrationファイルが生成されるプロジェクトのアセンブリの出力先を、Migrationを実行するプロジェクトの出力先と揃える

これはわかりにくいですが、仮に今回の例のソリューション構成でAppCoreでMigraionを実行する場合は、 AppCore/bin/$(Configuration)/AppCore.Migrations.dll
が存在するようにAppCore.Migrations側のOutputPathを設定しなければならない、ということです。
この挙動をさせるために、AppCore→AppCore.Migrationsのプロジェクト参照をもたせてしまうと循環参照になるのでNGです。

AppCore.MigrationsプロジェクトでMigrationを実行する場合はこの条件を勝手に満たすので、Migrationファイルが作成されるプロジェクトをMigration実行プロジェクトにしてしまうのが楽です。

もしもMigrations実行プロジェクトとMigrationsファイルが生成されるプロジェクトを分ける場合は、dotnet ef migrationsコマンドでも--projectオプションを渡す必要があります。

例のソリューション構成でAppCoreやWebApplication1でMigrationを実行する場合は以下のようになります。

dotnet ef migrations add MigrationName --project ../AppCore.Migrations