When using HasQueryFilter from inside an IEntityTypeConfiguration
However, when HasQueryFilter is invoked from the OnModelCreating method directly, it works as expected: the resulting query will have a parameter defined.
Correct behavior can be seen by uncommenting the following line:
```c#
//Works as expected
modelBuilder.Entity
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 [_]'
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
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.
Most helpful comment
When you are using inside
OnModelCreating, even though you are not explicitly writing, the variable is captured ascontext._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
_tenantIdProviderin EntityConfiguration's ctor, what we see in expression tree for that query filter isUserConfiguration._tenantIdProvider.TenandId. We do not have a way to know how this _tenantIdProvider's value is connected withDbContext._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.