web-dev-qa-db-ja.com

EFコアとグローバルクエリフィルターで循環参照を回避する方法

マルチテナントアプリケーションの構築に取り組んでおり、ベースデータプロバイダーとしてEntity Framework Coreを使用しています。 EF Coreでは、 グローバルクエリフィルター を使用して、EFによって生成されるすべてのクエリに適用されるカスタムフィルターを定義できます。

これらのフィルターは適切に機能し、ほとんどの目的を果たしているように見えますが、アプリケーションにマルチテナントフィルターを追加しようとすると、問題が発生し始めます。懸念事項をより適切に分離するために、テナントの識別子とハンドルとして機能するさまざまなクラスを作成しました。ただし、これらのセキュリティクラスはデータコンテキスト自体に依存しているため、プロジェクト全体が機能しなくなるため、テナント識別/セキュリティクラスを使用しようとすると、問題が発生し始めます。

アプリケーションの簡略化されたモデルは、次のようになります。

私たちのアプリケーションプロジェクトでは、次のことがわかります。

public class TenantService : ITenantService
{
    private IHttpContextAccessor _accessor;
    private ITenantIdentifier _identifier;
    private ITenantSecurityService _security;

    public TenantService(IHttpContextAccessor accessor,
                         ITenantIdentifier identifier,
                         ITenantSecurityService security) 
    {
        _accessor = accessor;
        _identifier = identifier;
        _security = security;
    }

    public Guid GetCurrentTenant() {
        var tenant = _identifier.GetTenant(_accessor.HttpContext);
        if (!_security.CanAccess(tenant))
            throw new TenantAuthorizationException("User cannot access tenant", tenant);

        return tenant.TenantId;
    }
}

public class TenantSecurityService : ITenantSecurityService
{
    private readonly IDbContext _dbContext;
    private readonly ICurrentUserService _userService
    public TenantSecurityService(IDbContext dbContext,
                                 ICurrentUserService userService)
    {
        _dbContext = dbContext;     
        _userService = userService;
    }

    public bool CanAccess(Tenant tenant) 
    {
        var currentUser = _userService.GetCurrentUser();
        // do various checks with dbContext data...

        return result;
    }
}

そして、私たちの永続化プロジェクトでは、次のようなものが表示されます。

public class ApplicationDbContext : DbContext, IDbContext
{
    private readonly ITenantService _tenantService;
    public ApplicationDbContext(ITenantService tenantService) 
    {
        _tenantService = tenantService;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        ConfigureTenantFilter(modelBuilder);
    }

    private void ConfigureTenantFilter(ModelBuilder modelBuilder)
    {
        var tenant = _tenantService?.GetCurrentTenant();
        foreach (var entity in modelBuilder.Model.GetEntityTypes().Where(x =>
            typeof(ITenantEntity).IsAssignableFrom(x.ClrType)))
        {
            entity.AddProperty(nameof(Tenant), typeof(Guid));
            if (!string.IsNullOrEmpty(tenant))
                modelBuilder
                    .Entity(entity.ClrType)
                    .HasQueryFilter(IsTenantRestriction(entity.ClrType, tenant));
        }
    }
}

お気づきかもしれませんが、上記のコードは循環依存関係のため実際には実行に失敗しますが、アプリケーションを構造化するための2つの個別のdbContexts(少しハックらしい)以外の方法を見つけることができないようです。

セキュリティをdbContextに密結合する必要なしに、これらのクエリフィルターを処理するためのより良いアプローチは何でしょうか?

1
JD Davis

質問には言及されていませんが、実際にはここに2つの循環問題があります:循環プロジェクト参照(私があなたが尋ねている問題だと私は推測します) )と循環依存グラフ(まだ気付いていないようです)。


循環プロジェクト参照

質問から、私はあなたのプロジェクト構造が次のようであると思います:

enter image description here

注意すべき重要な点の1つは、TenantSecurityServiceと永続化レイヤーの間で循環依存関係があることです(ITenantSecurityServiceと永続化レイヤーの間ではないnot(インターフェイスがデータ層に依存する実装はありません)。

ここでの秘訣は、コントラクトを実装から分離することです。私はしばしば契約ライブラリを特別に作成するので、そこにすべての(DI)インターフェイスを配置できます。すべてのプロジェクトがコントラクトライブラリを参照します。これにより、現在直面しているような「循環」依存関係レイヤーを持つ依存関係グラフを作成できます。

本質的には、プロジェクトの依存関係を次のように構成する必要があります。

enter image description here

修正トップレベルのプロジェクトも契約を参照する必要があります。作成した画像はすでに削除しているため修正できませんでした。

  • コントラクトにはすべてのDIインターフェイスが含まれています
  • 中間のプロジェクト(持続性、ビジネスロジック、その他すべて)は、DIインターフェースの具体的な実装を提供します
  • トップレベルのアプリケーションは、インターフェースを実装に接続します(DIコンテナーに登録することにより)。

中間のプロジェクトは互いに直接参照しないので、これらのプロジェクト間で論理的に循環参照することは決してありません。

これは基本的な例にすぎないことに注意してください。私が最も一般的に使用しているアーキテクチャには、複数のコントラクトライブラリがあります。特に、インターフェイスのヒープがある非常に大規模なアプリケーションでコントラクトライブラリが肥大化するのを防ぐためです。ただし、全体的なポイントは同じです。契約を分離します。

この設定を使用すると、パブリックインターフェイスがレイヤー固有のタイプを公開しないようにすることで、物事をきれいに保つことができます。たとえば、インターフェースが永続化プロジェクトに格納されている場合、インターフェースには永続化タイプ(MyDbContextなど)を返す(または取り込む)メソッドを含めることができますが、これは明らかな理由で行うべきではありません。

(他のプロジェクトに依存すると循環参照が作成されるため)独自のプロジェクトにコントラクトを持たせることにより、インターフェイスは、言語/フレームワークから一般的に知られている型、または明示的に作成したDTOのいずれかのみを使用するように強制されます契約ライブラリ内。


循環依存グラフ

これは常に、2つの概念(この場合はIDbContext)を組み合わせるという問題です。最終的に得られる循環グラフは次のとおりです。

_IDbContext (1)
  --> ITenantService
    --> ITenantSecurityService
      --> IDbContext (2)
        --> ITenantService
          --> ITenantSecurityService
            --> and so on...
_

注:明確にするために:これらの依存関係の少なくとも1つがシングルトンまたはスコープ付きの依存関係として登録されている場合、無限再帰例外を回避できます。しかし、ポイントはディペンデンシーグラフ自体が循環的であるということです。設定が機能し、この区別をする必要がない場合は、残りの回答をスキップできます。

IDbContext (1)にはセキュリティ層が必要ですか? はい。これは、アプリケーションの残りの部分がこのセキュリティレイヤーを通過する必要があるためです(この場合、マルチテナンシーセーフガードを実装するため)。

IDbContext (2)にはセキュリティ層が必要ですか? いいえ、なぜならそれはがセキュリティ層の一部だからです。この時点では、まだIDbContext (2)のマルチテナンシーフィルターを設定しているため、IDbContext (1)をテナントコンテキストにすることはできません。

Dbコンテキストには2つの異なる役割があり、それらを単一の概念として表すと、不要な循環依存関係に遭遇します。

2つの個別のdbContextがあることを除けば、アプリケーションを構造化するためのより良い方法を見つけることができないようです(これは少しハックに思えます)。

同意しません。個別のロールに個別のクラスを作成するよりも、単一のクラスで個別の類似した2つのロールを作成する方が「ハッキー」です。

完全に関連のない2つのdbコンテキストを作成する必要はあまりありませんが、同じコンテキストに基づいて2つのバリエーションを作成する必要があります。
状況を考慮して、私はこれらをITenantContext/TenantContext(つまりセキュリティフィルターを使用)およびIDbContext/ApplicationDbContext(非テナント固有)と呼ぶ傾向があります。

ITenantContextIDbContextから派生し、DIコンテナーがそれらを区別できるように2つのインターフェースを分離するマーカーインターフェースとして機能します。

_public interface ITenantContext : IDbContext { }
_

TenantContextは、ITenantServiceと依存関係があります(これとApplicationDbContextとの明確な違い)。

_public class TenantContext : ApplicationDbContext, ITenantContext
{
    // The same content as your ApplicationDbContext in the question
}
_

注:OnModelCreatingは、コンテキストの構築中に発生するため、難しいものです。一般的なシナリオでは、ここで合成より継承を使用することが可能です(ここで、TenantContextApplicationDbContextから派生していません)。ただし、OnModelCreatingの動作をオーバーライドしようとしているため、これははるかに複雑です。
作成された後、ApplicationDbContextにフィルターを設定する方法がある場合、ここでは、継承よりも構成を優先する必要があります。しかし、私は現在これを行う方法を考えることができません。

ApplicationDbContextは、本質的にはフィルタリングされていないテナントコンテキストです。つまり、ITenantService依存関係とカスタムOnModelCreatingが失われます。

_public class ApplicationDbContext : DbContext, IDbContext
{
    public ApplicationDbContext() { }
}
_

これにより、依存関係グラフの不要な再帰がクリーンアップされます。

_ITenantContext
  --> ITenantService
    --> ITenantSecurityService
      --> IDbContext
_

注:永続層以外の誰かがテナントされていないコンテキストにアクセスできないようにする場合は、ifを使用できます継承よりも構成。その場合、テナントされていないコンテキストをinternalクラスとして単純に保持できます。ただし、基本クラスはその派生クラスと同じ(最小)アクセサーを持つ必要があるため、悲しいことに現在の例では不可能です。派生クラスがそうでない場合、基本クラスを内部にすることはできません。
DIコンテナーにIDbContextを登録しないようにして、依存関係として使用できないようにすることはできますが、使用するクラスがIDbContextに依存しようとすると例外が発生します(許可されていません)。これは、ApplicationDbContextinternalである場合にコンパイル時エラーが発生するほどよくありません。

3
Flater