Efcore: 2.0.1-rtm-203 regression: System.InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.Linq.Expressions.LambdaExpression'

Created on 12 Nov 2017  路  23Comments  路  Source: dotnet/efcore

When configuring a query filter that captures a variable (I think that's whats it called?) in the expression 2.0.1-rtm-203 throws an InvalidCastException. This doesn't happen in 2.0.0

``` c#
// Works
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
modelBuilder.Entity().HasQueryFilter(e => e.TenantId == 123);
}

// Throws InvalidCastException when executing a query.
protected override void OnModelCreating(ModelBuilder modelBuilder)
{
int tenantId = 123;
modelBuilder.Entity().HasQueryFilter(e => e.TenantId == tenantId);
}

**OR**
1. `git clone https://github.com/nphmuller/EfCoreQueryFilterBug`
2. `dotnet run` throws exception. (See below for details)
3. open .csproj file and change version from 2.0.1-* to 2.0.0
4. `dotnet run` completes succesfully.

Same thing occurs when I use:
`https://dotnet.myget.org/gallery/aspnet-2-0-2-october2017-patch` - 2.0.1-rtm-207
instead of:
`https://dotnet.myget.org/gallery/aspnet-2-0-2-october2017-patch-public` - 2.0.1-rtm-203

Exception details:

Unhandled Exception: System.AggregateException: One or more errors occurred. (Unable to cast object of type 'System.Int32' to type 'System.Linq.Expressions.LambdaExpression'.) ---> System.InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.Linq.Expressions.LambdaExpression'.
at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.CreateSetFilterParametersExpressions(ParameterExpression& contextVariableExpression)
at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.CreateExecutorLambdaTResults
at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.CreateQueryExecutorTResult
at Microsoft.EntityFrameworkCore.Storage.Internal.InMemoryDatabase.CompileAsyncQueryTResult
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass24_01.<CompileAsyncQuery>b__0() at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func1 compiler)
at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.ExecuteAsyncTResult
at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryable1.System.Collections.Generic.IAsyncEnumerable<TResult>.GetEnumerator() at System.Linq.AsyncEnumerable.<Aggregate_>d__63.MoveNext()
--- End of inner exception stack trace ---
at System.Threading.Tasks.Task`1.GetResultCore(Boolean waitCompletionNotification)
at ConsoleApp3.Program.Main(String[] args) in C:\Dev_EfCoreQueryFilterBug\ConsoleApp3\Program.cs:line 14
```

closed-fixed regression type-bug

All 23 comments

On a sidenote: this regression is really biting me in the ass, because I have to use the preview feed because of https://github.com/aspnet/EntityFrameworkCore/issues/9038. A workaround would be really welcome, if known. :)

@nphmuller - How are you planning to change the value of tenantId in the query filter? The local variable is inside OnModelCreating. It will be run only once and it will pick up value of tenantId whenever it was run. Based on the query you posted above, putting the value inline will give the same results. As a matter of fact, in 2.0 version the generated SQL put value of 123 inline.
SQL generated for the same query in 2.0

      SELECT [b].[Id], [b].[TenantId]
      FROM [Blogs] AS [b]
      WHERE [b].[TenantId] = 123

I tried to give the easiest to repro sample. The same exception will also be thrown if a class level field or property, etc is used, if I remember correctly.

If a property/field at DbContext level is used then it is working as expected. If it is using property/field from a different class then problem remains the same, how are you planning to modify the value which went in the query filter? Or you are just trying to use constant defined on other class without modifying it ever? In that case the simplest work-around is to use the value inline.

Ah, seems I misremembered then. Sorry!

Initially I was trying to split the filter logic from my dbcontext, but since it鈥檚 not possible to change the filter value that way, I鈥檝e settled for setting the value at dbcontext level.

@nphmuller - No need to apologize. It is indeed bug. Thanks for reporting it to us. I am just try to collect data on use-case so that we can determine, how critical it is and should be fixed in next patch or not.

I'll pitch in my use case because my entire app just crashed when I updated to 2.0.1. I have a TenantProvider that is injected into my DbContext. The DbContext has a property called TenantId which is used in my query filters. My app depends on being able to pass in the tenant id, so I'd like to ask that you treat this with the highest priority possible. My sample code:

public sealed class TenantProvider :
    ITenantProvider {
    private readonly IHttpContextAccessor _accessor;

    public int? Id => _accessor.HttpContext.User.GetTenantId();
    public bool IsAuthenticated => _accessor.HttpContext.User.Identity.IsAuthenticated;

    public TenantProvider(
        IHttpContextAccessor accessor) {
        _accessor = accessor;
    }
}

public class MyContext :
    DbContext {
    private readonly ITenantProvider _tenantProvider;

    internal int? TenantId => _tenantProvider.Id;
    internal bool TenantIsAuthenticated => _tenantProvider.IsAuthenticated;

    public MyContext (
        DbContextOptions options,
        ITenantProvider tenantProvider) :
        base(options) {
        _tenantProvider = tenantProvider;
    }
}

internal sealed class GlobalUserConfiguration :
    GlobalEntityConfigurationBase<GlobalUser> {
    public GlobalUserConfiguration(
        MyContext context) :
        base("Users", context) {
    }

    protected override void ConfigureFilters(
        EntityTypeBuilder<GlobalUser> builder) {
        base.ConfigureFilters(builder);

        builder.HasQueryFilter(
            u =>
                (!Context.TenantIsAuthenticated
                    && u.Id != 1)
                || (Context.TenantIsAuthenticated
                    && u.Id != 1
                    && u.TenantId == Context.TenantId));
    }
}

@OphiCA - Did it work correctly in 2.0 version?

@smitpatel Yes, I had just gotten it to work too, and now it's just dead. I'm in the process of rolling everything back to 2.0.0 since a bunch of Core packages got updated.

@OphiCA - Please share stack trace and exception message you are getting.

@smitpatel Here you go:

System.InvalidCastException: Unable to cast object of type 'System.Int32' to type 'System.Linq.Expressions.LambdaExpression'.
   at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.CreateSetFilterParametersExpressions(ParameterExpression& contextVariableExpression)
   at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.CreateExecutorLambda[TResults]()
   at Microsoft.EntityFrameworkCore.Query.EntityQueryModelVisitor.CreateQueryExecutor[TResult](QueryModel queryModel)
   at Microsoft.EntityFrameworkCore.Storage.Database.CompileQuery[TResult](QueryModel queryModel)
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.CompileQueryCore[TResult](Expression query, INodeTypeProvider nodeTypeProvider, IDatabase database, IDiagnosticsLogger`1 logger, Type contextType)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.<>c__DisplayClass15_0`1.<Execute>b__0()
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQueryCore[TFunc](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.CompiledQueryCache.GetOrAddQuery[TResult](Object cacheKey, Func`1 compiler)
   at Microsoft.EntityFrameworkCore.Query.Internal.QueryCompiler.Execute[TResult](Expression query)
   at Microsoft.EntityFrameworkCore.Query.Internal.EntityQueryProvider.Execute[TResult](Expression expression)
   at Remotion.Linq.QueryableBase`1.GetEnumerator()
   at System.Collections.Generic.List`1.AddEnumerable(IEnumerable`1 enumerable)
   at System.Linq.Enumerable.ToList[TSource](IEnumerable`1 source)
   at AutoMapper.QueryableExtensions.AutoMapperQueryableExtensions.ProjectToList[TDestination](IQueryable queryable) in D:\Visual Studio\MyApp\MyApp.Web\Extensions\AutoMapperQueryableExtensions.cs:line 8
   at MyApp.Web.Sections.TenantRegions.List.QueryHandler.Handle(Query query) in D:\Visual Studio\MyApp\MyApp.Web\Sections\TenantRegions\List.cs:line 30
   at MediatR.Internal.RequestHandlerImpl`2.<>c__DisplayClass5_0.<GetHandlerFactory>b__1()
   at MediatR.Internal.RequestHandlerImpl`2.Handle(IRequest`1 request, CancellationToken cancellationToken, SingleInstanceFactory singleFactory, MultiInstanceFactory multiFactory)
   at MediatR.Mediator.Send[TResponse](IRequest`1 request, CancellationToken cancellationToken)
   at MyApp.Web.Sections.TenantRegions.TenantRegionsController.<Default>d__4.MoveNext() in D:\Visual Studio\MyApp\MyApp.Web\Sections\TenantRegions\TenantRegionsController.cs:line 36
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at MyApp.Web.Sections.TenantRegions.TenantRegionsController.<Default>d__3.MoveNext() in D:\Visual Studio\MyApp\MyApp.Web\Sections\TenantRegions\TenantRegionsController.cs:line 30
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at System.Runtime.CompilerServices.TaskAwaiter`1.GetResult()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeActionMethodAsync>d__12.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeNextActionFilterAsync>d__10.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Rethrow(ActionExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ControllerActionInvoker.<InvokeInnerFilterAsync>d__14.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.<InvokeNextResourceFilter>d__22.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.<InvokeFilterPipelineAsync>d__17.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.<InvokeAsync>d__15.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Builder.RouterMiddleware.<Invoke>d__4.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at MyApp.Web.Middlewares.MarkupMinifierMiddleware.<Invoke>d__2.MoveNext() in D:\Visual Studio\MyApp\MyApp.Web\Middlewares\MarkupMinifierMiddleware.cs:line 25
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.<Invoke>d__6.MoveNext()
--- End of stack trace from previous location where exception was thrown ---
   at System.Runtime.ExceptionServices.ExceptionDispatchInfo.Throw()
   at System.Runtime.CompilerServices.TaskAwaiter.HandleNonSuccessAndDebuggerNotification(Task task)
   at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.<Invoke>d__7.MoveNext()

Can you also share the project which reproduce the issue?

Unfortunately that I can't do. I'm sorry.

same case same result after 2.0.3,

       //Not work
        int languageRef = 1;
        modelBuilder.Entity<TEntity>().HasQueryFilter(x => x.LanguageRef == languageRef);

       //Work
        modelBuilder.Entity<TEntity>().HasQueryFilter(x => x.LanguageRef == 1);

details,

// POCO #1   
public partial class tb_Status : IAuditable
{
    public tb_Status()
    {
        tb_Entry = new HashSet<tb_Entry>();
        tb_EntryStatusChange = new HashSet<tb_EntryStatusChange>();
    }

    public int StatusRef { get; set; }
    public string StatusCode { get; set; }
    public int CreatedBy { get; set; }
    public DateTime CreatedDate { get; set; }
    public int ModifiedBy { get; set; }
    public DateTime ModifiedDate { get; set; }
    public bool IsDeleted { get; set; }

    public ICollection<tb_Entry> tb_Entry { get; set; }
    public ICollection<tb_EntryStatusChange> tb_EntryStatusChange { get; set; }
    public ICollection<tb_Status_Locale> tb_Status_Locale { get; set; }
}

// POCO #2  
public partial class tb_Status_Locale : IAuditable, ILocalizable
{
    public int StatusLocaleRef { get; set; }
    public int StatusRef { get; set; }
    public int LanguageRef { get; set; }
    public string StatusName { get; set; }
    public int CreatedBy { get; set; }
    public DateTime CreatedDate { get; set; }
    public int ModifiedBy { get; set; }
    public DateTime ModifiedDate { get; set; }
    public bool IsDeleted { get; set; }

    public tb_Status StatusRefNavigation { get; set; }
}

// Configuration
public void Configure(EntityTypeBuilder<tb_Status_Locale> builder)
{
        builder.HasKey(e => e.StatusLocaleRef);
        builder.Property(e => e.CreatedDate).HasColumnType("datetime");
        builder.Property(e => e.ModifiedDate).HasColumnType("datetime");
        builder.Property(e => e.StatusName)
            .IsRequired()
            .HasMaxLength(200);
        builder.HasOne(d => d.StatusRefNavigation)
            .WithMany(p => p.tb_Status_Locale)
            .HasForeignKey(d => d.StatusRef)
            .HasConstraintName("FK_tb_Status_Locale_tb_Status");
}

// Throw at
_context.tb_Status.Locale

@OphiCA - I will try reproducing based on the bits you shared but in 2.0.0 unless you use a property directly on context it will not work. Combined with other thing with query filter in EntityTypeConfiguration, even though it is based on context property but with different mechanism. In order for us to translate it correctly and allow users to change the value at runtime, it should be captured as member access on context. From my initial investigation, that was not the case when using EntityTypeConfiguration. Compiler generated somewhat different expression tree for it. Which again causes the same issue, even if you change value in your context, it will not change in the query filter. Is that what you really want? Can you try moving filter configuration inside OnModelCreating and see how that goes.

@mehmetsari - Yes it is the same case but again same question to you, when you are trying to use a local variable in filter, how are you planning to use it. You cannot change the value and if you pass value through variable or inline it both will generate the same result because there is no way to update the value of closure variable used in filter unless it is based on DbContext.

to All - As of 2.0.1, non-range variables used in query filters which are members on DbContext or members of class/struct which is in turn member of DbContext can have its value replaced from runtime DbContext.
Even after fixing this issue, any other variables no matter how you defined it, will get value inlined and you cannot change it afterwards. There is no other way for us to do anything otherwise. Is the expected behavior? If yes, then work-around is to just inline constant value rather than using variable. If no, then you need to update your query filter and it has nothing to do with this bug report.

@smitpatel - I shortened the content to show you actually, it was not a local variable. It was came from an identity service (constructor) injection to dbcontext and it was working yesterday like a charm.

    private readonly IIdentityService _identityService;

    public AppDBContext(DbContextOptions options, IIdentityService identityService) : base(options)
    {
        _identityService = identityService;
    }

    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        var applicationUser = _identityService.CurrentUser();

        foreach (var type in modelBuilder.Model.GetEntityTypes())
        {
            // Bool type, work.
            if (typeof(ISoftDeletable).IsAssignableFrom(type.ClrType))
                modelBuilder.SetSoftDeleteFilter(type.ClrType);

           // Int32 type, not work
            if (typeof(ILocalizable).IsAssignableFrom(type.ClrType))
                modelBuilder.SetLanguageFilter(type.ClrType, applicationUser.LanguageRef);
        }
    }

  //Extensions
   public static void SetSoftDeleteFilter<TEntity>(this ModelBuilder modelBuilder) where TEntity : class, ISoftDeletable
    {
        // Work
        modelBuilder.Entity<TEntity>().HasQueryFilter(x => !x.IsDeleted);
    }

  public static void SetLanguageFilter<TEntity>(this ModelBuilder modelBuilder, int languageRef) where TEntity : class, ILocalizable
    {
        // Not work
        modelBuilder.Entity<TEntity>().HasQueryFilter(x => x.LanguageRef == languageRef);
    }

@smitpatel I tested moving the filter to OnModelCreating and it works, though I'm not happy about having to do it this way. It defeats the purpose of me having a base configuration class for commonly grouped entities. My app currently is a baby, and the amount of entities that depend on a tenant id, which I need to filter by, will double, triple, or increase even more than that.

While I can't share the full project, I can at least share all the relevant code for my use case. So, basically, I am build a multi-tenant app. When a user signs in I add an identity claim to their cookie which contains the tenant id they belong to. On subsequent requests, I want to read the tenant id out of the cookie and use it as a query filter. A fairly straight forward scenario. I was having a really difficult time getting it to work, until a user on SO pointed me in the right direction. The SO Q&A is here, and in a twist of fate, I think you commented on it: https://stackoverflow.com/questions/47268072/ef-core-2-0-query-filter-is-caching-tenantid

Anyway, on to the relevant code:

MyAppUserClaimsPrincipalFactory.cs

public sealed class MyAppUserClaimsPrincipalFactory :
    UserClaimsPrincipalFactory<GlobalUser> {
    public MyAppUserClaimsPrincipalFactory(
        MyAppUserManager userManager,
        IOptions<IdentityOptions> optionsAccessor)
        : base(userManager, optionsAccessor) {
    }

    protected override async Task<ClaimsIdentity> GenerateClaimsAsync(
        GlobalUser user) {
        var identity = await base.GenerateClaimsAsync(user);

        identity.AddClaim(new Claim("MyApp.TenantId", user.TenantId.ToString()));

        return identity;
    }
}

TenantProvider.cs

public sealed class TenantProvider :
    ITenantProvider {
    private readonly IHttpContextAccessor _accessor;

    public int? Id => _accessor.HttpContext.User.GetTenantId();
    public bool IsAuthenticated => _accessor.HttpContext.User.Identity.IsAuthenticated;

    public TenantProvider(
        IHttpContextAccessor accessor) {
        _accessor = accessor;
    }
}

MyAppContext.cs

public class MyAppContext :
    DbContext {
    private readonly ITenantProvider _tenantProvider;

    internal int? TenantId => _tenantProvider.Id;
    internal bool TenantIsAuthenticated => _tenantProvider.IsAuthenticated;

    public DbSet<GlobalUser> GlobalUsers { get; set; }

    public MyAppContext(
        DbContextOptions options,
        ITenantProvider tenantProvider) :
        base(options) {
        _tenantProvider = tenantProvider;
    }

    protected override void OnModelCreating(
        ModelBuilder modelBuilder) {
        modelBuilder.ApplyConfiguration(new GlobalUserConfiguration(this));

        //  works in 2.0.1
        modelBuilder.Entity<GlobalUser>().HasQueryFilter(
            u =>
                (!TenantIsAuthenticated
                    && u.Id != 1)// user 1 is SYSTEM
                || (TenantIsAuthenticated
                    && u.Id != 1
                    && u.TenantId == TenantId));
    }
}

GlobalUserConfiguration.cs

internal sealed class GlobalUserConfiguration :
    GlobalEntityConfigurationBase<GlobalUser> {
    public GlobalUserConfiguration(
        MyAppContext context) :
        base("Users", context) {
    }

    protected override void ConfigureFilters(
        EntityTypeBuilder<GlobalUser> builder) {
        //  does NOT work in 2.0.1
        //builder.HasQueryFilter(
        //  u =>
        //      (!Context.TenantIsAuthenticated
        //          && u.Id != 1)
        //      || (Context.TenantIsAuthenticated
        //          && u.Id != 1
        //          && u.TenantId == Context.TenantId));
    }
}

And for completeness sake, GlobalEntityConfigurationBaseTEntityTKey.cs

internal abstract class GlobalEntityConfigurationBase<TEntity, TKey> :
    EntityConfigurationBase<TEntity, TKey>
    where TEntity : GlobalEntityBase<TKey>
    where TKey : IEquatable<TKey> {
    protected readonly MyAppContext Context;

    protected GlobalEntityConfigurationBase(
        string table,
        MyAppContext context) :
        base(table, Schemas.Global) {
        Context = context;
    }

    protected override void ConfigureFilters(
        EntityTypeBuilder<TEntity> builder) {
        base.ConfigureFilters(builder);

        //  does NOT work in 2.0.1
        builder.HasQueryFilter(
            e => e.TenantId == Context.TenantId);
    }
}

Lastly, Startup.cs

public class Startup {
    public void ConfigureServices(
        IServiceCollection services) {
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        services.AddDbContext<MyAppContext>(
            o => {
                o.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"));
            });
        services.AddMyAppIdentity();
        services.AddMvc();
        services.AddScoped<ITenantProvider, TenantProvider>();
    }
}

I think that about covers my entire process. Ultimately I just need to be able to filter by the tenant id so my multi-tenant app behaves like a multi-tenant app. I'm not happy about having to set the filters in the OnModelCreating method because then I can't leverage my base classes to their fullness. Also because it takes me forgetting to set one filter and something bad happening.

I hope we can go back to the way it worked in 2.0.0.

On a side note, not that it matters now that I can't filter the way I want in my base config classes, it's really annoying that I can't specify multiple filters by inheritance and have them all just collapse into one big filter. Every call to HasQueryFilter seems to override any other filters applied before rather than append to them. Or at least that's what my experience has been.

Thank you for taking the time to go through all of this.

This is 2.0.0 behavior:

For the example of @mehmetsari
OnModelCreating is run only once when building model first time. It is cached for later use. Once you create a local variable applicationUser, its value won't change even if the value of _identityService changes. Its like taking a snapshot of value at the time code is run. The query filter will have that value of local variable which has a fixed value assigned to it when running OnModelCreating. So everytime you run the query filter it will get same value regardless of the value of _identityService.

For the code of @OphiCA
While it is desirable to use EntityTypeConfiguration that is a limitation currently. The issue is similar to what I explained above. When creating Configuration object, you got a snapshot of current context and the value which went in query filter is not context dependent rather it is based on configuration object. Since that object is not changing, GlobalUserConfiguration.Context.TenantId is tied to a fix value. Even if context changes no new configuration is created.

For Filters & EntityTypeConfiguration I filed #10301

In summary for both cases while 2.0.0 did not throw any exception, it produced incorrect results. In 2.0.1 it is throwing exception now.

So are you sure that it worked properly in 2.0.0 and gave correct results? I am having hard time putting together an app which gives correct results in 2.0.0

Reopening for triage.

I can confirm that it does work as I expect it to work in 2.0.0. To test, I have three "admin" users. One doesn't have a tenant id, which makes them a system user, the other two have different tenant ids which makes them users for two different tenants.

I logged in and out of each one in order, 10 times each, for a total of 30 sessions. When logging into the system user I was redirected to the system dashboard page. When logging in as the tenant users I was redirected to the tenant dashboard page where the user's tenant company name was written out. Everything worked as it should have.

So, correct me if I'm wrong, but the official stance on this issue for 2.0.1+ is that query filters that depend on outside values have to be defined in the OnModelCreating method. The reason being is that although the DbContext is instanced per scope, the EntityTypeConfiguration classes are instanced once, probably at start of application or first call into the DbContext, and their results are cached for the duration of the app's lifetime and thus the query filter values are permanently inlined. Do I have it right?

Yes that is correct.
The only difference in 2.0.0 was we just looked at top level member expression and somehow it matched up Context.Member in EntityTypeConfiguration. That's how it worked by co-incidence. If you use multi-level property access or method call it wouldn't work. We will discuss in team about how to deal with that regression in #10301

As for this issue,
if people are using something which is not on context, that was incorrect result and now exception.

@smitpatel One thing that might make things "work" in 2.0.0 is if the app is causing a new internal service provider to be built each time--for example, if it is doing anything like .UseLoggerFactory(new LoggerFactory).

@OphiCA Do you use OnConfiguring at all? Is your AddDbContext exactly as posted above?

@ajcvickers I do not use OnConfiguring at all, and AddDbContext is exactly as posted.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

dgxhubbard picture dgxhubbard  路  97Comments

matteocontrini picture matteocontrini  路  88Comments

0xdeafcafe picture 0xdeafcafe  路  467Comments

divega picture divega  路  100Comments

econner20 picture econner20  路  97Comments