From 4c489b5371f28cd0d66c15fbfe2e7086ef904f25 Mon Sep 17 00:00:00 2001 From: Smit Patel Date: Wed, 21 Aug 2019 15:53:13 -0700 Subject: [PATCH] InMemory: Avoid N+1 queries for Include Don't send shapers to ServerQueryExpression Laying ground work for collection projection Part of #16963 --- ...yExpressionTranslatingExpressionVisitor.cs | 2 +- ...emoryProjectionBindingExpressionVisitor.cs | 14 +- ...nMemoryQueryExpression.ResultEnumerable.cs | 64 ++++++ .../Query/Internal/InMemoryQueryExpression.cs | 185 +++++++++--------- ...yableMethodTranslatingExpressionVisitor.cs | 2 +- ....CustomShaperCompilingExpressionVisitor.cs | 28 +-- ...jectionBindingRemovingExpressionVisitor.cs | 32 ++- ...ryShapedQueryCompilingExpressionVisitor.cs | 7 +- ...erExpressionProcessingExpressionVisitor.cs | 51 +++-- ....CustomShaperCompilingExpressionVisitor.cs | 3 +- 10 files changed, 235 insertions(+), 153 deletions(-) create mode 100644 src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.ResultEnumerable.cs diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs index 5502e01fe93..e9032f3c5ff 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs @@ -144,7 +144,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp return null; } - subquery.ApplyServerProjection(); + subquery.ApplyProjection(); if (subquery.Projection.Count != 1) { return null; diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryProjectionBindingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryProjectionBindingExpressionVisitor.cs index d3978ca5590..e7e1c543e5e 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryProjectionBindingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryProjectionBindingExpressionVisitor.cs @@ -89,10 +89,16 @@ public override Expression Visit(Expression expression) var translated = _queryableMethodTranslatingExpressionVisitor.TranslateSubquery( materializeCollectionNavigationExpression.Subquery); - return new ProjectionBindingExpression( - _queryExpression, - _queryExpression.AddToProjection(translated), - typeof(IEnumerable<>).MakeGenericType(materializeCollectionNavigationExpression.Navigation.GetTargetType().ClrType)); + var index = _queryExpression.AddSubqueryProjection(translated, out var innerShaper); + + return new CollectionShaperExpression( + new ProjectionBindingExpression( + _queryExpression, + index, + typeof(IEnumerable)), + innerShaper, + materializeCollectionNavigationExpression.Navigation, + materializeCollectionNavigationExpression.Navigation.GetTargetType().ClrType); case MethodCallExpression methodCallExpression: { diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.ResultEnumerable.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.ResultEnumerable.cs new file mode 100644 index 00000000000..a41d40644bf --- /dev/null +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.ResultEnumerable.cs @@ -0,0 +1,64 @@ +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. + +using System; +using System.Collections; +using System.Collections.Generic; +using Microsoft.EntityFrameworkCore.Storage; + +namespace Microsoft.EntityFrameworkCore.InMemory.Query.Internal +{ + public partial class InMemoryQueryExpression + { + private sealed class ResultEnumerable : IEnumerable + { + private readonly Func _getElement; + + public ResultEnumerable(Func getElement) + { + _getElement = getElement; + } + + public IEnumerator GetEnumerator() => new ResultEnumerator(_getElement()); + + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + private sealed class ResultEnumerator : IEnumerator + { + private readonly ValueBuffer _value; + private bool _moved; + + public ResultEnumerator(ValueBuffer value) + { + _value = value; + _moved = _value.IsEmpty; + } + + public bool MoveNext() + { + if (!_moved) + { + _moved = true; + + return _moved; + } + + return false; + } + + public void Reset() + { + _moved = false; + } + + object IEnumerator.Current => Current; + + public ValueBuffer Current => !_moved ? ValueBuffer.Empty : _value; + + void IDisposable.Dispose() + { + } + } + } + } +} diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs index 7f95e137800..a9942db5a81 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs @@ -2,7 +2,6 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; -using System.Collections; using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; @@ -14,70 +13,24 @@ namespace Microsoft.EntityFrameworkCore.InMemory.Query.Internal { - public class InMemoryQueryExpression : Expression + public partial class InMemoryQueryExpression : Expression { private static readonly ConstructorInfo _valueBufferConstructor = typeof(ValueBuffer).GetConstructors().Single(ci => ci.GetParameters().Length == 1); + private static readonly PropertyInfo _valueBufferCountMemberInfo + = typeof(ValueBuffer).GetTypeInfo().GetProperty(nameof(ValueBuffer.Count)); private readonly List _valueBufferSlots = new List(); - private IDictionary _projectionMapping = new Dictionary(); - - public virtual IReadOnlyList Projection => _valueBufferSlots; private readonly IDictionary> _entityProjectionCache = new Dictionary>(); - private sealed class ResultEnumerable : IEnumerable - { - private readonly Func _getElement; - - public ResultEnumerable(Func getElement) - { - _getElement = getElement; - } - - public IEnumerator GetEnumerator() => new ResultEnumerator(_getElement()); - - IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); - - private sealed class ResultEnumerator : IEnumerator - { - private readonly ValueBuffer _value; - private bool _moved; - - public ResultEnumerator(ValueBuffer value) - { - _value = value; - _moved = _value.IsEmpty; - } - - public bool MoveNext() - { - if (!_moved) - { - _moved = true; - - return _moved; - } - - return false; - } - - public void Reset() - { - _moved = false; - } - - object IEnumerator.Current => Current; - - public ValueBuffer Current => !_moved ? ValueBuffer.Empty : _value; - - void IDisposable.Dispose() - { - } - } - } + private IDictionary _projectionMapping = new Dictionary(); - private static readonly PropertyInfo _valueBufferCountMemberInfo = typeof(ValueBuffer).GetTypeInfo().GetProperty(nameof(ValueBuffer.Count)); + public virtual IReadOnlyList Projection => _valueBufferSlots; + public virtual Expression ServerQueryExpression { get; set; } + public virtual ParameterExpression ValueBufferParameter { get; } + public override Type Type => typeof(IEnumerable); + public sealed override ExpressionType NodeType => ExpressionType.Extension; public InMemoryQueryExpression(IEntityType entityType) { @@ -174,37 +127,87 @@ public virtual int AddToProjection(Expression expression) return _valueBufferSlots.Count - 1; } + public virtual int AddSubqueryProjection(ShapedQueryExpression shapedQueryExpression, out Expression innerShaper) + { + var subquery = (InMemoryQueryExpression)shapedQueryExpression.QueryExpression; + subquery.ApplyProjection(); + + innerShaper = new ShaperRemappingExpressionVisitor(subquery._projectionMapping) + .Visit(shapedQueryExpression.ShaperExpression); + + innerShaper = Lambda(innerShaper, subquery.ValueBufferParameter); + + return AddToProjection(subquery.ServerQueryExpression); + } + + private class ShaperRemappingExpressionVisitor : ExpressionVisitor + { + private readonly IDictionary _projectionMapping; + + public ShaperRemappingExpressionVisitor(IDictionary projectionMapping) + { + _projectionMapping = projectionMapping; + } + + public override Expression Visit(Expression expression) + { + if (expression is ProjectionBindingExpression projectionBindingExpression + && projectionBindingExpression.ProjectionMember != null) + { + var mappingValue = ((ConstantExpression)_projectionMapping[projectionBindingExpression.ProjectionMember]).Value; + if (mappingValue is IDictionary indexMap) + { + return new ProjectionBindingExpression(projectionBindingExpression.QueryExpression, indexMap); + } + else if (mappingValue is int index) + { + return new ProjectionBindingExpression( + projectionBindingExpression.QueryExpression, index, projectionBindingExpression.Type); + } + else + { + throw new InvalidOperationException("Invalid ProjectionMapping."); + } + } + + return base.Visit(expression); + } + } + private IEnumerable GetAllPropertiesInHierarchy(IEntityType entityType) => entityType.GetTypesInHierarchy().SelectMany(EntityTypeExtensions.GetDeclaredProperties); public virtual Expression GetMappedProjection(ProjectionMember member) => _projectionMapping[member]; - public virtual void ApplyPendingSelector() + public virtual void PushdownIntoSubquery() { var clientProjection = _valueBufferSlots.Count != 0; - var result = new Dictionary(); - foreach (var keyValuePair in _projectionMapping) + if (!clientProjection) { - if (keyValuePair.Value is EntityProjectionExpression entityProjection) + var result = new Dictionary(); + foreach (var keyValuePair in _projectionMapping) { - var map = new Dictionary(); - foreach (var property in GetAllPropertiesInHierarchy(entityProjection.EntityType)) + if (keyValuePair.Value is EntityProjectionExpression entityProjection) { - var index = AddToProjection(entityProjection.BindProperty(property)); - map[property] = CreateReadValueExpression(property.ClrType, index, property); + var map = new Dictionary(); + foreach (var property in GetAllPropertiesInHierarchy(entityProjection.EntityType)) + { + var index = AddToProjection(entityProjection.BindProperty(property)); + map[property] = CreateReadValueExpression(property.ClrType, index, property); + } + result[keyValuePair.Key] = new EntityProjectionExpression(entityProjection.EntityType, map); + } + else + { + var index = AddToProjection(keyValuePair.Value); + result[keyValuePair.Key] = CreateReadValueExpression( + keyValuePair.Value.Type, index, InferPropertyFromInner(keyValuePair.Value)); } - result[keyValuePair.Key] = new EntityProjectionExpression(entityProjection.EntityType, map); - } - else - { - var index = AddToProjection(keyValuePair.Value); - result[keyValuePair.Key] = CreateReadValueExpression( - keyValuePair.Value.Type, index, InferPropertyFromInner(keyValuePair.Value)); } - } - _projectionMapping = result; + _projectionMapping = result; + } var selectorLambda = Lambda( New( @@ -248,27 +251,30 @@ private IPropertyBase InferPropertyFromInner(Expression expression) return null; } - public virtual void ApplyServerProjection() + public virtual void ApplyProjection() { - var result = new Dictionary(); - foreach (var keyValuePair in _projectionMapping) + if (_valueBufferSlots.Count == 0) { - if (keyValuePair.Value is EntityProjectionExpression entityProjection) + var result = new Dictionary(); + foreach (var keyValuePair in _projectionMapping) { - var map = new Dictionary(); - foreach (var property in GetAllPropertiesInHierarchy(entityProjection.EntityType)) + if (keyValuePair.Value is EntityProjectionExpression entityProjection) { - map[property] = AddToProjection(entityProjection.BindProperty(property)); + var map = new Dictionary(); + foreach (var property in GetAllPropertiesInHierarchy(entityProjection.EntityType)) + { + map[property] = AddToProjection(entityProjection.BindProperty(property)); + } + result[keyValuePair.Key] = Constant(map); + } + else + { + result[keyValuePair.Key] = Constant(AddToProjection(keyValuePair.Value)); } - result[keyValuePair.Key] = Constant(map); - } - else - { - result[keyValuePair.Key] = Constant(AddToProjection(keyValuePair.Value)); } - } - _projectionMapping = result; + _projectionMapping = result; + } var selectorLambda = Lambda( New( @@ -286,11 +292,6 @@ public virtual void ApplyServerProjection() selectorLambda); } - public virtual Expression ServerQueryExpression { get; set; } - public virtual ParameterExpression ValueBufferParameter { get; } - public override Type Type => typeof(IEnumerable); - public sealed override ExpressionType NodeType => ExpressionType.Extension; - private Expression CreateReadValueExpression( Type type, int index, diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs index 46392f752cb..d20142dec52 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs @@ -189,7 +189,7 @@ protected override ShapedQueryExpression TranslateDistinct(ShapedQueryExpression { var inMemoryQueryExpression = (InMemoryQueryExpression)source.QueryExpression; - inMemoryQueryExpression.ApplyPendingSelector(); + inMemoryQueryExpression.PushdownIntoSubquery(); inMemoryQueryExpression.ServerQueryExpression = Expression.Call( InMemoryLinqOperatorProvider.Distinct.MakeGenericMethod(typeof(ValueBuffer)), diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs index b62b854557c..0d287ebeae0 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs @@ -9,6 +9,7 @@ using Microsoft.EntityFrameworkCore.Infrastructure; using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Query; +using Microsoft.EntityFrameworkCore.Storage; namespace Microsoft.EntityFrameworkCore.InMemory.Query.Internal { @@ -72,8 +73,9 @@ private static void IncludeReference private static void IncludeCollection( QueryContext queryContext, + IEnumerable innerValueBuffers, + Func innerShaper, TEntity entity, - IEnumerable relatedEntities, INavigation navigation, INavigation inverseNavigation, Action fixup, @@ -89,16 +91,18 @@ private static void IncludeCollection> _materializationContextBindings - = new Dictionary>(); - - public InMemoryProjectionBindingRemovingExpressionVisitor(InMemoryQueryExpression queryExpression) - { - _queryExpression = queryExpression; - } + private readonly IDictionary IndexMap, ParameterExpression valueBuffer)> + _materializationContextBindings + = new Dictionary IndexMap, ParameterExpression valueBuffer)>(); protected override Expression VisitBinary(BinaryExpression binaryExpression) { @@ -32,9 +26,11 @@ protected override Expression VisitBinary(BinaryExpression binaryExpression) var newExpression = (NewExpression)binaryExpression.Right; var projectionBindingExpression = (ProjectionBindingExpression)newExpression.Arguments[0]; + var queryExpression = (InMemoryQueryExpression)projectionBindingExpression.QueryExpression; _materializationContextBindings[parameterExpression] - = (IDictionary)GetProjectionIndex(projectionBindingExpression); + = ((IDictionary)GetProjectionIndex(queryExpression, projectionBindingExpression), + ((InMemoryQueryExpression)projectionBindingExpression.QueryExpression).ValueBufferParameter); var updatedExpression = Expression.New( newExpression.Constructor, @@ -53,13 +49,13 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp && methodCallExpression.Method.GetGenericMethodDefinition() == EntityMaterializerSource.TryReadValueMethod) { var property = (IProperty)((ConstantExpression)methodCallExpression.Arguments[2]).Value; - var indexMap = + var (indexMap, valueBuffer) = _materializationContextBindings[ (ParameterExpression)((MethodCallExpression)methodCallExpression.Arguments[0]).Object]; return Expression.Call( methodCallExpression.Method, - _queryExpression.ValueBufferParameter, + valueBuffer, Expression.Constant(indexMap[property]), methodCallExpression.Arguments[2]); } @@ -71,13 +67,15 @@ protected override Expression VisitExtension(Expression extensionExpression) { if (extensionExpression is ProjectionBindingExpression projectionBindingExpression) { - var projectionIndex = (int)GetProjectionIndex(projectionBindingExpression); + var queryExpression = (InMemoryQueryExpression)projectionBindingExpression.QueryExpression; + var projectionIndex = (int)GetProjectionIndex(queryExpression, projectionBindingExpression); + var valueBuffer = queryExpression.ValueBufferParameter; return Expression.Call( EntityMaterializerSource.TryReadValueMethod.MakeGenericMethod(projectionBindingExpression.Type), - _queryExpression.ValueBufferParameter, + valueBuffer, Expression.Constant(projectionIndex), - Expression.Constant(InferPropertyFromInner(_queryExpression.Projection[projectionIndex]), typeof(IPropertyBase))); + Expression.Constant(InferPropertyFromInner(queryExpression.Projection[projectionIndex]), typeof(IPropertyBase))); } return base.VisitExtension(extensionExpression); @@ -95,10 +93,10 @@ private IPropertyBase InferPropertyFromInner(Expression expression) return null; } - private object GetProjectionIndex(ProjectionBindingExpression projectionBindingExpression) + private object GetProjectionIndex(InMemoryQueryExpression queryExpression, ProjectionBindingExpression projectionBindingExpression) { return projectionBindingExpression.ProjectionMember != null - ? ((ConstantExpression)_queryExpression.GetMappedProjection(projectionBindingExpression.ProjectionMember)).Value + ? ((ConstantExpression)queryExpression.GetMappedProjection(projectionBindingExpression.ProjectionMember)).Value : (projectionBindingExpression.Index != null ? (object)projectionBindingExpression.Index : projectionBindingExpression.IndexMap); diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.cs index 8c0a6c587c9..1cdaa15668c 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryShapedQueryCompilingExpressionVisitor.cs @@ -32,7 +32,7 @@ protected override Expression VisitExtension(Expression extensionExpression) switch (extensionExpression) { case InMemoryQueryExpression inMemoryQueryExpression: - inMemoryQueryExpression.ApplyServerProjection(); + inMemoryQueryExpression.ApplyProjection(); return Visit(inMemoryQueryExpression.ServerQueryExpression); case InMemoryTableExpression inMemoryTableExpression: @@ -49,14 +49,15 @@ protected override Expression VisitShapedQueryExpression(ShapedQueryExpression s { var inMemoryQueryExpression = (InMemoryQueryExpression)shapedQueryExpression.QueryExpression; - var shaper = new ShaperExpressionProcessingExpressionVisitor(inMemoryQueryExpression) + var shaper = new ShaperExpressionProcessingExpressionVisitor( + inMemoryQueryExpression, inMemoryQueryExpression.ValueBufferParameter) .Inject(shapedQueryExpression.ShaperExpression); shaper = InjectEntityMaterializers(shaper); var innerEnumerable = Visit(inMemoryQueryExpression); - shaper = new InMemoryProjectionBindingRemovingExpressionVisitor(inMemoryQueryExpression).Visit(shaper); + shaper = new InMemoryProjectionBindingRemovingExpressionVisitor().Visit(shaper); shaper = new CustomShaperCompilingExpressionVisitor(IsTracking).Visit(shaper); diff --git a/src/EFCore.InMemory/Query/Internal/ShaperExpressionProcessingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/ShaperExpressionProcessingExpressionVisitor.cs index 000bf5d72dc..619ecb76edf 100644 --- a/src/EFCore.InMemory/Query/Internal/ShaperExpressionProcessingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/ShaperExpressionProcessingExpressionVisitor.cs @@ -4,6 +4,7 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; +using JetBrains.Annotations; using Microsoft.EntityFrameworkCore.Query; namespace Microsoft.EntityFrameworkCore.InMemory.Query.Internal @@ -11,40 +12,33 @@ namespace Microsoft.EntityFrameworkCore.InMemory.Query.Internal public class ShaperExpressionProcessingExpressionVisitor : ExpressionVisitor { private readonly InMemoryQueryExpression _queryExpression; + private readonly ParameterExpression _valueBufferParameter; private readonly IDictionary _mapping = new Dictionary(); private readonly List _variables = new List(); private readonly List _expressions = new List(); public ShaperExpressionProcessingExpressionVisitor( - InMemoryQueryExpression queryExpression) + [CanBeNull] InMemoryQueryExpression queryExpression, [NotNull] ParameterExpression valueBufferParameter) { _queryExpression = queryExpression; + _valueBufferParameter = valueBufferParameter; } public virtual Expression Inject(Expression expression) { var result = Visit(expression); + _expressions.Add(result); + result = Expression.Block(_variables, _expressions); - if (_expressions.All(e => e.NodeType == ExpressionType.Assign)) - { - result = new ReplacingExpressionVisitor(_expressions.Cast() - .ToDictionary(e => e.Left, e => e.Right)).Visit(result); - } - else - { - _expressions.Add(result); - result = Expression.Block(_variables, _expressions); - } - - return ConvertToLambda(result, Expression.Parameter(result.Type, "result")); + return ConvertToLambda(result); } - private LambdaExpression ConvertToLambda(Expression result, ParameterExpression resultParameter) + private LambdaExpression ConvertToLambda(Expression result) => Expression.Lambda( result, QueryCompilationContext.QueryContextParameter, - _queryExpression.ValueBufferParameter); + _valueBufferParameter); protected override Expression VisitExtension(Expression extensionExpression) { @@ -81,10 +75,26 @@ protected override Expression VisitExtension(Expression extensionExpression) case IncludeExpression includeExpression: { var entity = Visit(includeExpression.EntityExpression); - _expressions.Add( - includeExpression.Update( - entity, - Visit(includeExpression.NavigationExpression))); + if (includeExpression.NavigationExpression is CollectionShaperExpression collectionShaperExpression) + { + var innerLambda = (LambdaExpression)collectionShaperExpression.InnerShaper; + var innerShaper = new ShaperExpressionProcessingExpressionVisitor(null, innerLambda.Parameters[0]) + .Inject(innerLambda.Body); + + _expressions.Add( + includeExpression.Update( + entity, + collectionShaperExpression.Update( + Visit(collectionShaperExpression.Projection), + innerShaper))); + } + else + { + _expressions.Add( + includeExpression.Update( + entity, + Visit(includeExpression.NavigationExpression))); + } return entity; } @@ -94,7 +104,8 @@ protected override Expression VisitExtension(Expression extensionExpression) } private Expression GenerateKey(ProjectionBindingExpression projectionBindingExpression) - => projectionBindingExpression.ProjectionMember != null + => _queryExpression != null + && projectionBindingExpression.ProjectionMember != null ? _queryExpression.GetMappedProjection(projectionBindingExpression.ProjectionMember) : projectionBindingExpression; } diff --git a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs index e1fc08df398..3267afe9093 100644 --- a/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalShapedQueryCompilingExpressionVisitor.CustomShaperCompilingExpressionVisitor.cs @@ -216,8 +216,7 @@ void processCurrentElementRow() if (!trackingQuery) { fixup(entity, relatedEntity); - if (inverseNavigation != null - && !inverseNavigation.IsCollection()) + if (inverseNavigation != null) { SetIsLoadedNoTracking(relatedEntity, inverseNavigation); }