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

Posted

技术标签:

【中文标题】EF Core 2.0.0 查询过滤器正在缓存 TenantId(针对 2.0.1+ 更新)【英文标题】:EF Core 2.0.0 Query Filter is Caching TenantId (Updated for 2.0.1+) 【发布时间】:2017-11-13 15:39:10 【问题描述】:

我正在构建一个多租户应用程序,并且在我认为 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();
    

...注入到 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);
    

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

我已验证用户主体返回的租户 ID 会根据登录的租户用户而变化,所以这不是问题。提前感谢您的帮助!

更新

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

更新 2

还可以查看 Ivan 对 2.0.1+ 的更新,它代理了 DbContext 的过滤器表达式,它恢复了在基本配置类中定义它的能力。两种解决方案都有其优点和缺点。我再次选择了 Ivan's,因为我只想尽可能地利用我的基本配置。

【问题讨论】:

为什么IHttpContextAccessor 不能是作用域/瞬态的?可能值得显示您的依赖注入配置的相关元素。 @CalC:因为它是一个单例(基本上是一种工厂)并且调用它的HttpContext属性检索当前请求的http上下文 TenantProvider 是作用域还是单例?你的 DbContext 实例也是单例吗?对我来说,似乎有些东西在请求中幸存下来,通常暗示您的 IoC 存在生命周期问题 @Tseng TenantProvider 和 DbContext 都是作用域实例。 Ivan 的回答解决了这个问题,因为这正是 EF 的工作方式。 【参考方案1】:

目前(从 EF Core 2.0.0 开始)动态全局查询过滤非常有限。它在动态部分由目标DbContext 派生类(或其基类DbContext 派生类之一)的直接属性 提供。与文档中的 Model-level query filters 示例完全相同。正是这样——没有方法调用,没有嵌套的属性访问器——只是上下文的属性。这在链接中有所解释:

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

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

public abstract class TenantDbContext : DbContext

    protected ITenantProvider TenantProvider;
    internal int TenantId => TenantProvider.GetId();

从中派生您的上下文类,并以某种方式将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);
    

一切都会按预期进行。请记住,Context 变量类型必须是 DbContext 派生的 - 用 interface 替换它是行不通的。

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

但是,它引入了另一个要求 - 动态表达式必须 植根 DbContext

这个要求打破了上面的解决方案,因为表达式根是TenantEntityConfigurationBase&lt;TEntity, TKey&gt;类,并且由于缺乏生成常量表达式的编译时支持,在DbContext之外创建这样的表达式并不容易。

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

以下是修改:

TenantDbContext 类

internal Expression<Func<TEntity, bool>> CreateFilter<TEntity, TKey>()
    where TEntity : TenantEntityBase<TKey>
    where TKey : IEquatable<TKey>

    return e => e.TenantId == TenantId;

TenantEntityConfigurationBase

builder.HasQueryFilter(Context.CreateFilter<TEntity, TKey>());

【讨论】:

这可能会有所帮助github.com/aspnet/EntityFrameworkCore/releases/tag/2.0.1至于限制,其中一些在与发布相关的博文中有所提及。但这些通常是主要限制,导致我们延迟实施。虽然这也是主要限制,但它无意中结束了。因此,直到人们开始使用它,我们才知道它是一个限制。但是一旦我们知道了,我们就会改进它。 此外,通常不应该在次要版本中删除限制。 (它的功能,不应该是补丁的一部分)。我们在修复另一个 bug 时不小心搞砸了。 @MU 1. 它是代码优先方法 2. EntityConfigurationBase 是 OP 的某个类。你可以简单地忽略它。此外,没有必要使用单独的类进行配置 - 您可以简单地将所有配置代码放在 OnModelCreating 中。要为每个 ITenantEntity 设置过滤器,您可以使用与其他问题类似的循环。但是过滤器应该使用上下文的TenantId 属性,否则它不会是动态的。与docs.microsoft.com/en-us/ef/core/what-is-new/… 中的示例类似,但对于每个实现ITenantEntity 的实体。 @MU 在你的数据库上下文中创建一个通用实例方法:private void SetTenantFilter&lt;TEntity&gt;(ModelBuilder modelBuilder) where TEntity : class, ITenantEntity modelBuilder.Entity&lt;TEntity&gt;().HasQueryFilter(e =&gt; e.TenantId == this._TenantId); 然后在循环内使用反射调用它:foreach (var type in modelBuilder.Model.GetEntityTypes()) if (typeof(ITenantEntity).IsAssignableFrom(type.ClrType)) GetType().GetMethod("SetTenantFilter", BindingFlags.Instance | BindingFlags.NonPublic).MakeGenericMethod(type.ClrType).Invoke(this, new[] modelBuilder ); @MU 1. 不幸的是,这方面的内容不多,所以这是基于我的个人实验。我在这里展示了为多个实体设置查询过滤器的不同方法EF Core: Soft delete with shadow properties and query filters。 2. 我对那个方向一无所知,可能与 EF6 中使用的技术相同,例如覆盖 SaveChanges 和检查 ChangeTracker.Entries()【参考方案2】:

2.0.1+的答案

所以,在我开始工作的那天,EF Core 2.0.1 发布了。我一更新,这个解决方案就崩溃了。在here 上进行了很长时间的讨论后,结果发现它在 2.0.0 中运行确实是侥幸。

对于 2.0.1 及更高版本,任何依赖于外部值的查询过滤器(例如我的情况下的租户 ID)必须在 OnModelCreating 方法中定义并且必须引用一个 属性DbContext。原因是在应用程序首次运行或首次调用 EF 时,所有 EntityTypeConfiguration 类都会被处理,并且无论 DbContext 被实例化多少次,它们的结果都会被缓存。

这就是在 OnModelCreating 方法中定义查询过滤器有效的原因,因为它是一个新实例,过滤器随它而生。

public class MyDbContext : DbContext 
    private readonly ITenantService _tenantService;

    private int TenantId => TenantService.GetId();

    public DbSet<User> Users  get; set; 

    public MyDbContext(
        DbContextOptions options,
        ITenantService tenantService) 
        _tenantService = tenantService;
    

    protected override void OnModelCreating(
        ModelBuilder modelBuilder) 
        modelBuilder.Entity<User>().HasQueryFilter(
            u => u.TenantId == TenantId);
    

【讨论】:

它与缓存和上下文实例无关。这是纯粹的表达式树相关问题 - 请参阅我的更新。【参考方案3】:

更新:不幸的是,这不会按预期工作...... 我查看了 SQL 日志,未评估 lambda 表达式中的函数,这将导致返回完整的结果集,然后在客户端进行过滤。

我使用以下模式能够在外部添加过滤器,而无需上下文本身的属性。

    public class QueryFilters
    
        internal static IDictionary<Type, List<LambdaExpression>> Filters  get; set;  = new Dictionary<Type, List<LambdaExpression>>();

        public static void RegisterQueryFilter<T>(Expression<Func<T, bool>> expression)
        
            List<LambdaExpression> list = null;
            if (Filters.TryGetValue(typeof(T), out list) == false)
            
                list = new List<LambdaExpression>();
                Filters.Add(typeof(T), list);
            

            list.Add(expression);
        
    

在我的上下文中,我像这样添加查询过滤器:

    public class MyDbContext : DbContext
    
        protected override void OnModelCreating(ModelBuilder modelBuilder)
        
            base.OnModelCreating(modelBuilder);

            foreach (var type in QueryFilters.Filters.Keys)
                foreach (var filter in QueryFilters.Filters[type])
                    modelBuilder.Entity(type).HasQueryFilter(filter);
        
    

我在其他地方(即在某些配置代码中)注册我的查询过滤器,如下所示:

    Func<User, bool> func = i => IncludeSoftDeletedEntities.DisableFilter;
    QueryFilters.RegisterQueryFilter<User>(i => func(i) || EF.Property<bool>(i, "IsDeleted") == false);

在此示例中,我添加了一个软删除过滤器,可以使用“全局”IncludeSoftDeletedEntities.DisableFilter(实际上由范围机制提供支持)禁用该过滤器。

这里的问题是 EF.Property 不能在实际表达式之外使用,所以它需要在它所在的位置。 还有一点要提的是,我们需要将任何逻辑封装在一个 Func 中,以避免它被“缓存”。

【讨论】:

以上是关于EF Core 2.0.0 查询过滤器正在缓存 TenantId(针对 2.0.1+ 更新)的主要内容,如果未能解决你的问题,请参考以下文章

EF Core 全局查询过滤器复杂表达式

EF Core 多对多配置

过滤包含在 EF Core 中

讨论过后而引发对EF 6.x和EF Core查询缓存的思考

讨论过后而引发对EF 6.x和EF Core查询缓存的思考

EF Core 中实现 动态数据过滤器