Efcore: Using HasQueryFilter with dynamic filter criteria value from IEntityTypeConfiguration<TEntity> implementation

Created on 19 Feb 2019  路  2Comments  路  Source: dotnet/efcore

When using HasQueryFilter from inside an IEntityTypeConfiguration implementation, the filter criteria will be eagerly evaluated and the value will be passed in directly to the query instead of creating an sql parameter.
However, when HasQueryFilter is invoked from the OnModelCreating method directly, it works as expected: the resulting query will have a parameter defined.

Steps to reproduce

Correct behavior can be seen by uncommenting the following line:
```c#
//Works as expected
modelBuilder.Entity().HasQueryFilter(_ => _.TenantId == _tenantIdProvider.TenandId);

Sample application:
```c#
using Microsoft.EntityFrameworkCore;
using Microsoft.EntityFrameworkCore.Metadata.Builders;
using Microsoft.Extensions.Logging;
using Microsoft.Extensions.Logging.Console;
using System.Linq;

namespace EfMultiTenant
{
    public class ApplicationDbContext : DbContext
    {
        private readonly TenantIdProvider _tenantIdProvider;

        public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options, TenantIdProvider tenantIdProvider)
            : base(options)
        {
            _tenantIdProvider = tenantIdProvider;
        }

        public DbSet<User> Users { get; set; }

        protected override void OnModelCreating(ModelBuilder modelBuilder)
        {
            base.OnModelCreating(modelBuilder);

            // Works as expected
            //modelBuilder.Entity<User>().HasQueryFilter(_ => _.TenantId == _tenantIdProvider.TenandId);

            // Does not work
            modelBuilder.ApplyConfiguration(new UserConfiguration(_tenantIdProvider));
        }
    }

    public class TenantIdProvider
    {
        public int TenandId { get; set; }
    }

    public class User
    {
        public int Id { get; set; }
        public string Name { get; set; }
        public int TenantId { get; set; }
    }

    public class UserConfiguration : IEntityTypeConfiguration<User>
    {
        private readonly TenantIdProvider _tenantIdProvider;

        public UserConfiguration(TenantIdProvider tenantIdProvider)
        {
            _tenantIdProvider = tenantIdProvider;
        }

        public void Configure(EntityTypeBuilder<User> builder)
        {
            builder.HasQueryFilter(_ => _.TenantId == _tenantIdProvider.TenandId);
        }
    }

    internal class Program
    {
        private static void Main(string[] args)
        {
            var optionsBuilder = new DbContextOptionsBuilder<ApplicationDbContext>();
            var loggerFactory = new LoggerFactory(new[] { new ConsoleLoggerProvider((_, __) => true, true) });

            optionsBuilder
                .UseSqlServer("Server=(localdb)\\mssqllocaldb;Database=EfMultiTenant;Trusted_Connection=True;MultipleActiveResultSets=true")
                .UseLoggerFactory(loggerFactory)
                .EnableSensitiveDataLogging();

            var tenantIdProvider = new TenantIdProvider();

            tenantIdProvider.TenandId = 1;
            using (var context = new ApplicationDbContext(optionsBuilder.Options, tenantIdProvider))
            {
                var allUsers = context.Users.ToList();
            }

            tenantIdProvider.TenandId = 2;
            using (var context = new ApplicationDbContext(optionsBuilder.Options, tenantIdProvider))
            {
                var allUsers = context.Users.ToList();
            }
        }
    }
}

Optimized query model in case of correct behavior:

dbug: Microsoft.EntityFrameworkCore.Query[10104]
      => Microsoft.EntityFrameworkCore.Query.RelationalQueryModelVisitor
      Optimized query model:
      'from User _ in DbSet<User>
      where [_].TenantId == __ef_filter__TenandId_0
      select [_]'

Optimized query model in case of incorrect behavior:

dbug: Microsoft.EntityFrameworkCore.Query[10104]
      => Microsoft.EntityFrameworkCore.Query.RelationalQueryModelVisitor
      Optimized query model:
      'from User _ in DbSet<User>
      where [_].TenantId == 1
      select [_]'

Further technical details

EF Core version: 2.2.2
Database Provider: Microsoft.EntityFrameworkCore.SqlServer
Operating system: Windows 10 Version 1809 (OS Build 17763.316)
IDE: Microsoft Visual Studio Professional 2017 Version 15.9.7

closed-by-design customer-reported

Most helpful comment

When you are using inside OnModelCreating, even though you are not explicitly writing, the variable is captured as context._tenantIdProvider.TenandId, at query execution time we can inject current context to it and evaluate the value to send parameter in SQL.

But the way you are passing _tenantIdProvider in EntityConfiguration's ctor, what we see in expression tree for that query filter is UserConfiguration._tenantIdProvider.TenandId. We do not have a way to know how this _tenantIdProvider's value is connected with DbContext._tenantIdProvider. So there is no way to update the value, hence we stick a constant. (parameterization happens only for things which can be changed in expression tree for better catching).

If you want to configure QueryFilter inside EntityConfiguration, then consider passing DbContext in ctor to your entity configuration. So that we can still inject current DbContext instance to calculate value.

All 2 comments

Another way to reproduce the issue is just by assigning the _tenantIdProvider field to a local variable:
```c#
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
base.OnModelCreating(modelBuilder);

// Works as expected
//modelBuilder.Entity<User>().HasQueryFilter(_ => _.TenantId == _tenantIdProvider.TenandId);

// Does not work
//modelBuilder.ApplyConfiguration(new UserConfiguration(_tenantIdProvider));

// Does not work either
var localTenantIdProvider = _tenantIdProvider;
modelBuilder.Entity<User>().HasQueryFilter(_ => _.TenantId == localTenantIdProvider.TenandId);

}
```

When you are using inside OnModelCreating, even though you are not explicitly writing, the variable is captured as context._tenantIdProvider.TenandId, at query execution time we can inject current context to it and evaluate the value to send parameter in SQL.

But the way you are passing _tenantIdProvider in EntityConfiguration's ctor, what we see in expression tree for that query filter is UserConfiguration._tenantIdProvider.TenandId. We do not have a way to know how this _tenantIdProvider's value is connected with DbContext._tenantIdProvider. So there is no way to update the value, hence we stick a constant. (parameterization happens only for things which can be changed in expression tree for better catching).

If you want to configure QueryFilter inside EntityConfiguration, then consider passing DbContext in ctor to your entity configuration. So that we can still inject current DbContext instance to calculate value.

Was this page helpful?
0 / 5 - 0 ratings