EF Core 2.0.0 Query Filter - This is TenantId caching (updated for 2.0.1+)

I am building a multi-tenant application and am having difficulty in what I think is the EF Core caching the tenant ID for all requests. The only thing that seems to help is to constantly repair the application when I sign up and exit the tenants.

I thought that this could be due to the fact that the IHttpContextAccessor instance is single, but it cannot be limited, and when I enter and exit without restructuring, I see a tenant name change at the top of the page, so this is not a problem.

The only thing I can think of is that EF Core does some kind of query caching. Iโ€™m not sure why it will consider this to be an instance with a scope, and it should be rebuilt on every request, if Iโ€™m not mistaken, and I probably am. I was hoping it would behave like an instance with a scope, so I could just enter the tenant ID during model build on each instance.

I would really appreciate it if someone could point me in the right direction. Here is 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(); } } 

... which is introduced in TenantEntityConfigurationBase.cs , where I use it to configure 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 entity configurations. Unfortunately, it does not work, as I planned.

I have verified that the tenant ID returned by the user-user changes depending on which tenant user is registered, so this is not a problem. Thanks in advance for your help!

Update

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

Update 2

Also look at the Ivan update for version 2.0.1+, it proxies in the filter expression from DbContext, which restores the ability to define it once in the base configuration class. Both solutions have their pros and cons. I chose Ivan again, because I just want to make the most of the basic configurations.

+6
source share
3 answers

Currently (starting with EF Core 2.0.0) dynamic global query filtering is quite limited. This only works if the dynamic part is provided by the direct property of the target derived class DbContext (or one of its base derived classes DbContext ). In the same way as in the example of query filters at the model level from the documentation. Exactly this way - without method calls, without nested methods for accessing properties - only a context property. This is somehow explained in the link:

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

For this to work in your scenario, you must create a base class similar to the following:

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

extracts your context class from it and somehow injects an instance of TenantProvider into it. Then change the TenantEntityConfigurationBase class to get 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 type of the Context variable must be a derived DbContext class - replacing it with an interface will not work.

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

However, another requirement has been introduced - a dynamic expression must be based on DbContext .

This requirement violates the above solution, because the root of the expression is the TenantEntityConfigurationBase<TEntity, TKey> , and creating such an expression outside of DbContext not so simple due to the lack of compile-time support for generating constant expressions.

This problem can be solved with the help of some low-level expression manipulation methods, but in your case it would be easier to move the filter creation to the generic TenantDbContext instance 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 & lt; TEntity, TKey> class:

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

Answer for 2.0.1 +

So, the day I received it, EF Core 2.0.1 was released. As soon as I updated, this solution crashed. After a very long stream , it turned out that it was really an accident that it worked in 2.0.0.

Officially for 2.0.1 and outside of any query filters that depend on an external value, for example, the tenant ID in my case, must be defined in the OnModelCreating method and must refer to the property on DbContext . The reason is that the first time you launch the application or the first call to EF, all EntityTypeConfiguration classes EntityTypeConfiguration processed and their results are cached regardless of how many times DbContext displayed.

This is why defining query filters in the OnModelCreating method works because the new instance and filter live and die 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); } } 
0
source

Update: unfortunately, this will not work as expected ... I looked at the SQL log and the function in the lambda expression is not evaluated, which will cause the full set of results to be returned and then filtered on the client side.

I use the following template to be able to add external filters without having properties in the context itself.

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

And in my context, I add query filters like this:

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

And I register my query filters somewhere else (for example, in some configuration code) as follows:

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

In this example, I add a soft deletion filter that can be disabled using the "global" IncludeSoftDeletedEntities.DisableFilter (which actually works using the scope mechanism).

The hints are that EF.Property cannot be used outside the actual expression, so it should be where it is. Another thing worth mentioning is that we need to encapsulate any logic in Func to avoid caching it.

0
source

Source: https://habr.com/ru/post/1269820/


All Articles