EF Core 2.0.0 查询过滤器正在缓存 TenantId(针对 2.0.1+ 更新) [英] EF Core 2.0.0 Query Filter is Caching TenantId (Updated for 2.0.1+)

查看:22
本文介绍了EF Core 2.0.0 查询过滤器正在缓存 TenantId(针对 2.0.1+ 更新)的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

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

I'm building a multi-tenant application, and am running into difficulties with what I think is EF Core caching the tenant id across requests. The only thing that seems to help is constantly rebuilding the application as I sign in and out of tenants.

我认为这可能与 IHttpContextAccessor 实例是单例有关,但它无法确定范围,并且当我在不重建的情况下登录和退出时,我可以看到租户的名称更改在页面顶部,所以这不是问题.

I thought it may have something to do with the IHttpContextAccessor instance being a singleton, but it can't be scoped, and when I sign in and out without rebuilding I can see the tenant's name change at the top of the page, so it's not the issue.

我唯一能想到的另一件事是 EF Core 正在执行某种查询缓存.我不知道为什么它会考虑它是一个作用域实例并且它应该在每个请求上重建,除非我错了,我可能是错的.我希望它表现得像一个作用域实例,这样我就可以在模型构建时在每个实例上简单地注入租户 ID.

The only other thing I can think of is that EF Core is doing some sort of query caching. I'm not sure why it would be considering that it's a scoped instance and it should be getting rebuild on every request, unless I'm wrong, which I probably am. I was hoping it would behave like a scoped instance so I could simply inject the tenant id at model build time on each instance.

如果有人能指出我正确的方向,我将不胜感激.这是我当前的代码:

I'd really appreciate it if someone could point me in the right direction. Here's my current code:

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 中,我用它来设置全局查询过滤器.

...which is injected into TenantEntityConfigurationBase.cs where I use it to setup a global query filter.

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);
    }
}

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

...which is then inherited by all other tenant entity configurations. Unfortunately it doesn't seem to work as I had planned.

我已经验证用户主体返回的租户 ID 会根据登录的租户用户而改变,所以这不是问题.在此先感谢您的帮助!

I have verified that the tenant id being returned by the user principal is changing depending on what tenant user is logged in, so that's not the issue. Thanks in advance for any help!

更新

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

For a solution when using EF Core 2.0.1+, look at the not-accepted answer from me.

更新 2

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

Also look at Ivan's update for 2.0.1+, it proxies in the filter expression from the DbContext which restores the ability to define it once in a base configuration class. Both solutions have their pros and cons. I've opted for Ivan's again because I just want to leverage my base configurations as much as possible.

推荐答案

目前(从 EF Core 2.0.0 开始)动态全局查询过滤非常有限.它在动态部分由目标DbContext 派生类(或其基DbContext 派生类).与模型级查询过滤器完全一样strong> 文档中的示例.正是这样——没有方法调用,没有嵌套的属性访问器——只是上下文的属性.链接中对此进行了解释:

Currently (as of EF Core 2.0.0) the dynamic global query filtering is quite limited. It works only if the dynamic part is provided by direct property of the target DbContext derived class (or one of its base DbContext derived classes). Exactly as in the Model-level query filters example from the documentation. Exactly that way - no method calls, no nested property accessors - just property of the context. It's sort of explained in the link:

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

Note the use of a DbContext instance level property: TenantId. Model-level filters will use the value from the correct context instance. i.e. the one that is executing the query.

为了让它在你的场景中工作,你必须像这样创建一个基类:

To make it work in your scenario, you have to create a base class like this:

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

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

derive your context class from it and somehow inject the TenantProvider instance into it. Then modify the TenantEntityConfigurationBase class to receive 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 派生的 class - 将其替换为 interface 将不起作用.

and everything will work as expected. And remember, the Context variable type must be a DbContext derived class - replacing it with interface won't work.

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

Update for 2.0.1: As @Smit pointed out in the comments, v2.0.1 removed most of the limitations - now you can use methods and sub properties.

然而,它引入了另一个要求——动态表达式必须根植DbContext.

However, it introduced another requirement - the dynamic expression must be rooted at the DbContext.

这个要求打破了上面的解决方案,因为表达式根是 TenantEntityConfigurationBase 类,并且在 DbContext 之外创建这样的表达式并不容易,因为缺少生成常量表达式的编译时支持.

This requirement breaks the above solution, since the expression root is TenantEntityConfigurationBase<TEntity, TKey> class, and it's not so easy to create such expression outside the DbContext due to lack of compile time support for generating constant expressions.

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

It could be solved with some low level expression manipulation methods, but the easier in your case would be to move the filter creation in generic instance method of the TenantDbContext and call it from the entity configuration class.

修改如下:

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>());

这篇关于EF Core 2.0.0 查询过滤器正在缓存 TenantId(针对 2.0.1+ 更新)的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆