【问题标题】:Can I use a separate query plan cache per session?我可以为每个会话使用单独的查询计划缓存吗?
【发布时间】:2018-08-23 21:00:59
【问题描述】:

我有一个多租户 ASP.NET 应用程序,我们的数据库设置了软删除。最初,我们直接在查询级别处理数据的限制,例如:

var foos = context.Foos.Where(foo => !foo.Deleted && foo.TenantId = currentTenantId).ToList();

正如您可以想象的那样,这会使我们的数据访问层中的所有查询膨胀,并且如果忘记添加正确的过滤条件,会使 API 非常容易受到攻击。我们决定使用Z.EntityFramework.Plus.EF6 对上下文应用全局过滤:

public class FooDataContextFactory
{
    public FooDataContext CreateContext()
    {
        var context = new FooDataContext();

        context.Filter<Foo>(collection => collection.Where(foo=> !foo.Deleted));

        var principal = Thread.CurrentPrincipal as ClaimsPrincipal;
        if (principal.HasClaim(claim => claim.Type == "TenantId"))
        {
            var currentTenantId = int.Parse(principal.FindFirst("TenantId").Value);
            context.Filter<Foo>(collection => collection.Where(foo => foo.TenantId == currentTenantId));
        }

        return context;
    }
}

这非常适合单个用户。但是,当您切换租户时,我们会遇到将过滤器表达式保存在 the query plan cache 中的问题。这是带有 Entity Framework Plus 的known issue,由于它似乎没有得到解决,我需要找到一种解决方法。

我能想到的最直接的解决方案是将查询计划缓存的生命周期与当前会话相关联,当用户注销或切换租户时,缓存被销毁。这可能吗?如果可以,我该如何实现?

【问题讨论】:

  • 怀疑这是可能的。
  • 按照您的建议,only 方法可以做到这一点......我的意思是有很多更好的方法可以做到这一点而不必担心缓存......就是创建您的上下文在另一个应用程序域中。 EF QueryCache 存储在 AppDomain 级别,因此丢弃 AppDomain 会丢弃缓存。话虽如此,由于查询计划被缓存,您遇到了什么实际问题?
  • 我认为这个问题很清楚地说明了它 - 查询计划被缓存了,它包括类似于 WHERE TenantId = 3 的内容。该值未参数化,因此当我更改租户时,查询不会返回正确的结果
  • Jonathan 在链接问题中很好地总结了这个问题 - “真正的问题是计划在调用拦截器之前与表达式一起缓存。这意味着两个上下文具有不同的过滤器但具有相同的查询将使用相同的执行计划,这是非常糟糕的,因为不需要应用相同的过滤器。”

标签: c# asp.net-mvc-4 entity-framework-6 entity-framework-plus


【解决方案1】:

我遇到了同样的问题,并尝试使用 Z.EntityFramework.Plus.EF6 解决同样的问题。我发现 zzzprojects 团队也有EntityFramework.DynamicFilters,它在这个目的上效果更好。缓存的查询是参数化的,并在运行时使用您提供的选择器函数注入值。

using System.Data.Entity;
using EntityFramework.DynamicFilters;

public class Program
{   
    public class CustomContext : DbContext
    {
        private int _tenantId;

        public int GetTenantId()
        {
            return _tenantId;
        }

        // Call this function to set the tenant once authentication is complete.
        // Alternatively, you could pass tenantId in when constructing CustomContext if you already know it
        // or pass in a function that returns the tenant to the constructor and call it here.
        public void SetTenantId(int tenantId)
        {
            _tenantId = tenantId;
        }

        protected override void OnModelCreating(DbModelBuilder modelBuilder)
        {
            // Filter applies to any model that implements ITenantRestrictedObject
            modelBuilder.Filter(
                "TenantFilter",
                (ITenantRestrictedObject t, int tenantId) => t.TenantId == tenantId,
                (CustomContext ctx) => ctx.GetTenantId(), // Might could replace this with a property accessor... I haven't tried it
                opt => opt.ApplyToChildProperties(false)
            );
        }
    }

    public interface ITenantRestrictedObject
    {
        int TenantId { get; }
    }
}

【讨论】:

  • 我已将此视为另一种可能的解决方法。不幸的是,我们所做的一些过滤也是基于权限的,这些权限是从数据库中的表中读取的,但是您无法在 OnModelCreating 方法中从数据库中读取。这不是一个理想的过程 - 权限应该存储在访问令牌中,而不是每次调用端点时都从数据库中检索 - 但这是我们所拥有的,我需要让它工作。
  • 此解决方案也适用于权限。您可以在构造 CustomContext 之前从权限中读取一次,并将权限作为参数传入,或者调用像 SetTenantId 这样的函数来接受您的权限并将它们存储为变量。设置后,您可以从该权限对象中读取,例如(CustomContext ctx) =&gt; ctx.GetPermissions().Any({predicate for this object type})。您还可以按名称有条件地启用/禁用动态过滤器,因此您可以在加载启用/禁用过滤器的权限后调用一个函数。
猜你喜欢
  • 2016-02-23
  • 1970-01-01
  • 1970-01-01
  • 2017-03-04
  • 2019-01-23
  • 2019-03-28
  • 1970-01-01
  • 2021-10-04
  • 1970-01-01
相关资源
最近更新 更多