From 893759a6230b2d49e3aac4a745d7444693c166d3 Mon Sep 17 00:00:00 2001 From: Arthur Vickers Date: Sun, 4 Dec 2022 18:52:44 +0000 Subject: [PATCH] Throw if CompiledQuery is used with different models Fixes #13483 --- src/EFCore/Properties/CoreStrings.Designer.cs | 8 ++ src/EFCore/Properties/CoreStrings.resx | 3 + .../Query/Internal/CompiledQueryBase.cs | 28 +++++-- test/EFCore.Tests/Query/CompiledQueryTest.cs | 76 +++++++++++++++++++ 4 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 test/EFCore.Tests/Query/CompiledQueryTest.cs 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++; + } +}