Skip to content

Commit a5210e3

Browse files
committed
Query: Dedupe & simplify materializer
1 parent e174319 commit a5210e3

File tree

9 files changed

+226
-104
lines changed

9 files changed

+226
-104
lines changed

src/EFCore.InMemory/Extensions/InMemoryServiceCollectionExtensions.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ public static IServiceCollection AddEntityFrameworkInMemoryDatabase([NotNull] th
7777
.TryAdd<IShapedQueryCompilingExpressionVisitorFactory, InMemoryShapedQueryCompilingExpressionVisitorFactory>()
7878
.TryAdd<IQueryableMethodTranslatingExpressionVisitorFactory, InMemoryQueryableMethodTranslatingExpressionVisitorFactory>()
7979
.TryAdd<IEntityQueryableTranslatorFactory, InMemoryEntityQueryableTranslatorFactory>()
80+
.TryAdd<IShapedQueryOptimizerFactory, InMemoryShapedQueryOptimizerFactory>()
8081

8182

8283
.TryAdd<ISingletonOptions, IInMemorySingletonOptions>(p => p.GetService<IInMemorySingletonOptions>())

src/EFCore.InMemory/Query/PipeLine/InMemoryQueryExpression.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,8 @@ public void ApplyProjection(IDictionary<ProjectionMember, Expression> projection
117117
{
118118
foreach (var property in entityValuesExpression.EntityType.GetProperties())
119119
{
120-
_valueBufferSlots.Add(CreateReadValueExpression(property.ClrType, currentIndex + property.GetIndex(), property));
120+
_valueBufferSlots.Add(
121+
CreateReadValueExpression(property.ClrType, entityValuesExpression.StartIndex + property.GetIndex(), property));
121122
}
122123

123124
_projectionMapping[member] = new EntityProjectionExpression(entityValuesExpression.EntityType, currentIndex);
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System.Linq.Expressions;
5+
using Microsoft.EntityFrameworkCore.Query.Pipeline;
6+
7+
namespace Microsoft.EntityFrameworkCore.InMemory.Query.Pipeline
8+
{
9+
public class InMemoryShapedQueryOptimizer : ShapedQueryOptimizer
10+
{
11+
public override Expression Visit(Expression query)
12+
{
13+
query = base.Visit(query);
14+
15+
return query;
16+
}
17+
}
18+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using Microsoft.EntityFrameworkCore.Query.Pipeline;
5+
6+
namespace Microsoft.EntityFrameworkCore.InMemory.Query.Pipeline
7+
{
8+
public class InMemoryShapedQueryOptimizerFactory : ShapedQueryOptimizerFactory
9+
{
10+
public override ShapedQueryOptimizer Create(QueryCompilationContext2 queryCompilationContext)
11+
{
12+
return new InMemoryShapedQueryOptimizer();
13+
}
14+
}
15+
}

src/EFCore.Relational/Query/PipeLine/RelationalShapedQueryOptimizingExpressionVisitors.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ public RelationalShapedQueryOptimizer(QueryCompilationContext2 queryCompilationC
1818
public override Expression Visit(Expression query)
1919
{
2020
query = base.Visit(query);
21+
query = new ShaperExpressionDedupingExpressionVisitor().Process(query);
2122
query = new SelectExpressionProjectionApplyingExpressionVisitor().Visit(query);
2223
query = new SelectExpressionTableAliasUniquifyingExpressionVisitor().Visit(query);
2324
query = new NullComparisonTransformingExpressionVisitor().Visit(query);
Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
using System;
5+
using System.Collections.Generic;
6+
using System.Linq.Expressions;
7+
using Microsoft.EntityFrameworkCore.Query.Pipeline;
8+
using Microsoft.EntityFrameworkCore.Relational.Query.Pipeline.SqlExpressions;
9+
10+
namespace Microsoft.EntityFrameworkCore.Relational.Query.Pipeline
11+
{
12+
public class ShaperExpressionDedupingExpressionVisitor : ExpressionVisitor
13+
{
14+
private SelectExpression _selectExpression;
15+
private IDictionary<Expression, IList<Expression>> _duplicateShapers;
16+
17+
public Expression Process(Expression expression)
18+
{
19+
if (expression is ShapedQueryExpression shapedQueryExpression)
20+
{
21+
_selectExpression = (SelectExpression)shapedQueryExpression.QueryExpression;
22+
_duplicateShapers = new Dictionary<Expression, IList<Expression>>();
23+
Visit(shapedQueryExpression.ShaperExpression);
24+
25+
var variables = new List<ParameterExpression>();
26+
var expressions = new List<Expression>();
27+
var replacements = new Dictionary<Expression, Expression>();
28+
var index = 0;
29+
30+
foreach (var kvp in _duplicateShapers)
31+
{
32+
if (kvp.Value.Count > 1)
33+
{
34+
var firstShaper = kvp.Value[0];
35+
var entityParameter = Expression.Parameter(firstShaper.Type, $"entity{index++}");
36+
variables.Add(entityParameter);
37+
expressions.Add(Expression.Assign(
38+
entityParameter,
39+
firstShaper));
40+
41+
foreach (var shaper in kvp.Value)
42+
{
43+
replacements[shaper] = entityParameter;
44+
}
45+
}
46+
}
47+
48+
if (variables.Count == 0)
49+
{
50+
return shapedQueryExpression;
51+
}
52+
53+
expressions.Add(new ReplacingExpressionVisitor(replacements)
54+
.Visit(shapedQueryExpression.ShaperExpression));
55+
56+
shapedQueryExpression.ShaperExpression = Expression.Block(variables, expressions);
57+
58+
return shapedQueryExpression;
59+
}
60+
61+
return expression;
62+
}
63+
64+
protected override Expression VisitExtension(Expression extensionExpression)
65+
{
66+
if (extensionExpression is EntityShaperExpression entityShaperExpression)
67+
{
68+
var serverProjection = _selectExpression.GetProjectionExpression(
69+
entityShaperExpression.ValueBufferExpression.ProjectionMember);
70+
71+
if (_duplicateShapers.ContainsKey(serverProjection))
72+
{
73+
_duplicateShapers[serverProjection].Add(entityShaperExpression);
74+
}
75+
else
76+
{
77+
_duplicateShapers[serverProjection] = new List<Expression> { entityShaperExpression };
78+
}
79+
80+
return entityShaperExpression;
81+
}
82+
83+
return base.VisitExtension(extensionExpression);
84+
}
85+
}
86+
}

src/EFCore/Query/PipeLine/ProjectionBindingExpression.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,11 @@ public ProjectionBindingExpression(ProjectionMember projectionMember, Type type)
2020
public override Type Type { get; }
2121
public override ExpressionType NodeType => ExpressionType.Extension;
2222

23+
protected override Expression VisitChildren(ExpressionVisitor visitor)
24+
{
25+
return this;
26+
}
27+
2328
public void Print(ExpressionPrinter expressionPrinter)
2429
{
2530
expressionPrinter.StringBuilder.Append(nameof(ProjectionBindingExpression) + ": " + ProjectionMember);

src/EFCore/Query/PipeLine/ShapedQueryExpressionVisitor.cs

Lines changed: 80 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -146,9 +146,6 @@ protected virtual Expression InjectEntityMaterializer(Expression expression)
146146

147147
private class EntityMaterializerInjectingExpressionVisitor : ExpressionVisitor
148148
{
149-
private readonly IDictionary<EntityShaperExpression, ParameterExpression> _entityCache
150-
= new Dictionary<EntityShaperExpression, ParameterExpression>();
151-
152149
private static readonly ConstructorInfo _materializationContextConstructor
153150
= typeof(MaterializationContext).GetConstructors().Single(ci => ci.GetParameters().Length == 2);
154151

@@ -187,23 +184,10 @@ public EntityMaterializerInjectingExpressionVisitor(
187184
public Expression Inject(Expression expression)
188185
{
189186
var modifiedBody = Visit(expression);
190-
if (_async)
191-
{
192-
var resultVariable = Expression.Variable(typeof(Task<>).MakeGenericType(expression.Type), "result");
193-
_variables.Add(resultVariable);
194-
_expressions.Add(Expression.Assign(resultVariable,
195-
Expression.Call(
196-
_taskFromResultMethodInfo.MakeGenericMethod(expression.Type),
197-
modifiedBody)));
198-
_expressions.Add(resultVariable);
199-
}
200-
else
201-
{
202-
var resultVariable = Expression.Variable(expression.Type, "result");
203-
_variables.Add(resultVariable);
204-
_expressions.Add(Expression.Assign(resultVariable, modifiedBody));
205-
_expressions.Add(resultVariable);
206-
}
187+
_expressions.Add(
188+
_async
189+
? Expression.Call(_taskFromResultMethodInfo.MakeGenericMethod(expression.Type), modifiedBody)
190+
: modifiedBody);
207191

208192
return Expression.Block(_variables, _expressions);
209193
}
@@ -212,89 +196,7 @@ protected override Expression VisitExtension(Expression extensionExpression)
212196
{
213197
if (extensionExpression is EntityShaperExpression entityShaperExpression)
214198
{
215-
if (_entityCache.TryGetValue(entityShaperExpression, out var existingInstance))
216-
{
217-
return existingInstance;
218-
}
219-
220-
_currentEntityIndex++;
221-
var entityType = entityShaperExpression.EntityType;
222-
var valueBuffer = entityShaperExpression.ValueBufferExpression;
223-
var primaryKey = entityType.FindPrimaryKey();
224-
225-
if (_trackQueryResults && primaryKey == null)
226-
{
227-
throw new InvalidOperationException();
228-
}
229-
230-
var result = Expression.Variable(entityType.ClrType, "result" + _currentEntityIndex);
231-
_variables.Add(result);
232-
233-
if (_trackQueryResults)
234-
{
235-
var entry = Expression.Variable(typeof(InternalEntityEntry), "entry" + _currentEntityIndex);
236-
var hasNullKey = Expression.Variable(typeof(bool), "hasNullKey" + _currentEntityIndex);
237-
_variables.Add(entry);
238-
_variables.Add(hasNullKey);
239-
240-
_expressions.Add(
241-
Expression.Assign(
242-
entry,
243-
Expression.Call(
244-
Expression.MakeMemberAccess(
245-
QueryCompilationContext2.QueryContextParameter,
246-
_stateManagerMemberInfo),
247-
_tryGetEntryMethodInfo,
248-
Expression.Constant(primaryKey),
249-
Expression.NewArrayInit(
250-
typeof(object),
251-
primaryKey.Properties
252-
.Select(p => _entityMaterializerSource.CreateReadValueExpression(
253-
entityShaperExpression.ValueBufferExpression,
254-
typeof(object),
255-
p.GetIndex(),
256-
p))),
257-
Expression.Constant(!entityShaperExpression.Nullable),
258-
hasNullKey)));
259-
260-
_expressions.Add(
261-
Expression.Assign(
262-
result,
263-
Expression.Condition(
264-
hasNullKey,
265-
Expression.Constant(null, entityType.ClrType),
266-
Expression.Condition(
267-
Expression.NotEqual(
268-
entry,
269-
Expression.Constant(default(InternalEntityEntry), typeof(InternalEntityEntry))),
270-
Expression.Convert(
271-
Expression.MakeMemberAccess(entry, _entityMemberInfo),
272-
entityType.ClrType),
273-
MaterializeEntity(entityType, valueBuffer)))));
274-
}
275-
else
276-
{
277-
_expressions.Add(
278-
Expression.Assign(
279-
result,
280-
Expression.Condition(
281-
primaryKey.Properties
282-
.Select(p =>
283-
Expression.Equal(
284-
_entityMaterializerSource.CreateReadValueExpression(
285-
entityShaperExpression.ValueBufferExpression,
286-
typeof(object),
287-
p.GetIndex(),
288-
p),
289-
Expression.Constant(null)))
290-
.Aggregate((a, b) => Expression.OrElse(a, b)),
291-
Expression.Constant(null, entityType.ClrType),
292-
MaterializeEntity(entityType, valueBuffer))));
293-
}
294-
295-
_entityCache[entityShaperExpression] = result;
296-
297-
return result;
199+
return ProcessEntityShaper(entityShaperExpression);
298200
}
299201

300202
if (extensionExpression is EntityValuesExpression entityValuesExpression)
@@ -330,6 +232,81 @@ protected override Expression VisitExtension(Expression extensionExpression)
330232
return base.VisitExtension(extensionExpression);
331233
}
332234

235+
private Expression ProcessEntityShaper(EntityShaperExpression entityShaperExpression)
236+
{
237+
_currentEntityIndex++;
238+
var expressions = new List<Expression>();
239+
var variables = new List<ParameterExpression>();
240+
241+
var entityType = entityShaperExpression.EntityType;
242+
var valueBuffer = entityShaperExpression.ValueBufferExpression;
243+
244+
var primaryKey = entityType.FindPrimaryKey();
245+
246+
if (_trackQueryResults && primaryKey == null)
247+
{
248+
throw new InvalidOperationException();
249+
}
250+
251+
if (_trackQueryResults)
252+
{
253+
var entry = Expression.Variable(typeof(InternalEntityEntry), "entry" + _currentEntityIndex);
254+
var hasNullKey = Expression.Variable(typeof(bool), "hasNullKey" + _currentEntityIndex);
255+
variables.Add(entry);
256+
variables.Add(hasNullKey);
257+
258+
expressions.Add(
259+
Expression.Assign(
260+
entry,
261+
Expression.Call(
262+
Expression.MakeMemberAccess(
263+
QueryCompilationContext2.QueryContextParameter,
264+
_stateManagerMemberInfo),
265+
_tryGetEntryMethodInfo,
266+
Expression.Constant(primaryKey),
267+
Expression.NewArrayInit(
268+
typeof(object),
269+
primaryKey.Properties
270+
.Select(p => _entityMaterializerSource.CreateReadValueExpression(
271+
entityShaperExpression.ValueBufferExpression,
272+
typeof(object),
273+
p.GetIndex(),
274+
p))),
275+
Expression.Constant(!entityShaperExpression.Nullable),
276+
hasNullKey)));
277+
278+
expressions.Add(Expression.Condition(
279+
hasNullKey,
280+
Expression.Constant(null, entityType.ClrType),
281+
Expression.Condition(
282+
Expression.NotEqual(
283+
entry,
284+
Expression.Constant(default(InternalEntityEntry), typeof(InternalEntityEntry))),
285+
Expression.Convert(
286+
Expression.MakeMemberAccess(entry, _entityMemberInfo),
287+
entityType.ClrType),
288+
MaterializeEntity(entityType, valueBuffer))));
289+
}
290+
else
291+
{
292+
expressions.Add(Expression.Condition(
293+
primaryKey.Properties
294+
.Select(p =>
295+
Expression.Equal(
296+
_entityMaterializerSource.CreateReadValueExpression(
297+
entityShaperExpression.ValueBufferExpression,
298+
typeof(object),
299+
p.GetIndex(),
300+
p),
301+
Expression.Constant(null)))
302+
.Aggregate((a, b) => Expression.OrElse(a, b)),
303+
Expression.Constant(null, entityType.ClrType),
304+
MaterializeEntity(entityType, valueBuffer)));
305+
}
306+
307+
return Expression.Block(variables, expressions);
308+
}
309+
333310
private Expression MaterializeEntity(IEntityType entityType, Expression valueBuffer)
334311
{
335312
var expressions = new List<Expression>();

test/EFCore.Specification.Tests/Query/SimpleQueryTestBase.JoinGroupJoin.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,24 @@ join o in os on c.CustomerID equals o.CustomerID
4848
entryCount: 919);
4949
}
5050

51+
[ConditionalTheory]
52+
[MemberData(nameof(IsAsyncData))]
53+
public virtual Task Join_customers_orders_entities_same_entity_twice(bool isAsync)
54+
{
55+
return AssertQuery<Customer, Order>(
56+
isAsync,
57+
(cs, os) =>
58+
from c in cs
59+
join o in os on c.CustomerID equals o.CustomerID
60+
select new
61+
{
62+
A = c,
63+
B = c
64+
},
65+
e => e.A.CustomerID + " " + e.B.CustomerID,
66+
entryCount: 89);
67+
}
68+
5169
[ConditionalTheory(Skip = "QueryIssue")]
5270
[MemberData(nameof(IsAsyncData))]
5371
public virtual Task Join_select_many(bool isAsync)

0 commit comments

Comments
 (0)