diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs
index a3d465a0dd8..502aa450e37 100644
--- a/src/EFCore/Properties/CoreStrings.Designer.cs
+++ b/src/EFCore/Properties/CoreStrings.Designer.cs
@@ -424,6 +424,14 @@ public static string ComparerPropertyMismatch(object? type, object? entityType,
GetString("ComparerPropertyMismatch", nameof(type), nameof(entityType), nameof(propertyName), nameof(propertyType)),
type, entityType, propertyName, propertyType);
+ ///
+ /// The compiled query '{queryExpression}' was executed with a different model than it was compiled against. Compiled queries can only be used with a single model.
+ ///
+ public static string CompiledQueryDifferentModel(object? queryExpression)
+ => string.Format(
+ GetString("CompiledQueryDifferentModel", "queryExpression"),
+ queryExpression);
+
///
/// There are multiple properties with the [ForeignKey] attribute pointing to navigation '{1_entityType}.{0_navigation}'. To define a composite foreign key using data annotations, use the [ForeignKey] attribute on the navigation.
///
diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx
index a48ad96ab5a..b164e76ae65 100644
--- a/src/EFCore/Properties/CoreStrings.resx
+++ b/src/EFCore/Properties/CoreStrings.resx
@@ -267,6 +267,9 @@
The comparer for type '{type}' cannot be used for '{entityType}.{propertyName}' because its type is '{propertyType}'.
+
+ The compiled query '{queryExpression}' was executed with a different model than it was compiled against. Compiled queries can only be used with a single model.
+
There are multiple properties with the [ForeignKey] attribute pointing to navigation '{1_entityType}.{0_navigation}'. To define a composite foreign key using data annotations, use the [ForeignKey] attribute on the navigation.
diff --git a/src/EFCore/Query/Internal/CompiledQueryBase.cs b/src/EFCore/Query/Internal/CompiledQueryBase.cs
index ec33f3e0036..7c8ff04777a 100644
--- a/src/EFCore/Query/Internal/CompiledQueryBase.cs
+++ b/src/EFCore/Query/Internal/CompiledQueryBase.cs
@@ -16,7 +16,19 @@ public abstract class CompiledQueryBase
{
private readonly LambdaExpression _queryExpression;
- private Func? _executor;
+ private ExecutorAndModel? _executor;
+
+ private sealed class ExecutorAndModel
+ {
+ public ExecutorAndModel(Func executor, IModel model)
+ {
+ Executor = executor;
+ Model = model;
+ }
+
+ public Func Executor { get; }
+ public IModel Model { get; }
+ }
///
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
@@ -51,7 +63,13 @@ protected virtual TResult ExecuteCore(
CancellationToken cancellationToken,
params object?[] parameters)
{
- var executor = EnsureExecutor(context);
+ EnsureExecutor(context);
+
+ if (_executor!.Model != context.Model)
+ {
+ throw new InvalidOperationException(CoreStrings.CompiledQueryDifferentModel(_queryExpression.Print()));
+ }
+
var queryContextFactory = context.GetService();
var queryContext = queryContextFactory.Create();
@@ -64,7 +82,7 @@ protected virtual TResult ExecuteCore(
parameters[i]);
}
- return executor(queryContext);
+ return _executor.Executor(queryContext);
}
///
@@ -77,7 +95,7 @@ protected abstract Func CreateCompiledQuery(
IQueryCompiler queryCompiler,
Expression expression);
- private Func EnsureExecutor(TContext context)
+ private void EnsureExecutor(TContext context)
=> NonCapturingLazyInitializer.EnsureInitialized(
ref _executor,
this,
@@ -88,7 +106,7 @@ private Func EnsureExecutor(TContext context)
var queryCompiler = c.GetService();
var expression = new QueryExpressionRewriter(c, q.Parameters).Visit(q.Body);
- return t.CreateCompiledQuery(queryCompiler, expression);
+ return new ExecutorAndModel(t.CreateCompiledQuery(queryCompiler, expression), c.Model);
});
private sealed class QueryExpressionRewriter : ExpressionVisitor
diff --git a/test/EFCore.Tests/Query/CompiledQueryTest.cs b/test/EFCore.Tests/Query/CompiledQueryTest.cs
new file mode 100644
index 00000000000..b781fe287cd
--- /dev/null
+++ b/test/EFCore.Tests/Query/CompiledQueryTest.cs
@@ -0,0 +1,76 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+namespace Microsoft.EntityFrameworkCore.Query;
+
+public class CompiledQueryTest
+{
+ [ConditionalFact]
+ public void CompiledQuery_throws_when_used_with_different_models()
+ {
+ using var context1 = new SwitchContext();
+ using var context2 = new SwitchContext();
+
+ var query = EF.CompileQuery((SwitchContext c, Bar p1) => c.Foos.Where(e => e.Bars.Contains(p1)));
+
+ _ = query(context1, new Bar()).ToList();
+ _ = query(context1, new Bar()).ToList();
+
+ Assert.Equal(
+ CoreStrings.CompiledQueryDifferentModel("(c, p1) => c.Foos .Where(e => e.Bars.Contains(p1))"),
+ Assert.Throws(
+ () => query(context2, new Bar()).ToList())
+ .Message.Replace("\r", "").Replace("\n", ""), ignoreWhiteSpaceDifferences: true);
+
+ _ = query(context1, new Bar()).ToList();
+ }
+
+ [ConditionalFact]
+ public async Task CompiledQueryAsync_throws_when_used_with_different_models()
+ {
+ using var context1 = new SwitchContext();
+ using var context2 = new SwitchContext();
+
+ var query = EF.CompileAsyncQuery((SwitchContext c) => c.Foos);
+
+ _ = await query(context1).ToListAsync();
+ _ = await query(context1).ToListAsync();
+
+ Assert.Equal(
+ CoreStrings.CompiledQueryDifferentModel("c => c.Foos"),
+ (await Assert.ThrowsAsync(
+ () => query(context2).ToListAsync())).Message);
+
+ _ = await query(context1).ToListAsync();
+ }
+
+ private class Foo
+ {
+ public int Id { get; set; }
+ public List Bars { get; } = new();
+ }
+
+ private class Bar
+ {
+ public int Id { get; set; }
+ }
+
+ private class SwitchContext : DbContext
+ {
+ public DbSet Foos
+ => Set();
+
+ protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
+ => optionsBuilder
+ .UseInMemoryDatabase(nameof(SwitchContext))
+ .ReplaceService();
+ }
+
+ private class DegenerateCacheKeyFactory : IModelCacheKeyFactory
+ {
+ private static int _value;
+
+ public object Create(DbContext context, bool designTime)
+ => _value++;
+ }
+}