EF Core 2.0.0 Query Filter is Caching TenantId (Updated for 2.0.1+)
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:
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();
}
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);
}
}
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.
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.
However, it introduced another requirement - the dynamic expression must be rooted at the 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.
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.
Here are the modifications:
TenantDbContext class:
internal Expression<Func<TEntity, bool>> CreateFilter<TEntity, TKey>()
where TEntity : TenantEntityBase<TKey>
where TKey : IEquatable<TKey>
{
return e => e.TenantId == TenantId;
}
TenantEntityConfigurationBase<TEntity, TKey> class:
builder.HasQueryFilter(Context.CreateFilter<TEntity, TKey>());
Answer for 2.0.1+
So, the day I got it work, EF Core 2.0.1 was released. As soon as I updated, this solution came crashing down. After a very long thread over here, it turned out that it was really a fluke that it was working in 2.0.0.
Officially for 2.0.1 and beyond any query filters that depend on an outside value, like the tenant id in my case, must be defined in the OnModelCreating
method and must reference a property on the DbContext
. The reason is because on first run of the app or first call into EF all EntityTypeConfiguration
classes are processed and their results are cached regardless of how many times the DbContext
is instanced.
That's why defining the query filters in the OnModelCreating
method works because it's a fresh instance and the filter lives and dies with it.
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);
}
}