Skip to content

Commit

Permalink
Throw if CompiledQuery is used with different models
Browse files Browse the repository at this point in the history
Fixes #13483
  • Loading branch information
ajcvickers committed Dec 4, 2022
1 parent f7414ce commit fefd81d
Show file tree
Hide file tree
Showing 4 changed files with 111 additions and 10 deletions.
8 changes: 8 additions & 0 deletions src/EFCore/Properties/CoreStrings.Designer.cs

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions src/EFCore/Properties/CoreStrings.resx
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,9 @@
<data name="ComparerPropertyMismatch" xml:space="preserve">
<value>The comparer for type '{type}' cannot be used for '{entityType}.{propertyName}' because its type is '{propertyType}'.</value>
</data>
<data name="CompiledQueryDifferentModel" xml:space="preserve">
<value>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.</value>
</data>
<data name="CompositeFkOnProperty" xml:space="preserve">
<value>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.</value>
</data>
Expand Down
28 changes: 23 additions & 5 deletions src/EFCore/Query/Internal/CompiledQueryBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,19 @@ public abstract class CompiledQueryBase<TContext, TResult>
{
private readonly LambdaExpression _queryExpression;

private Func<QueryContext, TResult>? _executor;
private ExecutorAndModel? _executor;

private sealed class ExecutorAndModel
{
public ExecutorAndModel(Func<QueryContext, TResult> executor, IModel model)
{
Executor = executor;
Model = model;
}

public Func<QueryContext, TResult> Executor { get; }
public IModel Model { get; }
}

/// <summary>
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
Expand Down Expand Up @@ -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<IQueryContextFactory>();
var queryContext = queryContextFactory.Create();

Expand All @@ -64,7 +82,7 @@ protected virtual TResult ExecuteCore(
parameters[i]);
}

return executor(queryContext);
return _executor.Executor(queryContext);
}

/// <summary>
Expand All @@ -77,7 +95,7 @@ protected abstract Func<QueryContext, TResult> CreateCompiledQuery(
IQueryCompiler queryCompiler,
Expression expression);

private Func<QueryContext, TResult> EnsureExecutor(TContext context)
private void EnsureExecutor(TContext context)
=> NonCapturingLazyInitializer.EnsureInitialized(
ref _executor,
this,
Expand All @@ -88,7 +106,7 @@ private Func<QueryContext, TResult> EnsureExecutor(TContext context)
var queryCompiler = c.GetService<IQueryCompiler>();
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
Expand Down
82 changes: 77 additions & 5 deletions test/EFCore.Tests/Query/ExpressionPrinterTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,78 @@

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<InvalidOperationException>(
() => 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<InvalidOperationException>(
() => query(context2).ToListAsync())).Message);

_ = await query(context1).ToListAsync();
}

private class Foo
{
public int Id { get; set; }
public List<Bar> Bars { get; } = new();
}

private class Bar
{
public int Id { get; set; }
}

private class SwitchContext : DbContext
{
public DbSet<Foo> Foos
=> Set<Foo>();

protected internal override void OnConfiguring(DbContextOptionsBuilder optionsBuilder)
=> optionsBuilder
.UseInMemoryDatabase(nameof(SwitchContext))
.ReplaceService<IModelCacheKeyFactory, DegenerateCacheKeyFactory>();
}

private class DegenerateCacheKeyFactory : IModelCacheKeyFactory
{
private static int _value;

public object Create(DbContext context, bool designTime)
=> _value++;
}
}

public class ExpressionPrinterTest
{
private readonly ExpressionPrinter _expressionPrinter = new();
Expand Down Expand Up @@ -148,7 +220,7 @@ public void Complex_MethodCall_printed_correctly()
=> Assert.Equal(
"\"Foobar\""
+ @".Substring(
startIndex: 0,
startIndex: 0,
length: 4)",
_expressionPrinter.Print(
Expression.Call(
Expand All @@ -166,10 +238,10 @@ public void Linq_methods_printed_as_extensions()

Assert.Equal(
@"new int[]
{
1,
2,
3
{
1,
2,
3
}
.AsQueryable()
.Select(x => x.ToString())
Expand Down

0 comments on commit fefd81d

Please sign in to comment.