はじめに
.NET 6 ではコンソールアプリケーションのテンプレートが大きく変わり、下記のようにとてもシンプルなプロジェクトが作成されるようになりました。ただ、とてもシンプルな反面、なにから手を付けてよいのか少し迷ってしまいます。
EFCore 6 では EFCore 5 まであった「特定のメソッド名が無いとマイグレーションは動かせない」という制限をから解放されています。今回はこれまでのマイグレーションの実行方法を振り返りながら、.NET 6 + EFCore 6 のコンソールプロジェクトでトップレベルステートメントを使ったアプリがなぜデータベースマイグレーションを実行できるのかを確認していきます。
dotnet ef サブコマンドでデータベースを管理する
dotnet ef
サブコマンドでマイグレーションを行うためには次の前提条件があります。
- 環境に .NET SDK 及び
dotnet ef
サブコマンドがインストールされている -
Microsoft.EntityFrameworkCore.Design
パッケージが追加されている - デザイン時のデータコンテキストクラスが存在する(IHostBuilder を利用しない場合)
- 特定の名前を持つ初期化コードが存在する(EFCore 6 以前)
-
IHostBuilder
のRun
メソッドを呼び出すコードが存在する(EFCore 6 以降)
環境に .NET SDK 及び dotnet ef サブコマンドがインストールされている
dotnet ef
サブコマンドは .NET SDK を必要とします。また、.NET Core 3.1 からは dotnet ef
サブコマンドは .NET SDK とは別にインストールする必要があります。 次のコマンドを実行して dotnet ef
サブコマンドをインストールするか、アップデートしましょう。
❯ dotnet tool install --global dotnet-ef
次のコマンドを使用してツールを呼び出せます。dotnet-ef
ツール 'dotnet-ef' (バージョン '6.0.0') が正常にインストールされました。
❯ dotnet tool update --global dotnet-ef
ツール 'dotnet-ef' がバージョン '5.0.0' からバージョン '6.0.0' に正常に更新されました。
Microsoft.EntityFrameworkCore.Design
パッケージが追加されている
マイグレーションの実行には対象のプロジェクトに Microsoft.EntityFrameworkCore.Design
パッケージが必要です。
例えば、MySQL のマイグレーションを含むコンソールプロジェクト作る場合、スタートアッププロジェクトには最低限次のパッケージを追加しておく必要があります。
❯ dotnet new console -o Sample
❯ cd Sample
❯ dotnet add package Microsoft.EntityFrameworkCore.Design
❯ dotnet add package Microsoft.Extensions.Hosting
❯ dotnet add package Pomelo.EntityFrameworkCore.MySql
Microsoft.EntityFrameworkCore.Design
がスタートアッププロジェクトに含まれない場合、マイグレーションを登録したり、実行したりする場合に次のようなエラーメッセージが表示されます。
❯ dotnet ef migrations add first
Build started...
Build succeeded.
Your startup project 'Sample' doesn't reference Microsoft.EntityFrameworkCore.Design. This package is required for the Entity Framework Core Tools to work. Ensure your startup project
is correct, install the package, and try again.
デザイン時のデータコンテキストクラスが存在する(IHostBuilder を利用しない場合)
EFCore を利用した最も単純なプログラムは次のようなコードでしょう。
var options = new DbContextOptionsBuilder<BlogDbContext>()
.UseMySql(connectionString, ServerVersion.Parse("8.0"))
.Options;
using var dbContext = new BlogDbContext(options);
foreach (var blog in await dbContext.Blogs.ToListAsync())
{
Console.WriteLine($"{blog.Id}, {blog.Name}");
}
上記のコードはプログラム自体は問題なく実行されますが、dotnet ef コマンドなどでマイグレーションを登録したり、実行したりしたりすると次のようなエラーメッセージが表示されます。
❯ dotnet ef migrations add first
Build started...
Build succeeded.
An error occurred while accessing the Microsoft.Extensions.Hosting services. Continuing without the application service provider. Error: Unable to build IHost
Unable to create an object of type 'BlogDbContext'. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728
これは EFCore がマイグレーションの実行に必要なデータコンテキストの情報を取得できないために発生します。
このため、EFCore 6 以前ではマイグレーションを実行するために次のような特徴のメソッドやクラスが必要でした。
-
IDesignTimeDbContextFactory<TContext>
を実装したクラス - データコンテキストを含む
static IHostBuilder CreateHostBuilder(string[] args)
もしくはstatic IWebHostBuilder CreateWebHostBuilder(string[] args)
というシグネチャを持つメソッド
最も単純な解決方法は、IDesignTimeDbContextFactory<TContext>
を実装する次のようなクラスをプロジェクトに含めることです。EFCore はこのクラスをもとにマイグレーションを実行します。
public class BlogDesingTimeDbContext : IDesignTimeDbContextFactory<BlogDbContext>
{
public BlogDbContext CreateDbContext(string[] args)
{
const string connectionString = "Server=localhost;Port=3306;Uid=root;Pwd=root;Database=mydb";
var options = new DbContextOptionsBuilder<BlogDbContext>()
.UseMySql(connectionString, ServerVersion.Parse("8.0"))
.Options;
return new BlogDbContext(options);
}
}
特定の名前を持つ初期化コードが存在する(EFCore 6 以前)
IHostBuilder
を利用して DI コンテナにデータコンテキストを登録するようなアプリの場合、EF Core 6 までは CreateHostBuilder
や CreateWebHostBuilder
といったメソッド名を頼りにデータコンテキストを取得していました。
これらの名前が利用できない場合は前述の IDesignTimeDbContextFactory<TContext>
を実装したクラスを用意する必要がありました。
public static int Main(string[] args)
{
await CreateHostBuilder(args)
.Build()
.Services
.GetRequiredService<Runner>()
.Run();
}
static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.ConfigureServices((context, services) =>
{
services.AddScoped<Runner>();
services.AddDbContext<BlogDbContext>(options =>
{
options.UseMySql(connectionString, ServerVersion.Parse("8.0"));
});
});
class Runner
{
private readonly BlogDbContext _dbContext;
public Runner(BlogDbContext dbContext)
{
_dbContext = dbContext;
}
public async Task RunAsync()
{
foreach (var blog in await _dbContext.Blogs.ToListAsync())
{
Console.WriteLine($"{blog.Id}, {blog.Name}");
}
}
}
IHostBuilder
の Run
メソッドを呼び出すコードが存在する(EFCore 6 以降)
EFCore 6 のマイグレーションでは IHostBuilder
の Build
メソッドの実行が完了するのを監視しデータコンテキストを取得するようになったため、CreateHostBuilder
というメソッドを含んでいなくてもマイグレーションを実行できるようになりました。
.NET 6 のトップレベルステートメントと自然に融合しているため、初期化コードが分離されず上から下に読み下せるようになったのでこれまでよりもわかりやすくなりましたね。
var builder = Host.CreateDefaultBuilder(args);
builder.ConfigureServices((context, services) =>
{
services.AddScoped<Worker>();
services.AddDbContext<BlogDbContext>(options =>
{
options.UseMySql(
context.Configuration.GetConnectionString("blogdbcontext"),
ServerVersion.Parse("8.0"),
mysqlOption => { mysqlOption.EnableRetryOnFailure(10); })
.EnableSensitiveDataLogging();
});
});
await builder.Build().Services.GetRequiredService<Worker>().RunAsync();
.NET 6 の HostBuilder と EFCore のマイグレーション
EFCore はマイグレーション起動時に HostFactoryResolver.ResolveServiceProviderFactory を呼び出してデータコンテキストを含むサービスコンテナを探します。
繰り返しになりますが、EFCore 5 までは、マイグレーションの初期化時に CreateHostBuilder
や CreateWebHostBuilder
というメソッドの名前をもとに解決していました。
EFCore 6 からは、HostBuilder
の Build
メソッドがビルド開始前と完了時に DiagnosticListener
を通じて発行する HostBuilding イベントと HostBuilt イベントを HostingListener が監視し、ServiceProviderを取得することで解決しています。これによって単なる文字列ではなく、意味のある初期化コードをもとにマイグレーションが実行できるようになりました。
詳細については下記の投稿が詳しいです。
おわりに
コンソールプロジェクトはプロジェクトテンプレートにほとんど何も書かれていないので、初めて見たときは自由度が高すぎてどうしたらよいかわからなくなくなりますが、IHostBuilder を使ったアプリケーションを作成するのであれば ASP.NET Core と変わりがありません。