EFCoreに深入り(DbContextをnewするあたり)
2/22追記、この記事の内容の応用としてPomelo.EFCoreのシャーディング対応をgistにあげてみました Pomelo.EFCoreでSharding · GitHub
この記事は、以下のコードを読んでみた結果です。
github.com
当記事内のコードはどれも簡略化したものなので本当の実装が気になる型は上記公式リポジトリのコードを確認してください。
またDbContextPool
までは踏み込めていません。後日調べようと思っています。
DbContext
継承クラスは、基本的にDbContextOptions<TDbContext>
を受け取るコンストラクタが必要です。
public partial class MyDbContext : DbContext { public MyDbContext (DbContextOptions<MyDbContext > options) : base(options) { } }
これをDIに登録するときはStartup.csに以下のようなコードを書きます。例はPomelo.EntityFrameworkCoreのものですが、SqlServerでも似たようなものです。
public void ConfigureServices(IServiceCollection services) { services.AddDbContext<MyDbContext>(b => b.UseMySql( Configuration.GetConnectionString("ConnectionString"), new MySqlServerVersion(new Version(8, 0)) ) ); }
ラムダの引数であるところのb
はDbContextOptionsBuilder<MyDbContext>
になります。DbContextOptions<TDbContext>
を直接弄るのではなく、Builderのほうを弄ることで、弄った内容がOptionsに反映される、という形になっています。
AddDbContext<TDbContext>
拡張メソッドで登録されるDI設定は、デフォルトがServiceLifetime.Scoped
です。つまりWebの場合は同一リクエスト内ならば同じオブジェクトが使いまわされます。AddDbContext<TDbContext>
のオーバーロードによってはServiceLifetime.Scoped
を引数で指定できますが、ほとんどのケースでScopedのほうが望ましいでしょう。
また、DbContext以外にもDbContextOptionsのほうもDI登録されていて、同じようにServiceLifetime.Scoped
の寿命を持ちます。MyDbContextのコンストラクタに入ってくるDbContextOptionsはDIが解決しているのです。
思うにDbContextOptionsBuilder<MyDbContext>
やDbContextOptions<MyDbContext>
がジェネリックな型になっているのはDIの都合です。もしジェネリックでないDbContextOptions
を引数に取るとしたら、DbContext継承型が複数あった場合、つまりMyDbContext1とMyDbContext2で引数が同じオブジェクトになってしまいますから、分離するために型をつけているのです。このあたりはちょっとしょぼいというか強引というか。
さて、ServiceLifetime.Scoped
なのでMyDbContext
はリクエスト毎にnewされるわけですが、コンストラクタ引数のDbContextOptions
も同じ寿命であって、リクエスト毎に以下のようなコードが呼び出されることになります。(簡略化してあります)
private DbContextOptions<TDbContext> CreateOptions<TDbContext>(Action<DbContextOptionsBuilder<TDbContext>>? optionAction) { var builder = new DbContextOptionsBuilder<TContext>(); optionAction?.Invoke(builder); //UseMySqlとかUseSqlServerとかがこの中で呼ばれてConnectionStringをセットしたりする return builder.Options; }
//DI登録 services.TryAdd(new ServiceDescriptor(typeof(TDbContext), lifetime); services.TryAdd(new ServiceDescriptor(typeof(DbContextOptions<TDbContext>), sp => CreateOptions(optionAction), lifetime);
そもそもDbContextOptions
とは何ぞやというと、複数のIDbContextOptionsExtension
を束ねたDictionaryです。
abstract class DbContextOptions { IReadOnlyDictionary<Type, IDbContextOptionsExtension> _extensions; }
IDbContextOptionsExtension
自体にめぼしい機能はありませんが、これを実装した抽象クラスであるRelationalOptionsExtension
がConnectionStringを持っており、SqlServerOptionsExtension
やMySqlOptionsExtension
の親クラスになっています。
public abstract class RelationalOptionsExtension : IDbContextOptionsExtension { string _connectionString; //他にもTimeoutとかMigrationAssemblyとか設定いろいろ } public class SqlServerOptionsExtension : RelationalOptionsExtension { } public class MySqlOptionsExtension : RelationalOptionsExtension { }
DbContextOption
に対してUseMySql
はMySqlOptionsExtension
を突っ込むし、UseSqlServer
はSqlServerOptionsExtension
を突っ込みます。
public static DbContextOptionsBuilder UseMySql(this DbContextOptionsBuilder builder, string connectionString, ServerVersion serverVersion, Action<MySqlDbContextOptionsBuilder> optionsAction) { MySqlOptionsExtension extension = GetOrCreateExtension(builder) .WithServerVersion(serverVersion) .WithConnectionString(connectionString); builder.AddOrUpdate(extension); optionsAction?.Invoke(new MySqlDbContextOptionsBuilder(builder)); return builder; }
さて、これでMyDbContext
がnewされるまでの大体の要素が出そろいました。
DbContextOptionsBuilder
をnewするUseMySql
やUseSqlServer
相当のコードで、IDbContextOptionsExtension
をDbContextOptionsBuilder.Options
に登録する。この過程でConnectionStringが決まるActivatorUtilities.CreateInstance<MyDbContext>
でDbContextOptionsBuilder.Options
を引数に渡してインスタンスを作る- 上記1~3を行うファクトリをDI登録する
これで生成周りを乗っ取れそうです。RDS自前シャーディングなどでConnectionStringをKeyによって切り替えたい時などに使えるかもと思っています。