EF Core 2.0.0查询过滤器正在缓存TenantId(针对2.0.1+进行了更新)

Gup*_*R4c 7 c# caching multi-tenant entity-framework-core asp.net-core

我正在构建一个多租户应用程序,并且遇到了我认为EF Core在请求中缓存租户ID的困难.唯一似乎有帮助的是在我登录和退出租户时不断重建应用程序.

我认为它可能与IHttpContextAccessor实例是单例有关,但它不能作为范围,当我在没有重建的情况下登录和退出时,我可以看到租户的名称在页面顶部发生变化,所以它不是问题.

我能想到的另一件事是EF Core正在进行某种查询缓存.我不确定为什么它会考虑它是一个范围的实例,它应该在每个请求上重建,除非我错了,我可能是.我希望它的行为类似于作用域实例,因此我可以在模型构建时在每个实例上简单地注入租户ID.

如果有人能指出我正确的方向,我真的很感激.这是我目前的代码:

TenantProvider.cs

public sealed class TenantProvider :
    ITenantProvider {
    private readonly IHttpContextAccessor _accessor;

    public TenantProvider(
        IHttpContextAccessor accessor) {
        _accessor = accessor;
    }

    public int GetId() {
        return _accessor.HttpContext.User.GetTenantId();
    }
}
Run Code Online (Sandbox Code Playgroud)

...注入TenantEntityConfigurationBase.cs,我用它来设置全局查询过滤器.

internal abstract class TenantEntityConfigurationBase<TEntity, TKey> :
    EntityConfigurationBase<TEntity, TKey>
    where TEntity : TenantEntityBase<TKey>
    where TKey : IEquatable<TKey> {
    protected readonly ITenantProvider TenantProvider;

    protected TenantEntityConfigurationBase(
        string table,
        string schema,
        ITenantProvider tenantProvider) :
        base(table, schema) {
        TenantProvider = tenantProvider;
    }

    protected override void ConfigureFilters(
        EntityTypeBuilder<TEntity> builder) {
        base.ConfigureFilters(builder);

        builder.HasQueryFilter(
            e => e.TenantId == TenantProvider.GetId());
    }

    protected override void ConfigureRelationships(
        EntityTypeBuilder<TEntity> builder) {
        base.ConfigureRelationships(builder);

        builder.HasOne(
            t => t.Tenant).WithMany().HasForeignKey(
            k => k.TenantId);
    }
}
Run Code Online (Sandbox Code Playgroud)

...然后由所有其他租户实体配置继承.不幸的是,它似乎没有按照我的计划运作.

我已经验证了用户主体返回的租户ID正在根据租户用户的登录情况而变化,因此这不是问题.在此先感谢您的帮助!

更新

有关使用EF Core 2.0.1+的解决方案,请查看我未接受的答案.

更新2

另请参阅Ivan的2.0.1+更新,它代表DbContext的过滤器表达式,它恢复了在基本配置类中定义一次的能力.这两种解决方案各有利弊.我再次选择了Ivan,因为我只想尽可能地利用我的基本配置.

Iva*_*oev 9

目前(从EF Core 2.0.0开始),动态全局查询过滤非常有限.当动态部分由目标 派生类(或其基本派生类之一)的直接属性提供时,它才有效.正如在文档中的模型级查询过滤器示例中一样.正是这样 - 没有方法调用,没有嵌套属性访问器 - 只是上下文的属性.有点在链接中解释:DbContextDbContext

请注意DbContext实例级属性的使用:TenantId.模型级过滤器将使用来自正确上下文实例的值.即执行查询的那个.

要使它在您的场景中工作,您必须创建一个这样的基类:

public abstract class TenantDbContext : DbContext
{
    protected ITenantProvider TenantProvider;
    internal int TenantId => TenantProvider.GetId();
}
Run Code Online (Sandbox Code Playgroud)

从它派生你的上下文类,并以某种方式将TenantProvider实例注入其中.然后修改TenantEntityConfigurationBase类以接收TenantDbContext:

internal abstract class TenantEntityConfigurationBase<TEntity, TKey> :
    EntityConfigurationBase<TEntity, TKey>
    where TEntity : TenantEntityBase<TKey>
    where TKey : IEquatable<TKey> {
    protected readonly TenantDbContext Context;

    protected TenantEntityConfigurationBase(
        string table,
        string schema,
        TenantDbContext context) :
        base(table, schema) {
        Context = context;
    }

    protected override void ConfigureFilters(
        EntityTypeBuilder<TEntity> builder) {
        base.ConfigureFilters(builder);

        builder.HasQueryFilter(
            e => e.TenantId == Context.TenantId);
    }

    protected override void ConfigureRelationships(
        EntityTypeBuilder<TEntity> builder) {
        base.ConfigureRelationships(builder);

        builder.HasOne(
            t => t.Tenant).WithMany().HasForeignKey(
            k => k.TenantId);
    }
}
Run Code Online (Sandbox Code Playgroud)

一切都会按预期工作.请记住,Context变量类型必须是DbContext派生 - 用接口替换它将不起作用.

2.0.1的更新:正如@Smit在评论中指出的那样,v2.0.1删除了大部分限制 - 现在你可以使用方法和子属性了.

但是,它提出了另一个要求-动态表达必须扎根DbContext.

这个要求打破了上述解决方案,因为表达式root是TenantEntityConfigurationBase<TEntity, TKey>类,并且DbContext由于缺乏编译时间支持来生成常量表达式,因此在外部创建这样的表达式并不容易.

它可以通过一些低级表达式操作方法来解决,但在您的情况下更容易将过滤器创建移动到通用实例方法中TenantDbContext并从实体配置类中调用它.

以下是修改:

TenantDbContext类:

internal Expression<Func<TEntity, bool>> CreateFilter<TEntity, TKey>()
    where TEntity : TenantEntityBase<TKey>
    where TKey : IEquatable<TKey>
{
    return e => e.TenantId == TenantId;
}
Run Code Online (Sandbox Code Playgroud)

TenantEntityConfigurationBase <TEntity,TKey>类:

builder.HasQueryFilter(Context.CreateFilter<TEntity, TKey>());
Run Code Online (Sandbox Code Playgroud)