Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Precompiled queries assume the model for a given DbContext will never change #13483

Closed
JanEggers opened this issue Oct 3, 2018 · 28 comments · Fixed by #29767
Closed

Precompiled queries assume the model for a given DbContext will never change #13483

JanEggers opened this issue Oct 3, 2018 · 28 comments · Fixed by #29767
Labels
area-query closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported punted-for-2.2 punted-for-3.0 punted-for-5.0 punted-for-6.0 type-bug
Milestone

Comments

@JanEggers
Copy link

Steps to reproduce

when I use EF.CompileQuery to generate a Query it fails to find the result. executing the linq normally returns the result.

I also verified that it is not a threading issue. the item is there all the time and if im setting the debugger to call FindItemQuery again it still returns null.

The behavior is observed in a unittest. using xunit. If the test is executed alone it works most of the time. If I execute all the tests it sometimes returns null

var services = new ServiceCollection()
                .AddDbContext<DbContext>(o => o.UseInMemoryDatabase(nameof(TestClass)+ nameof(TestMethod)))
                .BuildServiceProvider();


private static readonly Func<DbContext, string, Item> FindItemQuery =
            EF.CompileQuery<DbContext, string, Item>((ctx, key) => ctx.Items
                .FirstOrDefault(x => x.Key == key));

        protected override Item FindItem(DbContext dbContext, string key)
        {
            var found = FindItemQuery(dbContext, key);

            if (found == null)
            {
                var xxx = dbContext.Items.FirstOrDefault(t => t.Key== key);
                //xxx is not null
            }

            return found ?? new Item()
            {
                Key = key
            };
        }

Further technical details

EF Core version: Microsoft.EntityFrameworkCore.InMemory Version="2.1.2"
Operating system: Win 10
IDE: (Visual Studio 2017 15.8)

@JanEggers
Copy link
Author

updating to Microsoft.EntityFrameworkCore.InMemory Version="2.1.4" did not resolve the issue

@JanEggers
Copy link
Author

from the in memoryQuery context i get from the precompiled view when removing firstordefault I see that the table contains the values but the enumerable is based on a snapshot that does not contain the values!

_innerEnumerable.results.source.source. Microsoft.EntityFrameworkCore.InMemory.Storage.Internal.InMemoryTableSnapshot = 0
!=
InMemoryQueryContext.Store.Tables.InMemoryTable.Rows.Count = 3

@JanEggers
Copy link
Author

creating a new compiled query also returns the correct result

@JanEggers
Copy link
Author

meh I dont get it if I do the exact same code as in EF.CompileQuery in my class it works if I use the default method I get an empty snapshot.

@JanEggers
Copy link
Author

removing static from the FindItemQuery also fixed the issue, so ill put the Precompiled Queries into a di Singleton instead of using static

@smitpatel
Copy link
Member

Likely, you are not using same InMemoryDatabase. InMemoryDatabase may change if service provider is re-created.

@ajcvickers
Copy link
Member

ajcvickers commented Oct 3, 2018

@JanEggers Can you post a runnable project/solution or complete code listing that demonstrates the issue?

@smitpatel That was my thought too, but then, I can't reconcile that with, "If the test is executed alone it works most of the time. If I execute all the tests it sometimes returns null."

@JanEggers
Copy link
Author

I will try to create a repro.

@JanEggers
Copy link
Author

here is a repro:
https://github.com/JanEggers/EfCoreRepro_PrecompiledQuery

and yes it is another context but that should not be an issue as I pass the new context to the precompiled query when executing it.

@JanEggers
Copy link
Author

JanEggers commented Oct 3, 2018

so this is the problem?

https://github.com/aspnet/EntityFrameworkCore/blob/a3f0a78c41fe209924f8fb39fc77c421236f1bbe/src/EFCore/Query/Internal/CompiledQueryBase.cs#L90-L116

I think that if the context is captured inside the compiled query it should be provided as a constructor argument. passing it with every execution makes no sense when there is some state leaking from the first usage.

@JanEggers
Copy link
Author

why isnt that something like:

(with a const or some reflection magic to get the context name)

if (typeof(TContext).GetTypeInfo().IsAssignableFrom(parameterExpression.Type.GetTypeInfo())) 
             { 
                 return Expression.Parameter( 
                     parameterExpression.Type, 
                     "context")
             } 

https://github.com/aspnet/EntityFrameworkCore/blob/a3f0a78c41fe209924f8fb39fc77c421236f1bbe/src/EFCore/Query/Internal/CompiledQueryBase.cs#L58-L65

and here:

queryContext.AddParameter(
                    "context",
                    context);

@ajcvickers
Copy link
Member

Note for triage: something strange is going on here with the in-memory database. Repro code is below. Running with SQL Server results in:

Query one: One
Query two: One
Query one: Two
Query two: Two

while with the only change being UseSqlServer to UseInMemoryDatabase the results are:

Query one: One
Query two: One
Query one:
Query two:

Note that the internal service provider is being managed by EF, The service provider created in the code is only used for AddDbContext and resolving that context.

SQL Server log fragment:

dbug: Microsoft.EntityFrameworkCore.Query[10101]
      => Microsoft.EntityFrameworkCore.Query.RelationalQueryModelVisitor
      Compiling query model:
      '(from Item <generated>_1 in DbSet<Item>
      select [<generated>_1]).SingleOrDefault()'
dbug: Microsoft.EntityFrameworkCore.Query[10104]
      => Microsoft.EntityFrameworkCore.Query.RelationalQueryModelVisitor
      Optimized query model:
      '(from Item <generated>_1 in DbSet<Item>
      select [<generated>_1]).SingleOrDefault()'
dbug: Microsoft.EntityFrameworkCore.Query[10107]
      => Microsoft.EntityFrameworkCore.Query.RelationalQueryModelVisitor
      (QueryContext queryContext) => IEnumerable<Item> _InterceptExceptions(
          source: IEnumerable<Item> _TrackEntities(
              results: IEnumerable<Item> _ToSequence(() => Item SingleOrDefault(IEnumerable<Item> _ShapedQuery(
                          queryContext: queryContext,
                          shaperCommandContext: SelectExpression:
                              SELECT TOP(2) [i].[Id], [i].[Name]
                              FROM [Items] AS [i],
                          shaper: UnbufferedEntityShaper<Item>))),
              queryContext: queryContext,
              entityTrackingInfos: { itemType: Item },
              entityAccessors: List<Func<Item, object>>
              {
                  Func<Item, Item>,
              }
          ),
          contextType: MyDbContext,
          logger: DiagnosticsLogger<Query>,
          queryContext: queryContext)
dbug: Microsoft.EntityFrameworkCore.Database.Connection[20000]
      Opening connection to database 'FOO' on server '(localdb)\mssqllocaldb'.
dbug: Microsoft.EntityFrameworkCore.Database.Connection[20001]
      Opened connection to database 'FOO' on server '(localdb)\mssqllocaldb'.
dbug: Microsoft.EntityFrameworkCore.Database.Command[20100]
      Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT TOP(2) [i].[Id], [i].[Name]
      FROM [Items] AS [i]

...

dbug: Microsoft.EntityFrameworkCore.Database.Connection[20000]
      Opening connection to database 'BAR' on server '(localdb)\mssqllocaldb'.
dbug: Microsoft.EntityFrameworkCore.Database.Connection[20001]
      Opened connection to database 'BAR' on server '(localdb)\mssqllocaldb'.
dbug: Microsoft.EntityFrameworkCore.Database.Command[20100]
      Executing DbCommand [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT TOP(2) [i].[Id], [i].[Name]
      FROM [Items] AS [i]
info: Microsoft.EntityFrameworkCore.Database.Command[20101]
      Executed DbCommand (0ms) [Parameters=[], CommandType='Text', CommandTimeout='30']
      SELECT TOP(2) [i].[Id], [i].[Name]
      FROM [Items] AS [i]

In-memory log:

dbug: Microsoft.EntityFrameworkCore.Query[10101]
      => Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryQueryModelVisitor
      Compiling query model:
      '(from Item <generated>_1 in DbSet<Item>
      select [<generated>_1]).SingleOrDefault()'
dbug: Microsoft.EntityFrameworkCore.Query[10104]
      => Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryQueryModelVisitor
      Optimized query model:
      '(from Item <generated>_1 in DbSet<Item>
      select [<generated>_1]).SingleOrDefault()'
dbug: Microsoft.EntityFrameworkCore.Query[10107]
      => Microsoft.EntityFrameworkCore.InMemory.Query.Internal.InMemoryQueryModelVisitor
      (QueryContext queryContext) => IEnumerable<Item> _InterceptExceptions(
          source: IEnumerable<Item> _TrackEntities(
              results: IEnumerable<Item> _ToSequence(() => Item SingleOrDefault(IEnumerable<Item> EntityQuery(
                          queryContext: queryContext,
                          entityType: EntityType: Item,
                          key: Key: Item.Id PK,
                          materializer: (IEntityType entityType | MaterializationContext materializationContext) =>
                          {
                              instance = new Item()
                              instance.<Id>k__BackingField = int TryReadValue(ValueBuffer materializationContext.get_ValueBuffer(), 0, Item.Id)
                              instance.<Name>k__BackingField = string TryReadValue(ValueBuffer materializationContext.get_ValueBuffer(), 1, Item.Name)
                              return instance
                          }
                          ,
                          queryStateManager: True))),
              queryContext: queryContext,
              entityTrackingInfos: List<EntityTrackingInfo>
              {
                  EntityTrackingInfo,
              }
              ,
              entityAccessors: List<Func<Item, object>>
              {
                  Func<Item, Item>,
              }
          ),
          contextType: MyDbContext,
          logger: DiagnosticsLogger<Query>,
          queryContext: queryContext)

Repro code:

public class MyDbContext : DbContext
{
    public MyDbContext(DbContextOptions<MyDbContext> options)
        : base(options)
    {
    }

    private static readonly LoggerFactory Logger
        = new LoggerFactory(new[] { new ConsoleLoggerProvider((_, __) => true, true) });

    protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
        => optionsBuilder.UseLoggerFactory(Logger);

    public DbSet<Item> Items { get; set; }
}

public class Item
{
    public int Id { get; set; }

    public string Name { get; set; }
}

public class MyServiceCollection : ServiceCollection
{
    public MyServiceCollection(string dbName)
    {
        this.AddDbContext<MyDbContext>(
            o => o.UseInMemoryDatabase($@"Server=(localdb)\mssqllocaldb;Database={dbName};ConnectRetryCount=0"));
    }
}

public class UnitTest1
{
    public static readonly Func<MyDbContext, Item> PreCompiled = EF.CompileQuery<MyDbContext, Item>(ctx => ctx.Items.SingleOrDefault());

    public void Test1()
    {
        using (var services = new MyServiceCollection("FOO").BuildServiceProvider())
        {
            using (var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var ctx = scope.ServiceProvider.GetRequiredService<MyDbContext>();

                ctx.Database.EnsureDeleted();
                ctx.Database.EnsureCreated();

                ctx.Add(new Item { Name = "One" });

                ctx.SaveChanges();

                var item = PreCompiled(ctx);
                Console.WriteLine($"Query one: {item?.Name}");
            }

            using (var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var ctx = scope.ServiceProvider.GetRequiredService<MyDbContext>();
                var item = PreCompiled(ctx);
                Console.WriteLine($"Query two: {item?.Name}");
            }
        }

        using (var services = new MyServiceCollection("BAR").BuildServiceProvider())
        {
            using (var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var ctx = scope.ServiceProvider.GetRequiredService<MyDbContext>();
                ctx.Database.EnsureDeleted();
                ctx.Database.EnsureCreated();

                ctx.Add(new Item { Name = "Two" });

                ctx.SaveChanges();

                var item = PreCompiled(ctx);
                Console.WriteLine($"Query one: {item?.Name}");
            }

            using (var scope = services.GetRequiredService<IServiceScopeFactory>().CreateScope())
            {
                var ctx = scope.ServiceProvider.GetRequiredService<MyDbContext>();
                var item = PreCompiled(ctx);
                Console.WriteLine($"Query two: {item?.Name}");
            }
        }
    }
}

@JanEggers
Copy link
Author

I investigated further and guess I found the issue:

https://github.com/aspnet/EntityFrameworkCore/blob/c599721a3f9bcc71a35f7e363bcb1e46ef92d8ba/src/EFCore.InMemory/Storage/Internal/InMemoryStore.cs#L119

the table is not found at that line because because the EntityType are not ReferenceEqual because one is from the first model and the other is from the second model

the easiest fix would be to enable matching by name that is already in place but I dont know why it is disabled by default

JanEggers added a commit to JanEggers/EntityFramework that referenced this issue Oct 4, 2018
…nt in memory database

- locate inmemory tables by entitytype.toString()
- removed option useNameMatching as the new method works for all scenarios

fixes dotnet#13483
@ajcvickers
Copy link
Member

@JanEggers Did you root cause why the in-memory database is rooted in a different internal service provider?

@JanEggers
Copy link
Author

JanEggers commented Oct 5, 2018

@ajcvickers I did not check. but the different database is because of the different strings provided in UseInMemoryDatabase. my understanding is that each unique string gets its own database. and that is the way I intended my tests so I get a fresh isolated database for each test.

https://github.com/aspnet/EntityFrameworkCore/blob/39acb62595ba64347e68ac986d55040ddd496395/src/EFCore.InMemory/Storage/Internal/InMemoryStoreCache.cs#L62-L63

from my point of view the real problem is that the entity types are not ref equal. so I guess i could also workaround the issue by creating the model first with a modelbuilder and then use the same model for multiple contexts.

@ajcvickers
Copy link
Member

Notes from triage: we think there are two things going here:

  • The in-memory database is creating a different model instance when used with the second external service provider. This is probably because a new internal service provider is being created, which may be by-design, but should be investigated anyway. @ajcvickers to investigate this aspect.
  • A query that was compiled with one model is then being re-used with another model without re-compiling. This is the more serious issue which we may need to fix for 2.2. @smitpatel to investigate this aspect.

@ajcvickers ajcvickers added this to the 2.2.0 milestone Oct 5, 2018
@smitpatel
Copy link
Member

Currently the query cache has 4 core components

  • Expression query
  • Model
  • QueryTrackingBehaviour
  • Async
    Relational adds UseRelationalNulls & SqlServer adds RowNumberPaging.

So we are safe from query cache side.

@smitpatel smitpatel removed their assignment Oct 8, 2018
@ajcvickers ajcvickers modified the milestones: 2.2.0-preview3, 2.2.0 Oct 15, 2018
@ajcvickers ajcvickers modified the milestones: 2.2.0, 3.0.0 Oct 26, 2018
@ajcvickers
Copy link
Member

Note from triage: this is blocked on the new compiled query API. With the new API it should be possible to use the same DbContext type with different underlying models. However, specifying the model to use explicitly may be required when compiling the query.

@ajcvickers ajcvickers modified the milestones: 6.0.0, Backlog May 5, 2021
@roji
Copy link
Member

roji commented May 6, 2021

To continue the above, since compiled queries are a high-perf thing, we should avoid perform an additional lookup at runtime on the model. So the model could be referenced by the compiled query itself, and on execution simply compared with the given context's model (throw if not the same).

@ajcvickers ajcvickers modified the milestones: Backlog, 7.0.0 Nov 10, 2021
@smitpatel smitpatel removed their assignment Nov 19, 2021
@ajcvickers ajcvickers removed the blocked label Dec 4, 2022
@ajcvickers ajcvickers self-assigned this Dec 4, 2022
@ajcvickers ajcvickers modified the milestones: Backlog, 8.0.0 Dec 4, 2022
@ajcvickers ajcvickers added the closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. label Dec 4, 2022
@ajcvickers ajcvickers modified the milestones: 8.0.0, 8.0.0-preview1 Jan 29, 2023
@ajcvickers ajcvickers modified the milestones: 8.0.0-preview1, 8.0.0 Nov 14, 2023
@ajcvickers ajcvickers removed their assignment Sep 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area-query closed-fixed The issue has been fixed and is/will be included in the release indicated by the issue milestone. customer-reported punted-for-2.2 punted-for-3.0 punted-for-5.0 punted-for-6.0 type-bug
Projects
None yet
5 participants