😟

.NET Aspireに依存したMySQLとEntity FrameworkとASP.NET Coreでマイグレーションを作成する

2025/01/25に公開

環境

.NET AspireとEntity Frameworkでマイグレーション(ドキュメント的には移行)を実現するためのプロジェクト構成・コマンド例については公式ドキュメントがすでに存在する。

https://learn.microsoft.com/ja-jp/dotnet/aspire/database/ef-core-migrations

上記のドキュメントとほぼ同じように、ソリューションに.NET 9をランタイムとして設定した以下のプロジェクトがいる環境で実行する。

  • .NET Aspireのプロジェクト (AppHost)
  • ASP.NET Coreのプロジェクト (以下、Web.Server)
  • クラスライブラリのプロジェクト(DbContextとCode Firstのモデルが入っている。Migrationも入れたい。以下、Web.Data)

ただし公式ドキュメントと違う点は、利用するコネクタが
Aspire.Pomelo.EntityFrameworkCore.MySql

Pomelo.EntityFrameworkCore.MySql 9.0.0-preview.2.efcore.9.0.0
という点。
※Previewを使うのはまだEF9対応版が出ていないため。

問題

環境に記述した通り、すでに構成例となるドキュメントはすでに整備されている。
なので基本はこれをもとにプロジェクトを作成していけば、実行手前の段階までは持っていくことができる。
このドキュメントで詰まるのが「マイグレーションの作成」というところ。

ここで行うのが、一番最初のマイグレーションを作る作業。
そこでマイグレーションを作成すれば以後のMigrationServiceが動作できるようになるのだが、次のエラーが出て作成することができない。

PS C:\…\Web\Web.Server> dotnet ef migrations add InitialCreate --project ..\..\Web.Data\Web.Data.csproj
Build started...
Build succeeded.
info: MySqlConnector.MySqlConnection[0]
      Session 0.1 connect timeout expired connecting to IP address 172.21.48.1 for host name
      System.Net.Sockets.SocketException (10038): ソケット以外のものに対して操作を実行しようとしました。 172.21.48.1:3306
         at System.Net.Sockets.Socket.DoConnect(EndPoint endPointSnapshot, SocketAddress socketAddress)
         at System.Net.Sockets.Socket.Connect(EndPoint remoteEP)
         at System.Net.Sockets.TcpClient.Connect(IPEndPoint remoteEP)
         at MySqlConnector.Core.ServerSession.OpenTcpSocketAsync(ConnectionSettings cs, ILoadBalancer loadBalancer, Activity activity, IOBehavior ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/Core/ServerSession.cs:line 1247
warn: Polly[3]
      Execution attempt. Source: 'Microsoft.Extensions.Hosting.AspireEFMySqlExtensions.ServerVersion/(null)/Retry', Operation Key: '', Result: 'Connect Timeout expired.', Handled: 'True', Attempt: '0', Execution Time: 15031.7774ms
      MySqlConnector.MySqlException (0x80004005): Connect Timeout expired.
       ---> MySqlConnector.MySqlException (0x80004005): Connect Timeout expired.
         at MySqlConnector.Core.ServerSession.OpenTcpSocketAsync(ConnectionSettings cs, ILoadBalancer loadBalancer, Activity activity, IOBehavior ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/Core/ServerSession.cs:line 1266
         at MySqlConnector.Core.ServerSession.ConnectAsync(ConnectionSettings cs, MySqlConnection connection, Int64 startingTimestamp, ILoadBalancer loadBalancer, Activity activity, IOBehavior ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/Core/ServerSession.cs:line 427
         at MySqlConnector.Core.ServerSession.ConnectAndRedirectAsync(ILogger connectionLogger, ILogger poolLogger, IConnectionPoolMetadata pool, ConnectionSettings cs, ILoadBalancer loadBalancer, MySqlConnection connection, Action`4 logMessage, Int64 startingTimestamp, Activity activity, IOBehavior ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/Core/ServerSession.cs:line 697
         at MySqlConnector.Core.ServerSession.ConnectAndRedirectAsync(ILogger connectionLogger, ILogger poolLogger, IConnectionPoolMetadata pool, ConnectionSettings cs, ILoadBalancer loadBalancer, MySqlConnection connection, Action`4 logMessage, Int64 startingTimestamp, Activity activity, IOBehavior ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/Core/ServerSession.cs:line 702
         at MySqlConnector.MySqlConnection.CreateSessionAsync(ConnectionPool pool, Int64 startingTimestamp, Activity activity, Nullable`1 ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/MySqlConnection.cs:line 1099
         at MySqlConnector.MySqlConnection.CreateSessionAsync(ConnectionPool pool, Int64 startingTimestamp, Activity activity, Nullable`1 ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/MySqlConnection.cs:line 1114
         at MySqlConnector.MySqlConnection.OpenAsync(Nullable`1 ioBehavior, CancellationToken cancellationToken) in /_/src/MySqlConnector/MySqlConnection.cs:line 567
         at MySqlConnector.MySqlConnection.Open() in /_/src/MySqlConnector/MySqlConnection.cs:line 522
         at Microsoft.EntityFrameworkCore.ServerVersion.AutoDetect(String connectionString)
         at Microsoft.Extensions.Hosting.AspireEFMySqlExtensions.<>c__2`1.<AddMySqlDbContext>b__2_5(String cs) in /_/src/Components/Aspire.Pomelo.EntityFrameworkCore.MySql/AspireEFMySqlExtensions.cs:line 108
         at Polly.ResiliencePipeline.<>c__36`2.<Execute>b__36_0(ResilienceContext _, ValueTuple`2 state)

または

PS C:\…\Web\Web.Server> dotnet ef migrations add InitialCreate --project ..\..\Web.Data\Web.Data.csproj
Build started...
Build succeeded.
The exception 'The string argument 'connectionString' cannot be empty.' was thrown while attempting to find 'DbContext' types. For the different patterns supported at design time, see https://go.microsoft.com/fwlink/?linkid=851728

が表示されてマイグレーションが作成することができない。

接続タイムアウトの対処

Pomelo.EntityFrameworkCore.MySqlで用意されている機能として、接続先のサーバーバージョンによって発行するSQLを切り替えて互換性を維持する機能がある。
そのサーバーバージョンを決めるのに、手動設定で行うか自動設定で行うか選ぶことができる。

Aspire.Pomelo.EntityFrameworkCore.MySqlを使うと、指定しない場合は自動設定(ServerVersion.AutoDetect)になるのだが、この自動設定というのはいったんMySQL/MariaDBに接続してバージョン情報を取得するというもの。
dotnet ef migrations addコマンドで実行されている場合、.NET Aspireを介しているわけではないので当然データベースは動作しておらず、接続できない。

そのため自動設定のための接続をしないために、手動でバージョン情報を渡してあげる必要がある。

ハードコーディングで回避するには、Web.Server(ASP.NET Coreが動くプロジェクト、コマンドでカレントディレクトリにするもの)のProgram.csを以下のように変更する。

builder.AddMySqlDbContext<ApplicationDbContext>(
    connectionName: "mariadb",
    settings =>
    {
        settings.ServerVersion = "8.0.41";
    });

または、appsettings.jsonにバージョン情報を記述してそれを取得するようにすればよい。

接続文字列が空の対処

The exception 'The string argument 'connectionString' cannot be empty.'

というのは文字通り接続文字列が空だといわれている。

.NET Aspireから起動すれば、自動的に接続情報が注入されるため設定が必要ないのだが、dotnet ef migrations addコマンドで実行されている場合、.NET Aspireを介しているわけではないので当然接続情報が注入されることはない。

手動でWeb.Server(ASP.NET Coreが動くプロジェクト、コマンドでカレントディレクトリにするもの)のappsettings.jsonに対して、以下のような設定を注入すればこの問題は回避できる。

{
  "ConnectionStrings": {
    "mariadb": "server=localhost;port=3306;database=dummydb;"
  },
~省略~
}

あくまで設定が入っていれば良いのでDB自体にはつながらなくても大丈夫。

終わりに

SQL Serverは問題ないのに、MySQLにするとドキュメントに書いてない問題が発生するの、.NET Coreあるあるだよね。

Aspireの公式リポジトリでも.NET Aspire+EFの問題は議論されていて[1]、その結果上で使ったドキュメントができてはいるのですが、こうもう一声というか…。

脚注
  1. https://github.com/dotnet/aspire/issues/398 ↩︎

Discussion