きなこもち.net

.NET Framework × UiPath,Orchestrator × Azure × AWS × Angularなどの忘備録

Entity Framework Core × DbContext × 継承する方法

目的

Entity Framework coreで、DBContextを継承したクラスが機能しない原因を考える
f:id:kinakomotitti:20210305231822p:plain

背景

DB Firstのアプローチで、以下のCommonDbContextを自動生成した。
ex. CommonDBContext: DBContext

このCommonDBContextをさらに継承させ、複数のDBContextで自動生成された部分を使いまわそうとしたが、期待通りに動かなかった。パット調べたところ、似たような問題に直面した記事や議論があったため、まとめてみた。

類似現象

似たような現象にはまった人たちの情報を集めてみた。

Case1: Entity Framework Core DbContext inheritance problem with DbOptions in constructor

現象

自動生成されたDbContextに機能を追加実装して利用しようとしている。DBに変更があった場合、DbContextが作り直されるため、追加で実装した部分に影響が出てしまう。それを避けるため自動生成されたDbContextから派生させた新しいDbContextを作成して、そこに独自の実装を追加しようとしたらしい。

  // 自動生成されたやつ
  public partial class ApplicationDatabaseContextGenerated : DbContext
  {
    public ApplicationDatabaseContextGenerated() {}
    public ApplicationDatabaseContextGenerated(DbContextOptions<ApplicationDatabaseContextGenerated> options) : base(options) {}
  }

  // 自動生成されたものからの派生クラス、かつ追加機能を持たせたやつ。
  public class ApplicationDatabaseContext : ApplicationDatabaseContextGenerated
  {
    public ILogger<ApplicationDatabaseContext> Logger { get; protected set; }
    public ApplicationDatabaseContext() : base() {}
    public ApplicationDatabaseContext(DbContextOptions<ApplicationDatabaseContext> options, ILogger<ApplicationDatabaseContext> logger) : base(options)
    {
      Logger = logger ?? throw new ArgumentNullException(nameof(logger));
    }
  }

  //StartUp.csでの設定
  public void ConfigureServices(IServiceCollection services)
  {
    services.AddDbContext<ApplicationDatabaseContext>(options => options.UseNpgsql(Configuration.GetConnectionString("db1-connection")));
  }

この実装をしたとき、cannot convert from 'DbContextOptions' to 'DbContextOptionsというエラーが出た。AddDbContextに、派生クラスを指定したら、(DIが実行されるときに)親クラスに変換できないというエラーが出た・・・ということらしい。
変換できないから、おおもとの親クラスであるDbContextをDbContextOptionsのジェネリッククラスに指定してみたけどダメだったとのこと。

  public partial class ApplicationDatabaseContextGenerated : DbContext
  {
    public ApplicationDatabaseContextGenerated() {}
    //                                                             ↓これ
    public ApplicationDatabaseContextGenerated(DbContextOptions<DbContext> options) : base(options) {}
  }

解決策

AddDbContext<派生クラス>では、DbContextOptions<派生クラス>型のインスタンスを生成する。DbContextOptions<派生クラス>が親クラスのコンストラクタにわたってきても対応できるようにするため、親クラスにジェネリックタイプではないDbContextOptions型の引数を受け付けるコンストラクタを追加すれば解決する.

public ApplicationDatabaseContextGenerated(DbContextOptions options) : base(options) { }

Case2: How do I implement DbContext inheritance for multiple databases in EF7 / .NET Core

現象

複数のシステム向けに、複数のDBがある状況。それぞれのDBには、ユーザー情報や設定情報を格納するための共通的な仕様のテーブル(とスキーマ?)が定義されている。
この共通する情報をそれぞれのDBContextで実装すると重複するコードが多くなりすぎる。それを避けるため、共通部分だけを定義したDB Contextを作り、そこからさらに派生させたDBContextを作成するという方針を試してみたらしい。
しかし、これはうまく機能しなかった。派生クラスでは、DbContextOptions<派生クラスの型>のコンストラクタが必要になるが、派生クラスのコンストラクタから呼ばれる親クラスのコンストラクタにDbContextOptions<派生クラスの型>を受け付けるパラメータがないためらしい。
下記の例でいうと、public SubDbContext (DbContextOptions options)では、DbContextOptionsが引数で渡されてきて、base(DbContextOptions)で親クラスのコンストラクタを呼び出すが、親クラスでは、DbContextOptions options)を受け付けようと待ち構えているため、うまくいかない…ということ。

public class MainDbContext : DbContext
{
    public MainDbContext(DbContextOptions<MainDbContext> options)
        : base(options){}
    //共通のテーブル定義など
}

public class SubDbContext : MainDbContext
{
    public SubDbContext (DbContextOptions<SubDbContext> options)
        : base(options){}
    //システム毎のテーブル定義など
}

解決策

Case1と同じ解決策か・・・と思ったが、解決方法としては、これが提示されていた。下記に例を示したが、DbContextOptionsを持つコンストラクタのアクセス修飾子をProtectedに変更しているところがポイントとなる。ジェネリックではないDbContextOptionsを受け付けるコンストラクタをProtectedすることで、DI実行時にこのコンストラクタを使わせないようにすることができるとのこと。
翻訳を間違えているのか、DIの仕様を理解していないのかわからないが、この説明ではまだ腹落ちしない…

public class MainDbContext : DbContext
{
    public MainDbContext(DbContextOptions<MainDbContext> options)
        : base(options){}

    protected MainDbContext(DbContextOptions options)
        : base(options){}
}

public class SubDbContext : MainDbContext
{
    public SubDbContext (DbContextOptions<SubDbContext> options)
        : base(options){}
}

  //StartUp.csでの設定
public void ConfigureServices(IServiceCollection services)
{
  services.AddDbContext<SubDbContext>(options => options.UseNpgsql(Configuration.GetConnectionString("db1-connection")));
}

まとめ

いろいろなケースがあるような気がしてネットサーフィンを始めたが、大きく2ケースしかなかった。 Case:2のリンクをたどっていて以下の結論にたどり着いた。結果的に、Case:2の方法が公式的にも推奨されている方法だった。Case:1はその解決方法で解決したのだろうか・・・ DbContextOptions と DbContextOptions)