我正在构建一个多租户应用程序,并且遇到了我认为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,因为我只想尽可能地利用我的基本配置。
答案 0 :(得分:7)
目前(从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在评论中指出的那样,v2.0.1删除了大部分限制 - 现在您可以使用方法和子属性。
但是,它引入了另一个要求 - 动态表达式必须 root 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;
}
TenantEntityConfigurationBase&lt; TEntity,TKey&gt;类:
builder.HasQueryFilter(Context.CreateFilter<TEntity, TKey>());
答案 1 :(得分:0)
回答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);
}
}
答案 2 :(得分:0)
更新:不幸的是,这无法按预期进行... 我查看了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中,以免被“缓存”。