diff --git a/src/EFCore.InMemory/Query/Internal/EntityProjectionExpression.cs b/src/EFCore.InMemory/Query/Internal/EntityProjectionExpression.cs index 198ecb25e38..e7c02c7812c 100644 --- a/src/EFCore.InMemory/Query/Internal/EntityProjectionExpression.cs +++ b/src/EFCore.InMemory/Query/Internal/EntityProjectionExpression.cs @@ -23,7 +23,7 @@ namespace Microsoft.EntityFrameworkCore.InMemory.Query.Internal /// public class EntityProjectionExpression : Expression, IPrintableExpression { - private readonly IDictionary _readExpressionMap; + private readonly IDictionary _readExpressionMap; private readonly IDictionary _navigationExpressionsCache = new Dictionary(); @@ -36,7 +36,7 @@ private readonly IDictionary _navigationExp /// public EntityProjectionExpression( [NotNull] IEntityType entityType, - [NotNull] IDictionary readExpressionMap) + [NotNull] IDictionary readExpressionMap) { EntityType = entityType; _readExpressionMap = readExpressionMap; @@ -83,7 +83,7 @@ public virtual EntityProjectionExpression UpdateEntityType([NotNull] IEntityType derivedType.DisplayName(), EntityType.DisplayName())); } - var readExpressionMap = new Dictionary(); + var readExpressionMap = new Dictionary(); foreach (var kvp in _readExpressionMap) { var property = kvp.Key; @@ -103,7 +103,7 @@ public virtual EntityProjectionExpression UpdateEntityType([NotNull] IEntityType /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual Expression BindProperty([NotNull] IProperty property) + public virtual MethodCallExpression BindProperty([NotNull] IProperty property) { if (!EntityType.IsAssignableFrom(property.DeclaringEntityType) && !property.DeclaringEntityType.IsAssignableFrom(EntityType)) diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs index 5b89d74277c..ac72bbb530b 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs @@ -1185,15 +1185,12 @@ protected override Expression VisitUnary(UnaryExpression unaryExpression) // if the result type change was just nullability change e.g from int to int? // we want to preserve the new type for null propagation - if (result.Type != type + return result.Type != type && !(result.Type.IsNullableType() && !type.IsNullableType() - && result.Type.UnwrapNullableType() == type)) - { - result = Expression.Convert(result, type); - } - - return result; + && result.Type.UnwrapNullableType() == type) + ? Expression.Convert(result, type) + : (Expression)result; } if (entityReferenceExpression.SubqueryEntity != null) diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs index d3732d09d36..90bfa6884c4 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryExpression.cs @@ -3,6 +3,8 @@ using System; using System.Collections.Generic; +using System.Diagnostics; +using System.Dynamic; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -35,7 +37,11 @@ private static readonly ConstructorInfo _valueBufferConstructor private static readonly PropertyInfo _valueBufferCountMemberInfo = typeof(ValueBuffer).GetRequiredProperty(nameof(ValueBuffer.Count)); - private readonly List _valueBufferSlots = new(); + private static readonly MethodInfo _leftJoinMethodInfo = typeof(InMemoryQueryExpression).GetTypeInfo() + .GetDeclaredMethods(nameof(LeftJoin)).Single(mi => mi.GetParameters().Length == 6); + + private readonly List _clientProjectionExpressions = new(); + private readonly List _projectionMappingExpressions = new(); private readonly IDictionary> _entityProjectionCache = new Dictionary>(); @@ -45,6 +51,75 @@ private readonly IDictionary _projectionMapping = new Dictionary(); private ParameterExpression? _groupingParameter; + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public InMemoryQueryExpression([NotNull] IEntityType entityType) + { + _valueBufferParameter = Parameter(typeof(ValueBuffer), "valueBuffer"); + ServerQueryExpression = new InMemoryTableExpression(entityType); + var readExpressionMap = new Dictionary(); + var selectorExpressions = new List(); + foreach (var property in entityType.GetAllBaseTypesInclusive().SelectMany(et => et.GetDeclaredProperties())) + { + var propertyExpression = CreateReadValueExpression(property.ClrType, property.GetIndex(), property); + selectorExpressions.Add(propertyExpression); + + Check.DebugAssert(property.GetIndex() == selectorExpressions.Count - 1, + "Properties should be ordered in same order as their indexes."); + readExpressionMap[property] = propertyExpression; + _projectionMappingExpressions.Add(propertyExpression); + } + + var discriminatorProperty = entityType.FindDiscriminatorProperty(); + if (discriminatorProperty != null) + { + var keyValueComparer = discriminatorProperty.GetKeyValueComparer()!; + foreach (var derivedEntityType in entityType.GetDerivedTypes()) + { + var entityCheck = derivedEntityType.GetConcreteDerivedTypesInclusive() + .Select( + e => keyValueComparer.ExtractEqualsBody( + readExpressionMap[discriminatorProperty], + Constant(e.GetDiscriminatorValue(), discriminatorProperty.ClrType))) + .Aggregate((l, r) => OrElse(l, r)); + + foreach (var property in derivedEntityType.GetDeclaredProperties()) + { + var propertyExpression = Condition( + entityCheck, + CreateReadValueExpression(property.ClrType, property.GetIndex(), property), + Default(property.ClrType)); + + selectorExpressions.Add(propertyExpression); + var readExpression = CreateReadValueExpression(property.ClrType, selectorExpressions.Count - 1, property); + readExpressionMap[property] = readExpression; + _projectionMappingExpressions.Add(readExpression); + } + } + + // Force a selector if entity projection has complex expressions. + var selectorLambda = Lambda( + New( + _valueBufferConstructor, + NewArrayInit( + typeof(object), + selectorExpressions.Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : e))), + CurrentParameter); + + ServerQueryExpression = Call( + EnumerableMethods.Select.MakeGenericMethod(typeof(ValueBuffer), typeof(ValueBuffer)), + ServerQueryExpression, + selectorLambda); + } + + var entityProjection = new EntityProjectionExpression(entityType, readExpressionMap); + _projectionMapping[new ProjectionMember()] = entityProjection; + } + /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -52,7 +127,7 @@ private readonly IDictionary public virtual IReadOnlyList Projection - => _valueBufferSlots; + => _clientProjectionExpressions; /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to @@ -89,46 +164,6 @@ public override Type Type public sealed override ExpressionType NodeType => ExpressionType.Extension; - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public InMemoryQueryExpression([NotNull] IEntityType entityType) - { - _valueBufferParameter = Parameter(typeof(ValueBuffer), "valueBuffer"); - ServerQueryExpression = new InMemoryTableExpression(entityType); - var readExpressionMap = new Dictionary(); - var discriminatorProperty = entityType.FindDiscriminatorProperty(); - var keyValueComparer = discriminatorProperty?.GetKeyValueComparer(); - foreach (var property in entityType.GetAllBaseTypesInclusive().SelectMany(et => et.GetDeclaredProperties())) - { - readExpressionMap[property] = CreateReadValueExpression(property.ClrType, property.GetIndex(), property); - } - - foreach (var derivedEntityType in entityType.GetDerivedTypes()) - { - var entityCheck = derivedEntityType.GetConcreteDerivedTypesInclusive() - .Select( - e => keyValueComparer!.ExtractEqualsBody( - readExpressionMap[discriminatorProperty!], - Constant(e.GetDiscriminatorValue(), discriminatorProperty!.ClrType))) - .Aggregate((l, r) => OrElse(l, r)); - - foreach (var property in derivedEntityType.GetDeclaredProperties()) - { - readExpressionMap[property] = Condition( - entityCheck, - CreateReadValueExpression(property.ClrType, property.GetIndex(), property), - Default(property.ClrType)); - } - } - - var entityProjection = new EntityProjectionExpression(entityType, readExpressionMap); - _projectionMapping[new ProjectionMember()] = entityProjection; - } - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -140,6 +175,8 @@ public virtual Expression GetSingleScalarProjection() var expression = CreateReadValueExpression(ServerQueryExpression.Type, 0, null); _projectionMapping.Clear(); _projectionMapping[new ProjectionMember()] = expression; + _projectionMappingExpressions.Add(expression); + _groupingParameter = null; ConvertToEnumerable(); @@ -188,10 +225,95 @@ public virtual void ConvertToEnumerable() public virtual void ReplaceProjectionMapping([NotNull] IDictionary projectionMappings) { _projectionMapping.Clear(); - foreach (var kvp in projectionMappings) + _projectionMappingExpressions.Clear(); + LambdaExpression? selectorLambda = null; + if (_clientProjectionExpressions.Count > 0) + { + var remappedProjections = _clientProjectionExpressions + .Select((e, i) => CreateReadValueExpression(e.Type, i, InferPropertyFromInner(e))).ToList(); + + selectorLambda = Lambda( + New( + _valueBufferConstructor, + NewArrayInit( + typeof(object), + _clientProjectionExpressions.Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : e))), + CurrentParameter); + + _clientProjectionExpressions.Clear(); + _clientProjectionExpressions.AddRange(remappedProjections); + } + else { - _projectionMapping[kvp.Key] = kvp.Value; + var selectorExpressions = new List(); + foreach (var kvp in projectionMappings) + { + if (kvp.Value is EntityProjectionExpression entityProjectionExpression) + { + _projectionMapping[kvp.Key] = UpdateEntityProjection(entityProjectionExpression); + } + else + { + selectorExpressions.Add(kvp.Value); + var expression = CreateReadValueExpression( + kvp.Value.Type, selectorExpressions.Count - 1, InferPropertyFromInner(kvp.Value)); + _projectionMapping[kvp.Key] = expression; + _projectionMappingExpressions.Add(expression); + } + } + + if (selectorExpressions.Count == 0) + { + // No server correlated term in projection so add dummy 1. + selectorExpressions.Add(Constant(1)); + } + + selectorLambda = Lambda( + New( + _valueBufferConstructor, + NewArrayInit( + typeof(object), + selectorExpressions.Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : e))), + CurrentParameter); + + EntityProjectionExpression UpdateEntityProjection(EntityProjectionExpression entityProjection) + { + var readExpressionMap = new Dictionary(); + foreach (var property in GetAllPropertiesInHierarchy(entityProjection.EntityType)) + { + var expression = entityProjection.BindProperty(property); + selectorExpressions.Add(expression); + var newExpression = CreateReadValueExpression(expression.Type, selectorExpressions.Count - 1, property); + readExpressionMap[property] = newExpression; + _projectionMappingExpressions.Add(newExpression); + } + + var result = new EntityProjectionExpression(entityProjection.EntityType, readExpressionMap); + + // Also compute nested entity projections + foreach (var navigation in entityProjection.EntityType.GetAllBaseTypes() + .Concat(entityProjection.EntityType.GetDerivedTypesInclusive()) + .SelectMany(t => t.GetDeclaredNavigations())) + { + var boundEntityShaperExpression = entityProjection.BindNavigation(navigation); + if (boundEntityShaperExpression != null) + { + var innerEntityProjection = (EntityProjectionExpression)boundEntityShaperExpression.ValueBufferExpression; + var newInnerEntityProjection = UpdateEntityProjection(innerEntityProjection); + boundEntityShaperExpression = boundEntityShaperExpression.Update(newInnerEntityProjection); + result.AddNavigationBinding(navigation, boundEntityShaperExpression); + } + } + + return result; + } } + + ServerQueryExpression = Call( + EnumerableMethods.Select.MakeGenericMethod(CurrentParameter.Type, typeof(ValueBuffer)), + ServerQueryExpression, + selectorLambda); + _groupingParameter = null; } /// @@ -224,9 +346,9 @@ public virtual IDictionary AddToProjection([NotNull] EntityProje /// public virtual int AddToProjection([NotNull] Expression expression) { - _valueBufferSlots.Add(expression); + _clientProjectionExpressions.Add(expression); - return _valueBufferSlots.Count - 1; + return _clientProjectionExpressions.Count - 1; } /// @@ -261,38 +383,6 @@ public virtual int AddSubqueryProjection( return AddToProjection(serverQueryExpression); } - private sealed class ShaperRemappingExpressionVisitor : ExpressionVisitor - { - private readonly IDictionary _projectionMapping; - - public ShaperRemappingExpressionVisitor(IDictionary projectionMapping) - { - _projectionMapping = projectionMapping; - } - - [return: CA.NotNullIfNotNull("expression")] - public override Expression? Visit(Expression? expression) - { - if (expression is ProjectionBindingExpression projectionBindingExpression - && projectionBindingExpression.ProjectionMember != null) - { - var mappingValue = ((ConstantExpression)_projectionMapping[projectionBindingExpression.ProjectionMember]).Value; - return mappingValue is IDictionary indexMap - ? new ProjectionBindingExpression(projectionBindingExpression.QueryExpression, indexMap) - : mappingValue is int index - ? new ProjectionBindingExpression( - projectionBindingExpression.QueryExpression, index, projectionBindingExpression.Type) - : throw new InvalidOperationException(CoreStrings.UnknownEntity("ProjectionMapping")); - } - - return base.Visit(expression); - } - } - - private IEnumerable GetAllPropertiesInHierarchy(IEntityType entityType) - => entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive()) - .SelectMany(t => t.GetDeclaredProperties()); - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -311,74 +401,6 @@ public virtual Expression GetMappedProjection([NotNull] ProjectionMember member) public virtual void UpdateServerQueryExpression([NotNull] Expression serverQueryExpression) => ServerQueryExpression = serverQueryExpression; - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual void PushdownIntoSubquery() - { - var clientProjection = _valueBufferSlots.Count != 0; - if (!clientProjection) - { - var result = new Dictionary(); - foreach (var keyValuePair in _projectionMapping) - { - if (keyValuePair.Value is EntityProjectionExpression entityProjection) - { - var map = new Dictionary(); - foreach (var property in GetAllPropertiesInHierarchy(entityProjection.EntityType)) - { - var expressionToAdd = entityProjection.BindProperty(property); - var index = AddToProjection(expressionToAdd); - map[property] = CreateReadValueExpression(expressionToAdd.Type, 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)); - } - } - - _projectionMapping = result; - } - - var selectorLambda = Lambda( - New( - _valueBufferConstructor, - NewArrayInit( - typeof(object), - _valueBufferSlots - .Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : e))), - CurrentParameter); - - _groupingParameter = null; - - ServerQueryExpression = Call( - EnumerableMethods.Select.MakeGenericMethod(ServerQueryExpression.Type.GetSequenceType(), typeof(ValueBuffer)), - ServerQueryExpression, - selectorLambda); - - if (clientProjection) - { - var newValueBufferSlots = _valueBufferSlots - .Select((e, i) => CreateReadValueExpression(e.Type, i, InferPropertyFromInner(e))) - .ToList(); - - _valueBufferSlots.Clear(); - _valueBufferSlots.AddRange(newValueBufferSlots); - } - else - { - _valueBufferSlots.Clear(); - } - } - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -387,10 +409,12 @@ public virtual void PushdownIntoSubquery() /// public virtual void ApplySetOperation([NotNull] MethodInfo setOperationMethodInfo, [NotNull] InMemoryQueryExpression source2) { - var clientProjection = _valueBufferSlots.Count != 0; - if (!clientProjection) + Check.DebugAssert(_groupingParameter == null, "Cannot apply set operation after GroupBy without flattening."); + if (_clientProjectionExpressions.Count == 0) { - var result = new Dictionary(); + var projectionMapping = new Dictionary(); + var source1SelectorExpressions = new List(); + var source2SelectorExpressions = new List(); foreach (var (key, value1, value2) in _projectionMapping.Join( source2._projectionMapping, kv => kv.Key, kv => kv.Key, (kv1, kv2) => (kv1.Key, Value1: kv1.Value, Value2: kv2.Value))) @@ -398,89 +422,84 @@ public virtual void ApplySetOperation([NotNull] MethodInfo setOperationMethodInf if (value1 is EntityProjectionExpression entityProjection1 && value2 is EntityProjectionExpression entityProjection2) { - var map = new Dictionary(); + var map = new Dictionary(); foreach (var property in GetAllPropertiesInHierarchy(entityProjection1.EntityType)) { var expressionToAdd1 = entityProjection1.BindProperty(property); var expressionToAdd2 = entityProjection2.BindProperty(property); - var index = AddToProjection(expressionToAdd1); - source2.AddToProjection(expressionToAdd2); + source1SelectorExpressions.Add(expressionToAdd1); + source2SelectorExpressions.Add(expressionToAdd2); var type = expressionToAdd1.Type; if (!type.IsNullableType() && expressionToAdd2.Type.IsNullableType()) { type = expressionToAdd2.Type; } - map[property] = CreateReadValueExpression(type, index, property); + map[property] = CreateReadValueExpression(type, source1SelectorExpressions.Count - 1, property); } - result[key] = new EntityProjectionExpression(entityProjection1.EntityType, map); + projectionMapping[key] = new EntityProjectionExpression(entityProjection1.EntityType, map); } else { - var index = AddToProjection(value1); - source2.AddToProjection(value2); + source1SelectorExpressions.Add(value1); + source2SelectorExpressions.Add(value2); var type = value1.Type; if (!type.IsNullableType() && value2.Type.IsNullableType()) { type = value2.Type; } - result[key] = CreateReadValueExpression(type, index, InferPropertyFromInner(value1)); + projectionMapping[key] = CreateReadValueExpression(type, source1SelectorExpressions.Count - 1, InferPropertyFromInner(value1)); } } - _projectionMapping = result; - } - - var selectorLambda = Lambda( - New( - _valueBufferConstructor, - NewArrayInit( - typeof(object), - _valueBufferSlots - .Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : e))), - CurrentParameter); - - _groupingParameter = null; - - ServerQueryExpression = Call( - EnumerableMethods.Select.MakeGenericMethod(ServerQueryExpression.Type.GetSequenceType(), typeof(ValueBuffer)), - ServerQueryExpression, - selectorLambda); - - var selectorLambda2 = Lambda( - New( - _valueBufferConstructor, - NewArrayInit( - typeof(object), - source2._valueBufferSlots - .Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : e))), - source2.CurrentParameter); - - source2._groupingParameter = null; - - source2.ServerQueryExpression = Call( - EnumerableMethods.Select.MakeGenericMethod(source2.ServerQueryExpression.Type.GetSequenceType(), typeof(ValueBuffer)), - source2.ServerQueryExpression, - selectorLambda2); - - ServerQueryExpression = Call( - setOperationMethodInfo.MakeGenericMethod(typeof(ValueBuffer)), ServerQueryExpression, source2.ServerQueryExpression); - - if (clientProjection) - { - var newValueBufferSlots = _valueBufferSlots - .Select((e, i) => CreateReadValueExpression(e.Type, i, InferPropertyFromInner(e))) - .ToList(); + _projectionMapping = projectionMapping; - _valueBufferSlots.Clear(); - _valueBufferSlots.AddRange(newValueBufferSlots); + ServerQueryExpression = Call( + EnumerableMethods.Select.MakeGenericMethod(ServerQueryExpression.Type.GetSequenceType(), typeof(ValueBuffer)), + ServerQueryExpression, + Lambda( + New( + _valueBufferConstructor, + NewArrayInit( + typeof(object), + source1SelectorExpressions.Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : e))), + CurrentParameter)); + + + source2.ServerQueryExpression = Call( + EnumerableMethods.Select.MakeGenericMethod(source2.ServerQueryExpression.Type.GetSequenceType(), typeof(ValueBuffer)), + source2.ServerQueryExpression, + Lambda( + New( + _valueBufferConstructor, + NewArrayInit( + typeof(object), + source2SelectorExpressions.Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : e))), + source2.CurrentParameter)); } else { - _valueBufferSlots.Clear(); + Check.DebugAssert(_clientProjectionExpressions.Count == source2._clientProjectionExpressions.Count, + "Index count in both source should match."); + + // In case of client projections, indexes must match so we don't worry about it. + // We still have to formualte outer client projections again for nullability. + for (var i = 0; i < source2._clientProjectionExpressions.Count; i++) + { + var type1 = _clientProjectionExpressions[i].Type; + var type2 = source2._clientProjectionExpressions[i].Type; + if (!type1.IsNullableValueType() + && type2.IsNullableValueType()) + { + _clientProjectionExpressions[i] = MakeReadValueNullable(_clientProjectionExpressions[i]); + } + } } + + ServerQueryExpression = Call( + setOperationMethodInfo.MakeGenericMethod(typeof(ValueBuffer)), ServerQueryExpression, source2.ServerQueryExpression); } /// @@ -491,67 +510,43 @@ public virtual void ApplySetOperation([NotNull] MethodInfo setOperationMethodInf /// public virtual void ApplyDefaultIfEmpty() { - if (_valueBufferSlots.Count != 0) + if (_clientProjectionExpressions.Count != 0) { throw new InvalidOperationException(InMemoryStrings.DefaultIfEmptyAppliedAfterProjection); } - var result = new Dictionary(); + var projectionMapping = new Dictionary(); foreach (var keyValuePair in _projectionMapping) { if (keyValuePair.Value is EntityProjectionExpression entityProjection) { - var map = new Dictionary(); + var map = new Dictionary(); foreach (var property in GetAllPropertiesInHierarchy(entityProjection.EntityType)) { - var expressionToAdd = entityProjection.BindProperty(property); - var index = AddToProjection(expressionToAdd); - map[property] = CreateReadValueExpression(expressionToAdd.Type.MakeNullable(), index, property); + map[property] = MakeReadValueNullable(entityProjection.BindProperty(property)); } - result[keyValuePair.Key] = new EntityProjectionExpression(entityProjection.EntityType, map); + projectionMapping[keyValuePair.Key] = new EntityProjectionExpression(entityProjection.EntityType, map); } else { - var index = AddToProjection(keyValuePair.Value); - result[keyValuePair.Key] = CreateReadValueExpression( - keyValuePair.Value.Type.MakeNullable(), index, InferPropertyFromInner(keyValuePair.Value)); + projectionMapping[keyValuePair.Key] = MakeReadValueNullable(keyValuePair.Value); } } - _projectionMapping = result; - - var selectorLambda = Lambda( - New( - _valueBufferConstructor, - NewArrayInit( - typeof(object), - _valueBufferSlots - .Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : e))), - CurrentParameter); + _projectionMapping = projectionMapping; + var projectionMappingExpressions = _projectionMappingExpressions.Select(e => MakeReadValueNullable(e)).ToList(); + _projectionMappingExpressions.Clear(); + _projectionMappingExpressions.AddRange(projectionMappingExpressions); _groupingParameter = null; - ServerQueryExpression = Call( - EnumerableMethods.Select.MakeGenericMethod(ServerQueryExpression.Type.GetSequenceType(), typeof(ValueBuffer)), - ServerQueryExpression, - selectorLambda); - ServerQueryExpression = Call( EnumerableMethods.DefaultIfEmptyWithArgument.MakeGenericMethod(typeof(ValueBuffer)), ServerQueryExpression, - New(_valueBufferConstructor, NewArrayInit(typeof(object), Enumerable.Repeat(Constant(null), _valueBufferSlots.Count)))); - - _valueBufferSlots.Clear(); + Constant(new ValueBuffer(Enumerable.Repeat((object?)null, _projectionMappingExpressions.Count).ToArray()))); } - private static IPropertyBase? InferPropertyFromInner(Expression expression) - => expression is MethodCallExpression methodCallExpression - && methodCallExpression.Method.IsGenericMethod - && methodCallExpression.Method.GetGenericMethodDefinition() == ExpressionExtensions.ValueBufferTryReadValueMethod - ? methodCallExpression.Arguments[2].GetConstantValue() - : null; - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -560,25 +555,14 @@ public virtual void ApplyDefaultIfEmpty() /// public virtual void ApplyProjection() { - if (_valueBufferSlots.Count == 0) + if (_clientProjectionExpressions.Count == 0) { var result = new Dictionary(); foreach (var keyValuePair in _projectionMapping) { - if (keyValuePair.Value is EntityProjectionExpression entityProjection) - { - 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] = keyValuePair.Value is EntityProjectionExpression entityProjection + ? Constant(AddToProjection(entityProjection)) + : Constant(AddToProjection(keyValuePair.Value)); } _projectionMapping = result; @@ -589,9 +573,7 @@ public virtual void ApplyProjection() _valueBufferConstructor, NewArrayInit( typeof(object), - _valueBufferSlots - .Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : e) - .ToArray())), + _clientProjectionExpressions.Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : e).ToArray())), CurrentParameter); ServerQueryExpression = Call( @@ -608,20 +590,35 @@ public virtual void ApplyProjection() /// public virtual InMemoryGroupByShaperExpression ApplyGrouping( [NotNull] Expression groupingKey, - [NotNull] Expression shaperExpression) + [NotNull] Expression shaperExpression, + bool defaultElementSelector) { - PushdownIntoSubquery(); - - var selectMethod = (MethodCallExpression)ServerQueryExpression; - var groupBySource = selectMethod.Arguments[0]; - var elementSelector = selectMethod.Arguments[1]; - _groupingParameter = Parameter(typeof(IGrouping), "grouping"); - var groupingKeyAccessExpression = PropertyOrField(_groupingParameter, nameof(IGrouping.Key)); - var groupingKeyExpressions = new List(); - groupingKey = GetGroupingKey(groupingKey, groupingKeyExpressions, groupingKeyAccessExpression); - var keySelector = Lambda( - New( - _valueBufferConstructor, + var source = ServerQueryExpression; + Expression? selector = null; + if (defaultElementSelector) + { + selector = Lambda( + New( + _valueBufferConstructor, + NewArrayInit( + typeof(object), + _projectionMappingExpressions.Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : (Expression)e))), + _valueBufferParameter); + } + else + { + var selectMethodCall = (MethodCallExpression)ServerQueryExpression; + source = selectMethodCall.Arguments[0]; + selector = selectMethodCall.Arguments[1]; + } + + _groupingParameter = Parameter(typeof(IGrouping), "grouping"); + var groupingKeyAccessExpression = PropertyOrField(_groupingParameter, nameof(IGrouping.Key)); + var groupingKeyExpressions = new List(); + groupingKey = GetGroupingKey(groupingKey, groupingKeyExpressions, groupingKeyAccessExpression); + var keySelector = Lambda( + New( + _valueBufferConstructor, NewArrayInit( typeof(object), groupingKeyExpressions.Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : e))), @@ -630,9 +627,9 @@ public virtual InMemoryGroupByShaperExpression ApplyGrouping( ServerQueryExpression = Call( EnumerableMethods.GroupByWithKeyElementSelector.MakeGenericMethod( typeof(ValueBuffer), typeof(ValueBuffer), typeof(ValueBuffer)), - selectMethod.Arguments[0], + source, keySelector, - selectMethod.Arguments[1]); + selector); return new InMemoryGroupByShaperExpression( groupingKey, @@ -641,53 +638,6 @@ public virtual InMemoryGroupByShaperExpression ApplyGrouping( _valueBufferParameter); } - private Expression GetGroupingKey(Expression key, List groupingExpressions, Expression groupingKeyAccessExpression) - { - switch (key) - { - case NewExpression newExpression: - var arguments = new Expression[newExpression.Arguments.Count]; - for (var i = 0; i < arguments.Length; i++) - { - arguments[i] = GetGroupingKey(newExpression.Arguments[i], groupingExpressions, groupingKeyAccessExpression); - } - - return newExpression.Update(arguments); - - case MemberInitExpression memberInitExpression: - if (memberInitExpression.Bindings.Any(mb => !(mb is MemberAssignment))) - { - goto default; - } - - var updatedNewExpression = (NewExpression)GetGroupingKey( - memberInitExpression.NewExpression, groupingExpressions, groupingKeyAccessExpression); - var memberBindings = new MemberAssignment[memberInitExpression.Bindings.Count]; - for (var i = 0; i < memberBindings.Length; i++) - { - var memberAssignment = (MemberAssignment)memberInitExpression.Bindings[i]; - memberBindings[i] = memberAssignment.Update( - GetGroupingKey( - memberAssignment.Expression, - groupingExpressions, - groupingKeyAccessExpression)); - } - - return memberInitExpression.Update(updatedNewExpression, memberBindings); - - default: - var index = groupingExpressions.Count; - groupingExpressions.Add(key); - return groupingKeyAccessExpression.CreateValueBufferReadValueExpression( - key.Type, - index, - InferPropertyFromInner(key)); - } - } - - private Expression CreateReadValueExpression(Type type, int index, IPropertyBase? property) - => _valueBufferParameter.CreateValueBufferReadValueExpression(type, index, property); - /// /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to /// the same compatibility standards as public APIs. It may be changed or removed without notice in @@ -699,85 +649,111 @@ public virtual void AddInnerJoin( [NotNull] LambdaExpression outerKeySelector, [NotNull] LambdaExpression innerKeySelector, [NotNull] Type transparentIdentifierType) + => AddJoin(innerQueryExpression, outerKeySelector, innerKeySelector, transparentIdentifierType, innerNullable: false); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual void AddLeftJoin( + [NotNull] InMemoryQueryExpression innerQueryExpression, + [NotNull] LambdaExpression outerKeySelector, + [NotNull] LambdaExpression innerKeySelector, + [NotNull] Type transparentIdentifierType) + => AddJoin(innerQueryExpression, outerKeySelector, innerKeySelector, transparentIdentifierType, innerNullable: true); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual void AddSelectMany( + [NotNull] InMemoryQueryExpression innerQueryExpression, + [NotNull] Type transparentIdentifierType, + bool innerNullable) + => AddJoin(innerQueryExpression, null, null, transparentIdentifierType, innerNullable); + + /// + /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to + /// the same compatibility standards as public APIs. It may be changed or removed without notice in + /// any release. You should only use it directly in your code with extreme caution and knowing that + /// doing so can result in application failures when updating to a new Entity Framework Core release. + /// + public virtual EntityShaperExpression AddNavigationToWeakEntityType( + [NotNull] EntityProjectionExpression entityProjectionExpression, + [NotNull] INavigation navigation, + [NotNull] InMemoryQueryExpression innerQueryExpression, + [NotNull] LambdaExpression outerKeySelector, + [NotNull] LambdaExpression innerKeySelector) { + var innerNullable = !navigation.ForeignKey.IsRequiredDependent; var outerParameter = Parameter(typeof(ValueBuffer), "outer"); var innerParameter = Parameter(typeof(ValueBuffer), "inner"); - var resultValueBufferExpressions = new List(); - var projectionMapping = new Dictionary(); var replacingVisitor = new ReplacingExpressionVisitor( new Expression[] { CurrentParameter, innerQueryExpression.CurrentParameter }, new Expression[] { outerParameter, innerParameter }); + var resultSelectorExpressions = _projectionMappingExpressions + .Select(e => replacingVisitor.Visit(e)) + .ToList(); - var index = 0; - var outerMemberInfo = transparentIdentifierType.GetTypeInfo().GetRequiredDeclaredField("Outer"); - foreach (var projection in _projectionMapping) - { - if (projection.Value is EntityProjectionExpression entityProjection) - { - var readExpressionMap = new Dictionary(); - foreach (var property in GetAllPropertiesInHierarchy(entityProjection.EntityType)) - { - var replacedExpression = replacingVisitor.Visit(entityProjection.BindProperty(property)); - resultValueBufferExpressions.Add(replacedExpression); - readExpressionMap[property] = CreateReadValueExpression(replacedExpression.Type, index++, property); - } - - projectionMapping[projection.Key.Prepend(outerMemberInfo)] - = new EntityProjectionExpression(entityProjection.EntityType, readExpressionMap); - } - else - { - resultValueBufferExpressions.Add(replacingVisitor.Visit(projection.Value)); - projectionMapping[projection.Key.Prepend(outerMemberInfo)] - = CreateReadValueExpression(projection.Value.Type, index++, InferPropertyFromInner(projection.Value)); - } - } + var outerIndex = resultSelectorExpressions.Count; + var innerEntityProjection = (EntityProjectionExpression)innerQueryExpression.GetMappedProjection(new ProjectionMember()); - var innerMemberInfo = transparentIdentifierType.GetTypeInfo().GetRequiredDeclaredField("Inner"); - foreach (var projection in innerQueryExpression._projectionMapping) + var innerReadExpressionMap = new Dictionary(); + foreach (var property in GetAllPropertiesInHierarchy(innerEntityProjection.EntityType)) { - if (projection.Value is EntityProjectionExpression entityProjection) - { - var readExpressionMap = new Dictionary(); - foreach (var property in GetAllPropertiesInHierarchy(entityProjection.EntityType)) - { - var replacedExpression = replacingVisitor.Visit(entityProjection.BindProperty(property)); - resultValueBufferExpressions.Add(replacedExpression); - readExpressionMap[property] = CreateReadValueExpression(replacedExpression.Type, index++, property); - } - - projectionMapping[projection.Key.Prepend(innerMemberInfo)] - = new EntityProjectionExpression(entityProjection.EntityType, readExpressionMap); - } - else + var replacedExpression = replacingVisitor.Visit(innerEntityProjection.BindProperty(property)); + if (innerNullable) { - resultValueBufferExpressions.Add(replacingVisitor.Visit(projection.Value)); - projectionMapping[projection.Key.Prepend(innerMemberInfo)] - = CreateReadValueExpression(projection.Value.Type, index++, InferPropertyFromInner(projection.Value)); + replacedExpression = MakeReadValueNullable(replacedExpression); } + resultSelectorExpressions.Add(replacedExpression); + var readValueExperssion = CreateReadValueExpression(replacedExpression.Type, resultSelectorExpressions.Count - 1, property); + innerReadExpressionMap[property] = readValueExperssion; + _projectionMappingExpressions.Add(readValueExperssion); } + innerEntityProjection = new EntityProjectionExpression(innerEntityProjection.EntityType, innerReadExpressionMap); + var resultSelector = Lambda( - New( - _valueBufferConstructor, - NewArrayInit( - typeof(object), - resultValueBufferExpressions - .Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : e) - .ToArray())), + New(_valueBufferConstructor, + NewArrayInit(typeof(object), + resultSelectorExpressions.Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : e))), outerParameter, innerParameter); - ServerQueryExpression = Call( - EnumerableMethods.Join.MakeGenericMethod( - typeof(ValueBuffer), typeof(ValueBuffer), outerKeySelector.ReturnType, typeof(ValueBuffer)), - ServerQueryExpression, - innerQueryExpression.ServerQueryExpression, - outerKeySelector, - innerKeySelector, - resultSelector); + if (innerNullable) + { + ServerQueryExpression = Call( + _leftJoinMethodInfo.MakeGenericMethod( + typeof(ValueBuffer), typeof(ValueBuffer), outerKeySelector.ReturnType, typeof(ValueBuffer)), + ServerQueryExpression, + innerQueryExpression.ServerQueryExpression, + outerKeySelector, + innerKeySelector, + resultSelector, + Constant(new ValueBuffer( + Enumerable.Repeat((object?)null, innerQueryExpression._projectionMappingExpressions.Count).ToArray()))); + } + else + { + ServerQueryExpression = Call( + EnumerableMethods.Join.MakeGenericMethod( + typeof(ValueBuffer), typeof(ValueBuffer), outerKeySelector.ReturnType, typeof(ValueBuffer)), + ServerQueryExpression, + innerQueryExpression.ServerQueryExpression, + outerKeySelector, + innerKeySelector, + resultSelector); + } - _projectionMapping = projectionMapping; + var entityShaper = new EntityShaperExpression(innerEntityProjection.EntityType, innerEntityProjection, nullable: innerNullable); + entityProjectionExpression.AddNavigationBinding(navigation, entityShaper); + + return entityShaper; } /// @@ -786,181 +762,105 @@ public virtual void AddInnerJoin( /// any release. You should only use it directly in your code with extreme caution and knowing that /// doing so can result in application failures when updating to a new Entity Framework Core release. /// - public virtual void AddLeftJoin( - [NotNull] InMemoryQueryExpression innerQueryExpression, - [NotNull] LambdaExpression outerKeySelector, - [NotNull] LambdaExpression innerKeySelector, - [NotNull] Type transparentIdentifierType) + void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) { - // GroupJoin phase - var groupTransparentIdentifierType = TransparentIdentifierFactory.Create( - typeof(ValueBuffer), typeof(IEnumerable)); - var outerParameter = Parameter(typeof(ValueBuffer), "outer"); - var innerParameter = Parameter(typeof(IEnumerable), "inner"); - var outerMemberInfo = groupTransparentIdentifierType.GetTypeInfo().GetRequiredDeclaredField("Outer"); - var innerMemberInfo = groupTransparentIdentifierType.GetTypeInfo().GetRequiredDeclaredField("Inner"); - var resultSelector = Lambda( - New( - groupTransparentIdentifierType.GetTypeInfo().DeclaredConstructors.Single(), - new[] { outerParameter, innerParameter }, outerMemberInfo, innerMemberInfo), - outerParameter, - innerParameter); - - var groupJoinExpression = Call( - EnumerableMethods.GroupJoin.MakeGenericMethod( - typeof(ValueBuffer), typeof(ValueBuffer), outerKeySelector.ReturnType, groupTransparentIdentifierType), - ServerQueryExpression, - innerQueryExpression.ServerQueryExpression, - outerKeySelector, - innerKeySelector, - resultSelector); - - // SelectMany phase - var collectionParameter = Parameter(groupTransparentIdentifierType, "collection"); - var collection = MakeMemberAccess(collectionParameter, innerMemberInfo); - outerParameter = Parameter(groupTransparentIdentifierType, "outer"); - innerParameter = Parameter(typeof(ValueBuffer), "inner"); - - var resultValueBufferExpressions = new List(); - var projectionMapping = new Dictionary(); - var replacingVisitor = new ReplacingExpressionVisitor( - new Expression[] { CurrentParameter, innerQueryExpression.CurrentParameter }, - new Expression[] { MakeMemberAccess(outerParameter, outerMemberInfo), innerParameter }); + Check.NotNull(expressionPrinter, nameof(expressionPrinter)); - var nullableReadValueExpressionVisitor = new NullableReadValueExpressionVisitor(); - var index = 0; - outerMemberInfo = transparentIdentifierType.GetTypeInfo().GetRequiredDeclaredField("Outer"); - foreach (var projection in _projectionMapping) + expressionPrinter.AppendLine(nameof(InMemoryQueryExpression) + ": "); + using (expressionPrinter.Indent()) { - if (projection.Value is EntityProjectionExpression entityProjection) - { - projectionMapping[projection.Key.Prepend(outerMemberInfo)] = CopyEntityProjectionToOuter(entityProjection); - } - else + expressionPrinter.AppendLine(nameof(ServerQueryExpression) + ": "); + using (expressionPrinter.Indent()) { - var replacedExpression = replacingVisitor.Visit(projection.Value); - resultValueBufferExpressions.Add(replacedExpression); - projectionMapping[projection.Key.Prepend(outerMemberInfo)] = CreateReadValueExpression( - replacedExpression.Type, index++, InferPropertyFromInner(projection.Value)); + expressionPrinter.Visit(ServerQueryExpression); } - } - var outerIndex = index; - innerMemberInfo = transparentIdentifierType.GetTypeInfo().GetRequiredDeclaredField("Inner"); - foreach (var projection in innerQueryExpression._projectionMapping) - { - if (projection.Value is EntityProjectionExpression entityProjection) - { - projectionMapping[projection.Key.Prepend(innerMemberInfo)] = CopyEntityProjectionToOuter(entityProjection, true); - } - else + expressionPrinter.AppendLine(); + expressionPrinter.AppendLine("ProjectionMapping:"); + using (expressionPrinter.Indent()) { - var replacedExpression = replacingVisitor.Visit(projection.Value); - replacedExpression = nullableReadValueExpressionVisitor.Visit(replacedExpression); - resultValueBufferExpressions.Add(replacedExpression); - projectionMapping[projection.Key.Prepend(innerMemberInfo)] = CreateReadValueExpression( - replacedExpression.Type, index++, InferPropertyFromInner(projection.Value)); + foreach (var projectionMapping in _projectionMapping) + { + expressionPrinter.Append("Member: " + projectionMapping.Key + " Projection: "); + expressionPrinter.Visit(projectionMapping.Value); + expressionPrinter.AppendLine(","); + } } - } - - var collectionSelector = Lambda( - Call( - EnumerableMethods.DefaultIfEmptyWithArgument.MakeGenericMethod(typeof(ValueBuffer)), - collection, - New( - _valueBufferConstructor, - NewArrayInit( - typeof(object), - Enumerable.Range(0, index - outerIndex).Select(i => Constant(null))))), - collectionParameter); - resultSelector = Lambda( - New( - _valueBufferConstructor, - NewArrayInit( - typeof(object), - resultValueBufferExpressions - .Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : e) - .ToArray())), - outerParameter, - innerParameter); - - ServerQueryExpression = Call( - EnumerableMethods.SelectManyWithCollectionSelector.MakeGenericMethod( - groupTransparentIdentifierType, typeof(ValueBuffer), typeof(ValueBuffer)), - groupJoinExpression, - collectionSelector, - resultSelector); - - _projectionMapping = projectionMapping; + expressionPrinter.AppendLine(); + } + } - EntityProjectionExpression CopyEntityProjectionToOuter(EntityProjectionExpression entityProjection, bool nullable = false) + private Expression GetGroupingKey(Expression key, List groupingExpressions, Expression groupingKeyAccessExpression) + { + switch (key) { - var readExpressionMap = new Dictionary(); - foreach (var property in GetAllPropertiesInHierarchy(entityProjection.EntityType)) - { - var replacedExpression = replacingVisitor.Visit(entityProjection.BindProperty(property)); - if (nullable) + case NewExpression newExpression: + var arguments = new Expression[newExpression.Arguments.Count]; + for (var i = 0; i < arguments.Length; i++) { - replacedExpression = nullableReadValueExpressionVisitor.Visit(replacedExpression); + arguments[i] = GetGroupingKey(newExpression.Arguments[i], groupingExpressions, groupingKeyAccessExpression); } - resultValueBufferExpressions.Add(replacedExpression); - readExpressionMap[property] = CreateReadValueExpression( - replacedExpression.Type, index++, property); - } - var newEntityProjection = new EntityProjectionExpression(entityProjection.EntityType, readExpressionMap); + return newExpression.Update(arguments); - // Also lift nested entity projections - foreach (var navigation in entityProjection.EntityType.GetAllBaseTypes() - .Concat(entityProjection.EntityType.GetDerivedTypesInclusive()) - .SelectMany(t => t.GetDeclaredNavigations())) - { - var boundEntityShaperExpression = entityProjection.BindNavigation(navigation); - if (boundEntityShaperExpression != null) + case MemberInitExpression memberInitExpression: + if (memberInitExpression.Bindings.Any(mb => !(mb is MemberAssignment))) { - var innerEntityProjection = (EntityProjectionExpression)boundEntityShaperExpression.ValueBufferExpression; - var newInnerEntityProjection = CopyEntityProjectionToOuter(innerEntityProjection); - boundEntityShaperExpression = boundEntityShaperExpression.Update(newInnerEntityProjection); - newEntityProjection.AddNavigationBinding(navigation, boundEntityShaperExpression); + goto default; } - } - return newEntityProjection; + var updatedNewExpression = (NewExpression)GetGroupingKey( + memberInitExpression.NewExpression, groupingExpressions, groupingKeyAccessExpression); + var memberBindings = new MemberAssignment[memberInitExpression.Bindings.Count]; + for (var i = 0; i < memberBindings.Length; i++) + { + var memberAssignment = (MemberAssignment)memberInitExpression.Bindings[i]; + memberBindings[i] = memberAssignment.Update( + GetGroupingKey( + memberAssignment.Expression, + groupingExpressions, + groupingKeyAccessExpression)); + } + + return memberInitExpression.Update(updatedNewExpression, memberBindings); + + default: + var index = groupingExpressions.Count; + groupingExpressions.Add(key); + return groupingKeyAccessExpression.CreateValueBufferReadValueExpression( + key.Type, + index, + InferPropertyFromInner(key)); } } - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual void AddSelectMany( - [NotNull] InMemoryQueryExpression innerQueryExpression, - [NotNull] Type transparentIdentifierType, + private void AddJoin( + InMemoryQueryExpression innerQueryExpression, + LambdaExpression? outerKeySelector, + LambdaExpression? innerKeySelector, + Type transparentIdentifierType, bool innerNullable) { var outerParameter = Parameter(typeof(ValueBuffer), "outer"); var innerParameter = Parameter(typeof(ValueBuffer), "inner"); - var resultValueBufferExpressions = new List(); var projectionMapping = new Dictionary(); var replacingVisitor = new ReplacingExpressionVisitor( new Expression[] { CurrentParameter, innerQueryExpression.CurrentParameter }, new Expression[] { outerParameter, innerParameter }); - var index = 0; var outerMemberInfo = transparentIdentifierType.GetTypeInfo().GetRequiredDeclaredField("Outer"); + var innerMemberInfo = transparentIdentifierType.GetTypeInfo().GetRequiredDeclaredField("Inner"); foreach (var projection in _projectionMapping) { if (projection.Value is EntityProjectionExpression entityProjection) { - var readExpressionMap = new Dictionary(); + var readExpressionMap = new Dictionary(); foreach (var property in GetAllPropertiesInHierarchy(entityProjection.EntityType)) { var replacedExpression = replacingVisitor.Visit(entityProjection.BindProperty(property)); - resultValueBufferExpressions.Add(replacedExpression); - readExpressionMap[property] = CreateReadValueExpression(replacedExpression.Type, index++, property); + readExpressionMap[property] = CreateReadValueExpression( + replacedExpression.Type, GetIndex(replacedExpression), property); } projectionMapping[projection.Key.Prepend(outerMemberInfo)] @@ -968,29 +868,27 @@ public virtual void AddSelectMany( } else { - resultValueBufferExpressions.Add(replacingVisitor.Visit(projection.Value)); - projectionMapping[projection.Key.Prepend(outerMemberInfo)] - = CreateReadValueExpression(projection.Value.Type, index++, InferPropertyFromInner(projection.Value)); + var replacedExpression = replacingVisitor.Visit(projection.Value); + projectionMapping[projection.Key.Prepend(outerMemberInfo)] = CreateReadValueExpression( + projection.Value.Type, GetIndex(replacedExpression), InferPropertyFromInner(projection.Value)); } } - var innerMemberInfo = transparentIdentifierType.GetTypeInfo().GetRequiredDeclaredField("Inner"); - var nullableReadValueExpressionVisitor = new NullableReadValueExpressionVisitor(); + var outerIndex = _projectionMappingExpressions.Count; foreach (var projection in innerQueryExpression._projectionMapping) { if (projection.Value is EntityProjectionExpression entityProjection) { - var readExpressionMap = new Dictionary(); + var readExpressionMap = new Dictionary(); foreach (var property in GetAllPropertiesInHierarchy(entityProjection.EntityType)) { var replacedExpression = replacingVisitor.Visit(entityProjection.BindProperty(property)); if (innerNullable) { - replacedExpression = nullableReadValueExpressionVisitor.Visit(replacedExpression); + replacedExpression = MakeReadValueNullable(replacedExpression); } - - resultValueBufferExpressions.Add(replacedExpression); - readExpressionMap[property] = CreateReadValueExpression(replacedExpression.Type, index++, property); + readExpressionMap[property] = CreateReadValueExpression( + replacedExpression.Type, GetIndex(replacedExpression) + outerIndex, property); } projectionMapping[projection.Key.Prepend(innerMemberInfo)] @@ -1001,261 +899,152 @@ public virtual void AddSelectMany( var replacedExpression = replacingVisitor.Visit(projection.Value); if (innerNullable) { - replacedExpression = nullableReadValueExpressionVisitor.Visit(replacedExpression); + replacedExpression = MakeReadValueNullable(replacedExpression); } - - resultValueBufferExpressions.Add(replacedExpression); - projectionMapping[projection.Key.Prepend(innerMemberInfo)] - = CreateReadValueExpression(replacedExpression.Type, index++, InferPropertyFromInner(projection.Value)); + projectionMapping[projection.Key.Prepend(innerMemberInfo)] = CreateReadValueExpression( + replacedExpression.Type, GetIndex(replacedExpression) + outerIndex, InferPropertyFromInner(replacedExpression)); } } - var resultSelector = Lambda( - New( - _valueBufferConstructor, - NewArrayInit( - typeof(object), - resultValueBufferExpressions - .Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : e) - .ToArray())), - outerParameter, - innerParameter); - - ServerQueryExpression = Call( - EnumerableMethods.SelectManyWithCollectionSelector.MakeGenericMethod( - typeof(ValueBuffer), typeof(ValueBuffer), typeof(ValueBuffer)), - ServerQueryExpression, - Lambda(innerQueryExpression.ServerQueryExpression, CurrentParameter), - resultSelector); + var resultSelectorExpressions = new List(); + foreach (var expression in _projectionMappingExpressions) + { + var updatedExpression = replacingVisitor.Visit(expression); + resultSelectorExpressions.Add( + updatedExpression.Type.IsValueType ? Convert(updatedExpression, typeof(object)) : updatedExpression); + } - _projectionMapping = projectionMapping; - } + foreach (var expression in innerQueryExpression._projectionMappingExpressions) + { + var replacedExpression = replacingVisitor.Visit(expression); + if (innerNullable) + { + replacedExpression = MakeReadValueNullable(replacedExpression); + } + resultSelectorExpressions.Add( + replacedExpression.Type.IsValueType ? Convert(replacedExpression, typeof(object)) : replacedExpression); + + _projectionMappingExpressions.Add( + CreateReadValueExpression( + innerNullable ? expression.Type.MakeNullable() : expression.Type, + GetIndex(expression) + outerIndex, + InferPropertyFromInner(expression))); + } - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - public virtual EntityShaperExpression AddNavigationToWeakEntityType( - [NotNull] EntityProjectionExpression entityProjectionExpression, - [NotNull] INavigation navigation, - [NotNull] InMemoryQueryExpression innerQueryExpression, - [NotNull] LambdaExpression outerKeySelector, - [NotNull] LambdaExpression innerKeySelector) - { - // GroupJoin phase - var groupTransparentIdentifierType = TransparentIdentifierFactory.Create( - typeof(ValueBuffer), typeof(IEnumerable)); - var outerParameter = Parameter(typeof(ValueBuffer), "outer"); - var innerParameter = Parameter(typeof(IEnumerable), "inner"); - var outerMemberInfo = groupTransparentIdentifierType.GetTypeInfo().GetRequiredDeclaredField("Outer"); - var innerMemberInfo = groupTransparentIdentifierType.GetTypeInfo().GetRequiredDeclaredField("Inner"); var resultSelector = Lambda( - New( - groupTransparentIdentifierType.GetTypeInfo().DeclaredConstructors.Single(), - new[] { outerParameter, innerParameter }, outerMemberInfo, innerMemberInfo), + New(_valueBufferConstructor, NewArrayInit(typeof(object), resultSelectorExpressions)), outerParameter, innerParameter); - var groupJoinExpression = Call( - EnumerableMethods.GroupJoin.MakeGenericMethod( - typeof(ValueBuffer), typeof(ValueBuffer), outerKeySelector.ReturnType, groupTransparentIdentifierType), - ServerQueryExpression, - innerQueryExpression.ServerQueryExpression, - outerKeySelector, - innerKeySelector, - resultSelector); - - // SelectMany phase - var collectionParameter = Parameter(groupTransparentIdentifierType, "collection"); - var collection = MakeMemberAccess(collectionParameter, innerMemberInfo); - outerParameter = Parameter(groupTransparentIdentifierType, "outer"); - innerParameter = Parameter(typeof(ValueBuffer), "inner"); - - var resultValueBufferExpressions = new List(); - var projectionMapping = new Dictionary(); - var replacingVisitor = new ReplacingExpressionVisitor( - new Expression[] { CurrentParameter, innerQueryExpression.CurrentParameter }, - new Expression[] { MakeMemberAccess(outerParameter, outerMemberInfo), innerParameter }); - - foreach (var projection in _projectionMapping) + if (outerKeySelector != null + && innerKeySelector != null) { - if (projection.Value is EntityProjectionExpression entityProjection) + if (innerNullable) { - projectionMapping[projection.Key] = CopyEntityProjectionToOuter(entityProjection); + ServerQueryExpression = Call( + _leftJoinMethodInfo.MakeGenericMethod( + typeof(ValueBuffer), typeof(ValueBuffer), outerKeySelector.ReturnType, typeof(ValueBuffer)), + ServerQueryExpression, + innerQueryExpression.ServerQueryExpression, + outerKeySelector, + innerKeySelector, + resultSelector, + Constant(new ValueBuffer( + Enumerable.Repeat((object?)null, innerQueryExpression._projectionMappingExpressions.Count).ToArray()))); } else { - var replacedExpression = replacingVisitor.Visit(projection.Value); - resultValueBufferExpressions.Add(replacedExpression); - projectionMapping[projection.Key] = CreateReadValueExpression( - replacedExpression.Type, resultValueBufferExpressions.Count - 1, InferPropertyFromInner(projection.Value)); + ServerQueryExpression = Call( + EnumerableMethods.Join.MakeGenericMethod( + typeof(ValueBuffer), typeof(ValueBuffer), outerKeySelector.ReturnType, typeof(ValueBuffer)), + ServerQueryExpression, + innerQueryExpression.ServerQueryExpression, + outerKeySelector, + innerKeySelector, + resultSelector); } } - - _projectionMapping = projectionMapping; - var outerIndex = resultValueBufferExpressions.Count; - var nullableReadValueExpressionVisitor = new NullableReadValueExpressionVisitor(); - var innerEntityProjection = (EntityProjectionExpression)innerQueryExpression.GetMappedProjection(new ProjectionMember()); - - var innerReadExpressionMap = new Dictionary(); - foreach (var property in GetAllPropertiesInHierarchy(innerEntityProjection.EntityType)) + else { - var replacedExpression = replacingVisitor.Visit(innerEntityProjection.BindProperty(property)); - replacedExpression = nullableReadValueExpressionVisitor.Visit(replacedExpression); - resultValueBufferExpressions.Add(replacedExpression); - innerReadExpressionMap[property] = CreateReadValueExpression( - replacedExpression.Type, resultValueBufferExpressions.Count - 1, property); + // inner nullable should do something different here + // Issue#17536 + ServerQueryExpression = Call( + EnumerableMethods.SelectManyWithCollectionSelector.MakeGenericMethod( + typeof(ValueBuffer), typeof(ValueBuffer), typeof(ValueBuffer)), + ServerQueryExpression, + Lambda(innerQueryExpression.ServerQueryExpression, CurrentParameter), + resultSelector); } - innerEntityProjection = new EntityProjectionExpression(innerEntityProjection.EntityType, innerReadExpressionMap); - - var collectionSelector = Lambda( - Call( - EnumerableMethods.DefaultIfEmptyWithArgument.MakeGenericMethod(typeof(ValueBuffer)), - collection, - New( - _valueBufferConstructor, - NewArrayInit( - typeof(object), - Enumerable.Range(0, resultValueBufferExpressions.Count - outerIndex).Select(i => Constant(null))))), - collectionParameter); - - resultSelector = Lambda( - New( - _valueBufferConstructor, - NewArrayInit( - typeof(object), - resultValueBufferExpressions - .Select(e => e.Type.IsValueType ? Convert(e, typeof(object)) : e) - .ToArray())), - outerParameter, - innerParameter); - - ServerQueryExpression = Call( - EnumerableMethods.SelectManyWithCollectionSelector.MakeGenericMethod( - groupTransparentIdentifierType, typeof(ValueBuffer), typeof(ValueBuffer)), - groupJoinExpression, - collectionSelector, - resultSelector); - - var entityShaper = new EntityShaperExpression(innerEntityProjection.EntityType, innerEntityProjection, nullable: true); - entityProjectionExpression.AddNavigationBinding(navigation, entityShaper); - - return entityShaper; - - EntityProjectionExpression CopyEntityProjectionToOuter(EntityProjectionExpression entityProjection) - { - var readExpressionMap = new Dictionary(); - foreach (var property in GetAllPropertiesInHierarchy(entityProjection.EntityType)) - { - var replacedExpression = replacingVisitor.Visit(entityProjection.BindProperty(property)); - resultValueBufferExpressions.Add(replacedExpression); - readExpressionMap[property] = CreateReadValueExpression( - replacedExpression.Type, resultValueBufferExpressions.Count - 1, property); - } + _projectionMapping = projectionMapping; + } - var newEntityProjection = new EntityProjectionExpression(entityProjection.EntityType, readExpressionMap); - if (ReferenceEquals(entityProjectionExpression, entityProjection)) - { - entityProjectionExpression = newEntityProjection; - } + private static int GetIndex(Expression expression) + => (int)((ConstantExpression)((MethodCallExpression)expression).Arguments[1]).Value!; - // Also lift nested entity projections - foreach (var navigation in entityProjection.EntityType.GetAllBaseTypes() - .Concat(entityProjection.EntityType.GetDerivedTypesInclusive()) - .SelectMany(t => t.GetDeclaredNavigations())) - { - var boundEntityShaperExpression = entityProjection.BindNavigation(navigation); - if (boundEntityShaperExpression != null) - { - var innerEntityProjection = (EntityProjectionExpression)boundEntityShaperExpression.ValueBufferExpression; - var newInnerEntityProjection = CopyEntityProjectionToOuter(innerEntityProjection); - boundEntityShaperExpression = boundEntityShaperExpression.Update(newInnerEntityProjection); - newEntityProjection.AddNavigationBinding(navigation, boundEntityShaperExpression); - } - } + private MethodCallExpression CreateReadValueExpression(Type type, int index, IPropertyBase? property) + => (MethodCallExpression)_valueBufferParameter.CreateValueBufferReadValueExpression(type, index, property); - return newEntityProjection; - } + private IEnumerable GetAllPropertiesInHierarchy(IEntityType entityType) + => entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive()) + .SelectMany(t => t.GetDeclaredProperties()); - static int GetValueBufferIndex(Expression expression) - => expression is ConditionalExpression conditionalExpression - ? GetValueBufferIndex(conditionalExpression.IfTrue) - : ((MethodCallExpression)expression).Arguments[1].GetConstantValue(); - } + private static IPropertyBase? InferPropertyFromInner(Expression expression) + => expression is MethodCallExpression methodCallExpression + && methodCallExpression.Method.IsGenericMethod + && methodCallExpression.Method.GetGenericMethodDefinition() == ExpressionExtensions.ValueBufferTryReadValueMethod + ? methodCallExpression.Arguments[2].GetConstantValue() + : null; - /// - /// This is an internal API that supports the Entity Framework Core infrastructure and not subject to - /// the same compatibility standards as public APIs. It may be changed or removed without notice in - /// any release. You should only use it directly in your code with extreme caution and knowing that - /// doing so can result in application failures when updating to a new Entity Framework Core release. - /// - void IPrintableExpression.Print(ExpressionPrinter expressionPrinter) + private static IEnumerable LeftJoin( + IEnumerable outer, + IEnumerable inner, + Func outerKeySelector, + Func innerKeySelector, + Func resultSelector, + TInner defaultValue) + => outer.GroupJoin(inner, outerKeySelector, innerKeySelector, (oe, ies) => new { oe, ies }) + .SelectMany(t => t.ies.DefaultIfEmpty(defaultValue), (t, i) => resultSelector(t.oe, i)); + + private MethodCallExpression MakeReadValueNullable(Expression expression) { - Check.NotNull(expressionPrinter, nameof(expressionPrinter)); + Check.DebugAssert(expression is MethodCallExpression, "Expression must be method call expression."); - expressionPrinter.AppendLine(nameof(InMemoryQueryExpression) + ": "); - using (expressionPrinter.Indent()) - { - expressionPrinter.AppendLine(nameof(ServerQueryExpression) + ": "); - using (expressionPrinter.Indent()) - { - expressionPrinter.Visit(ServerQueryExpression); - } + var methodCallExpression = (MethodCallExpression)expression; - expressionPrinter.AppendLine(); - expressionPrinter.AppendLine("ProjectionMapping:"); - using (expressionPrinter.Indent()) - { - foreach (var projectionMapping in _projectionMapping) - { - expressionPrinter.Append("Member: " + projectionMapping.Key + " Projection: "); - expressionPrinter.Visit(projectionMapping.Value); - expressionPrinter.AppendLine(","); - } - } - - expressionPrinter.AppendLine(); - } + return methodCallExpression.Type.IsNullableType() + ? methodCallExpression + : Call( + ExpressionExtensions.ValueBufferTryReadValueMethod.MakeGenericMethod(methodCallExpression.Type.MakeNullable()), + methodCallExpression.Arguments); } - private sealed class NullableReadValueExpressionVisitor : ExpressionVisitor + private sealed class ShaperRemappingExpressionVisitor : ExpressionVisitor { - protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression) + private readonly IDictionary _projectionMapping; + + public ShaperRemappingExpressionVisitor(IDictionary projectionMapping) { - Check.NotNull(methodCallExpression, nameof(methodCallExpression)); - - return methodCallExpression.Method.IsGenericMethod - && methodCallExpression.Method.GetGenericMethodDefinition() == ExpressionExtensions.ValueBufferTryReadValueMethod - && !methodCallExpression.Type.IsNullableType() - ? Call( - ExpressionExtensions.ValueBufferTryReadValueMethod.MakeGenericMethod(methodCallExpression.Type.MakeNullable()), - methodCallExpression.Arguments) - : base.VisitMethodCall(methodCallExpression); + _projectionMapping = projectionMapping; } - protected override Expression VisitConditional(ConditionalExpression conditionalExpression) + [return: CA.NotNullIfNotNull("expression")] + public override Expression? Visit(Expression? expression) { - Check.NotNull(conditionalExpression, nameof(conditionalExpression)); - - var test = Visit(conditionalExpression.Test); - var ifTrue = Visit(conditionalExpression.IfTrue); - var ifFalse = Visit(conditionalExpression.IfFalse); - - if (ifTrue.Type.IsNullableType() - && conditionalExpression.IfTrue.Type == ifTrue.Type.UnwrapNullableType() - && ifFalse is DefaultExpression) + if (expression is ProjectionBindingExpression projectionBindingExpression + && projectionBindingExpression.ProjectionMember != null) { - ifFalse = Default(ifTrue.Type); + var mappingValue = ((ConstantExpression)_projectionMapping[projectionBindingExpression.ProjectionMember]).Value; + return mappingValue is IDictionary indexMap + ? new ProjectionBindingExpression(projectionBindingExpression.QueryExpression, indexMap) + : mappingValue is int index + ? new ProjectionBindingExpression( + projectionBindingExpression.QueryExpression, index, projectionBindingExpression.Type) + : throw new InvalidOperationException(CoreStrings.UnknownEntity("ProjectionMapping")); } - return Condition(test, ifTrue, ifFalse); + return base.Visit(expression); } - - protected override Expression VisitUnary(UnaryExpression unaryExpression) - => unaryExpression.Update(Visit(unaryExpression.Operand)); } } } diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs index bd17f926c8e..07e45241520 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs @@ -156,7 +156,6 @@ private static ShapedQueryExpression CreateShapedQueryExpressionStatic(IEntityTy if (source.ShaperExpression is GroupByShaperExpression) { inMemoryQueryExpression.ReplaceProjectionMapping(new Dictionary()); - inMemoryQueryExpression.PushdownIntoSubquery(); } inMemoryQueryExpression.UpdateServerQueryExpression( @@ -191,7 +190,6 @@ private static ShapedQueryExpression CreateShapedQueryExpressionStatic(IEntityTy if (source.ShaperExpression is GroupByShaperExpression) { inMemoryQueryExpression.ReplaceProjectionMapping(new Dictionary()); - inMemoryQueryExpression.PushdownIntoSubquery(); } inMemoryQueryExpression.UpdateServerQueryExpression( @@ -304,7 +302,6 @@ private static ShapedQueryExpression CreateShapedQueryExpressionStatic(IEntityTy if (source.ShaperExpression is GroupByShaperExpression) { inMemoryQueryExpression.ReplaceProjectionMapping(new Dictionary()); - inMemoryQueryExpression.PushdownIntoSubquery(); } inMemoryQueryExpression.UpdateServerQueryExpression( @@ -346,7 +343,6 @@ private static ShapedQueryExpression CreateShapedQueryExpressionStatic(IEntityTy var inMemoryQueryExpression = (InMemoryQueryExpression)source.QueryExpression; - inMemoryQueryExpression.PushdownIntoSubquery(); inMemoryQueryExpression.UpdateServerQueryExpression( Expression.Call( EnumerableMethods.Distinct.MakeGenericMethod(inMemoryQueryExpression.CurrentParameter.Type), @@ -430,13 +426,14 @@ private static ShapedQueryExpression CreateShapedQueryExpressionStatic(IEntityTy var translatedKey = TranslateGroupingKey(remappedKeySelector); if (translatedKey != null) { - if (elementSelector != null) + var inMemoryQueryExpression = (InMemoryQueryExpression)source.QueryExpression; + var defaultElementSelector = elementSelector == null || elementSelector.Body == elementSelector.Parameters[0]; + if (!defaultElementSelector) { - source = TranslateSelect(source, elementSelector); + source = TranslateSelect(source, elementSelector!); } - var inMemoryQueryExpression = (InMemoryQueryExpression)source.QueryExpression; - var groupByShaper = inMemoryQueryExpression.ApplyGrouping(translatedKey, source.ShaperExpression); + var groupByShaper = inMemoryQueryExpression.ApplyGrouping(translatedKey, source.ShaperExpression, defaultElementSelector); if (resultSelector == null) { @@ -452,7 +449,6 @@ private static ShapedQueryExpression CreateShapedQueryExpressionStatic(IEntityTy newResultSelectorBody = ExpandWeakEntities(inMemoryQueryExpression, newResultSelectorBody); var newShaper = _projectionBindingExpressionVisitor.Translate(inMemoryQueryExpression, newResultSelectorBody); - inMemoryQueryExpression.PushdownIntoSubquery(); return source.UpdateShaperExpression(newShaper); } @@ -806,7 +802,6 @@ static bool IsConvertedToNullable(Expression outer, Expression inner) if (source.ShaperExpression is GroupByShaperExpression) { inMemoryQueryExpression.ReplaceProjectionMapping(new Dictionary()); - inMemoryQueryExpression.PushdownIntoSubquery(); } inMemoryQueryExpression.UpdateServerQueryExpression( @@ -974,14 +969,9 @@ protected override ShapedQueryExpression TranslateSelect(ShapedQueryExpression s var newSelectorBody = ReplacingExpressionVisitor.Replace( selector.Parameters.Single(), source.ShaperExpression, selector.Body); - var groupByQuery = source.ShaperExpression is GroupByShaperExpression; var queryExpression = (InMemoryQueryExpression)source.QueryExpression; var newShaper = _projectionBindingExpressionVisitor.Translate(queryExpression, newSelectorBody); - if (groupByQuery) - { - queryExpression.PushdownIntoSubquery(); - } return source.UpdateShaperExpression(newShaper); } @@ -1384,7 +1374,7 @@ protected override Expression VisitExtension(Expression extensionExpression) private Expression? TryExpand(Expression? source, MemberIdentity member) { source = source.UnwrapTypeConversion(out var convertedType); - if (!(source is EntityShaperExpression entityShaperExpression)) + if (source is not EntityShaperExpression entityShaperExpression) { return null; } diff --git a/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsQueryInMemoryTest.cs index f2a3a141276..b3e81983136 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsQueryInMemoryTest.cs @@ -14,35 +14,5 @@ public ComplexNavigationsQueryInMemoryTest(ComplexNavigationsQueryInMemoryFixtur { //TestLoggerFactory.TestOutputHelper = testOutputHelper; } - - [ConditionalFact(Skip = "issue #18194")] - public override void Member_pushdown_chain_3_levels_deep_entity() - { - base.Member_pushdown_chain_3_levels_deep_entity(); - } - - [ConditionalTheory(Skip = "issue #17620")] - public override Task Lift_projection_mapping_when_pushing_down_subquery(bool async) - { - return base.Lift_projection_mapping_when_pushing_down_subquery(async); - } - - [ConditionalTheory(Skip = "issue #19344")] - public override Task Select_subquery_single_nested_subquery(bool async) - { - return base.Select_subquery_single_nested_subquery(async); - } - - [ConditionalTheory(Skip = "issue #19344")] - public override Task Select_subquery_single_nested_subquery2(bool async) - { - return base.Select_subquery_single_nested_subquery2(async); - } - - [ConditionalTheory(Skip = "issue #17539")] - public override Task Union_over_entities_with_different_nullability(bool async) - { - return base.Union_over_entities_with_different_nullability(async); - } } } diff --git a/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryInMemoryTest.cs index c1ccee85fd1..8f32d8e8486 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/ComplexNavigationsSharedTypeQueryInMemoryTest.cs @@ -18,83 +18,5 @@ public ComplexNavigationsSharedTypeQueryInMemoryTest( { //TestLoggerFactory.TestOutputHelper = testOutputHelper; } - - [ConditionalTheory(Skip = "Issue#17539")] - public override Task Join_navigations_in_inner_selector_translated_without_collision(bool async) - { - return base.Join_navigations_in_inner_selector_translated_without_collision(async); - } - - [ConditionalTheory(Skip = "Issue#17539")] - public override Task Join_with_navigations_in_the_result_selector1(bool async) - { - return base.Join_with_navigations_in_the_result_selector1(async); - } - - [ConditionalTheory(Skip = "Issue#17539")] - public override Task Where_nav_prop_reference_optional1_via_DefaultIfEmpty(bool async) - { - return base.Where_nav_prop_reference_optional1_via_DefaultIfEmpty(async); - } - - [ConditionalTheory(Skip = "Issue#17539")] - public override Task Where_nav_prop_reference_optional2_via_DefaultIfEmpty(bool async) - { - return base.Where_nav_prop_reference_optional2_via_DefaultIfEmpty(async); - } - - [ConditionalTheory(Skip = "Issue#17539")] - public override Task Optional_navigation_propagates_nullability_to_manually_created_left_join2(bool async) - { - return base.Optional_navigation_propagates_nullability_to_manually_created_left_join2(async); - } - - [ConditionalTheory(Skip = "issue #17620")] - public override Task Lift_projection_mapping_when_pushing_down_subquery(bool async) - { - return base.Lift_projection_mapping_when_pushing_down_subquery(async); - } - - [ConditionalTheory(Skip = "issue #18912")] - public override Task OrderBy_collection_count_ThenBy_reference_navigation(bool async) - { - return base.OrderBy_collection_count_ThenBy_reference_navigation(async); - } - - [ConditionalTheory(Skip = "issue #19344")] - public override Task Select_subquery_single_nested_subquery(bool async) - { - return base.Select_subquery_single_nested_subquery(async); - } - - [ConditionalTheory(Skip = "issue #19344")] - public override Task Select_subquery_single_nested_subquery2(bool async) - { - return base.Select_subquery_single_nested_subquery2(async); - } - - [ConditionalTheory(Skip = "issue #19967")] - public override Task SelectMany_with_outside_reference_to_joined_table_correctly_translated_to_apply(bool async) - { - return base.SelectMany_with_outside_reference_to_joined_table_correctly_translated_to_apply(async); - } - - [ConditionalTheory(Skip = "issue #19967")] - public override Task Nested_SelectMany_correlated_with_join_table_correctly_translated_to_apply(bool async) - { - return base.Nested_SelectMany_correlated_with_join_table_correctly_translated_to_apply(async); - } - - [ConditionalTheory(Skip = "issue #19742")] - public override Task Contains_over_optional_navigation_with_null_column(bool async) - { - return base.Contains_over_optional_navigation_with_null_column(async); - } - - [ConditionalTheory(Skip = "issue #19742")] - public override Task Contains_over_optional_navigation_with_null_entity_reference(bool async) - { - return base.Contains_over_optional_navigation_with_null_entity_reference(async); - } } } diff --git a/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs index 5ccaa5b5735..02340f99ac6 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/GearsOfWarQueryInMemoryTest.cs @@ -23,32 +23,16 @@ public override Task Client_member_and_unsupported_string_Equals_in_the_same_que CoreStrings.QueryUnableToTranslateMember(nameof(Gear.IsMarcus), nameof(Gear))); } - [ConditionalFact(Skip = "issue #17537")] - public override void Include_on_GroupJoin_SelectMany_DefaultIfEmpty_with_coalesce_result1() - => base.Include_on_GroupJoin_SelectMany_DefaultIfEmpty_with_coalesce_result1(); - - [ConditionalFact(Skip = "issue #17537")] - public override void Include_on_GroupJoin_SelectMany_DefaultIfEmpty_with_coalesce_result2() - => base.Include_on_GroupJoin_SelectMany_DefaultIfEmpty_with_coalesce_result2(); - [ConditionalTheory(Skip = "issue #17540")] public override Task Null_semantics_is_correctly_applied_for_function_comparisons_that_take_arguments_from_optional_navigation_complex(bool async) => base.Null_semantics_is_correctly_applied_for_function_comparisons_that_take_arguments_from_optional_navigation_complex( async); - [ConditionalTheory(Skip = "issue #17620")] - public override Task Select_subquery_projecting_single_constant_inside_anonymous(bool async) - => base.Select_subquery_projecting_single_constant_inside_anonymous(async); - [ConditionalTheory(Skip = "issue #19683")] public override Task Group_by_on_StartsWith_with_null_parameter_as_argument(bool async) => base.Group_by_on_StartsWith_with_null_parameter_as_argument(async); - [ConditionalTheory(Skip = "issue #17537")] - public override Task SelectMany_predicate_with_non_equality_comparison_with_Take_doesnt_convert_to_join(bool async) - => base.SelectMany_predicate_with_non_equality_comparison_with_Take_doesnt_convert_to_join(async); - [ConditionalTheory(Skip = "issue #19584")] public override Task Cast_to_derived_followed_by_include_and_FirstOrDefault(bool async) => base.Cast_to_derived_followed_by_include_and_FirstOrDefault(async); diff --git a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindGroupByQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindGroupByQueryInMemoryTest.cs index 4ce933d9a49..65d8145d2d9 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/NorthwindGroupByQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/NorthwindGroupByQueryInMemoryTest.cs @@ -19,12 +19,6 @@ public NorthwindGroupByQueryInMemoryTest( //TestLoggerFactory.TestOutputHelper = testOutputHelper; } - [ConditionalTheory(Skip = "Issue#17536")] - public override Task Join_GroupBy_Aggregate_with_left_join(bool async) - { - return base.Join_GroupBy_Aggregate_with_left_join(async); - } - [ConditionalTheory(Skip = "Issue#24324")] public override Task Complex_query_with_groupBy_in_subquery4(bool async) { diff --git a/test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryTest.cs index 32e7642114e..69ec7040234 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/OwnedQueryInMemoryTest.cs @@ -16,12 +16,6 @@ public OwnedQueryInMemoryTest(OwnedQueryInMemoryFixture fixture, ITestOutputHelp //TestLoggerFactory.TestOutputHelper = testOutputHelper; } - [ConditionalTheory(Skip = "issue #19742")] - public override Task Projecting_collection_correlated_with_keyless_entity_after_navigation_works_using_parent_identifiers(bool async) - { - return base.Projecting_collection_correlated_with_keyless_entity_after_navigation_works_using_parent_identifiers(async); - } - public class OwnedQueryInMemoryFixture : OwnedQueryFixtureBase { protected override ITestStoreFactory TestStoreFactory diff --git a/test/EFCore.InMemory.FunctionalTests/Query/QueryBugsInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/QueryBugsInMemoryTest.cs index 1b2d808ddaa..7c2d3a089e9 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/QueryBugsInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/QueryBugsInMemoryTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Collections.ObjectModel; +using System.ComponentModel.DataAnnotations; using System.Linq; using System.Linq.Expressions; using System.Threading.Tasks; @@ -1035,6 +1036,8 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) } } + #endregion + #region Issue19253 [ConditionalFact] @@ -1213,7 +1216,7 @@ public virtual void Intersect_combines_nullability_of_entity_shapers() } } - public class MyContext19253 : DbContext + private class MyContext19253 : DbContext { public DbSet A { get; set; } public DbSet B { get; set; } @@ -1226,14 +1229,14 @@ protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) } } - public class JoinResult19253 + private class JoinResult19253 { public TLeft Left { get; set; } public TRight Right { get; set; } } - public class A19253 + private class A19253 { public int Id { get; set; } public string a { get; set; } @@ -1242,7 +1245,7 @@ public class A19253 } - public class B19253 + private class B19253 { public int Id { get; set; } public string b { get; set; } @@ -1270,8 +1273,6 @@ private static void Seed19253(MyContext19253 context) #endregion - #endregion - #region Issue23285 [ConditionalFact] @@ -1296,24 +1297,24 @@ private static void Seed23285(MyContext23285 context) } [Owned] - public class OwnedClass23285 + private class OwnedClass23285 { public string A { get; set; } public string B { get; set; } } - public class Root23285 + private class Root23285 { public int Id { get; set; } public OwnedClass23285 OwnedProp { get; set; } } - public class ChildA23285 : Root23285 + private class ChildA23285 : Root23285 { public bool Prop { get; set; } } - public class ChildB23285 : Root23285 + private class ChildB23285 : Root23285 { public double Prop { get; set; } } @@ -1363,13 +1364,13 @@ private static void Seed23687(MyContext23687 context) } [Owned] - public class OwnedClass23687 + private class OwnedClass23687 { public string A { get; set; } public string B { get; set; } } - public class Root23687 + private class Root23687 { public int Id1 { get; set; } public int Id2 { get; set; } @@ -1556,6 +1557,573 @@ protected override void OnModelCreating(ModelBuilder modelBuilder) #endregion + #region Issue18435 + + [ConditionalFact] + public virtual void Shared_owned_property_on_multiple_level_in_Select() + { + using (CreateScratch(Seed18435, "18435")) + { + using var context = new MyContext18435(); + + var result = context.TestEntities + .Select(x => new + { + x.Value, + A = x.Owned.First, + B = x.Owned.Second, + C = x.Child.Owned.First, + D = x.Child.Owned.Second + }).FirstOrDefault(); + + Assert.Equal("test", result.Value); + Assert.Equal(2, result.A); + Assert.Equal(4, result.B); + Assert.Equal(1, result.C); + Assert.Equal(3, result.D); + } + } + + private static void Seed18435(MyContext18435 context) + { + context.Add(new RootEntity18435 + { + Value = "test", + Owned = new TestOwned18435 + { + First = 2, + Second = 4, + AnotherValueType = "yay" + }, + Child = new ChildEntity18435 + { + Owned = new TestOwned18435 + { + First = 1, + Second = 3, + AnotherValueType = "nay" + } + } + }); + + context.SaveChanges(); + } + + private class RootEntity18435 + { + public int Id { get; set; } + public string Value { get; set; } + public TestOwned18435 Owned { get; set; } + public ChildEntity18435 Child { get; set; } + + } + + private class ChildEntity18435 + { + public int Id { get; set; } + public string Value { get; set; } + public TestOwned18435 Owned { get; set; } + } + + [Owned] + private class TestOwned18435 + { + public int First { get; set; } + public int Second { get; set; } + public string AnotherValueType { get; set; } + } + + private class MyContext18435 : DbContext + { + public DbSet TestEntities { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider) + .UseInMemoryDatabase("18435"); + } + } + + #endregion + + #region Issue19425 + + [ConditionalFact(Skip = "Issue#19425")] + public virtual void Non_nullable_cast_in_null_check() + { + using (CreateScratch(Seed19425, "19425")) + { + using var context = new MyContext19425(); + + var query = (from foo in context.FooTable + select new + { + Bar = foo.Bar != null ? (Bar19425)foo.Bar : (Bar19425?)null + }).ToList(); + + Assert.Single(query); + } + } + + private static void Seed19425(MyContext19425 context) + { + context.FooTable.Add(new FooTable19425 { Id = 1, Bar = null }); + + context.SaveChanges(); + } + + private enum Bar19425 + { + value1, + value2 + }; + + private class FooTable19425 + { + public int Id { get; set; } + public byte? Bar { get; set; } + } + + private class MyContext19425 : DbContext + { + public DbSet FooTable { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider) + .UseInMemoryDatabase("19425"); + } + } + + #endregion + + #region Issue19667 + + [ConditionalFact] + public virtual void Property_access_on_nullable_converted_scalar_type() + { + using (CreateScratch(Seed19667, "19667")) + { + using var context = new MyContext19667(); + + var query = context.Entities.OrderByDescending(e => e.Id).FirstOrDefault(p => p.Type.Date.Year == 2020); + + Assert.Equal(2, query.Id); + } + } + + private static void Seed19667(MyContext19667 context) + { + context.Entities.Add(new MyEntity19667 { Id = 1, Type = new MyType19667 { Date = new DateTime(2020, 1, 1) } }); + context.Entities.Add(new MyEntity19667 { Id = 2, Type = new MyType19667 { Date = new DateTime(2020, 1, 1).AddDays(1) } }); + + context.SaveChanges(); + } + + private class MyEntity19667 + { + public int Id { get; set; } + + public MyType19667 Type { get; set; } + } + + [Owned] + private class MyType19667 + { + public DateTime Date { get; set; } + } + + private class MyContext19667 : DbContext + { + public DbSet Entities { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider) + .UseInMemoryDatabase("19667"); + } + } + + #endregion + + #region Issue20359 + + [ConditionalFact] + public virtual void Changing_order_of_projection_in_anonymous_type_works() + { + using (CreateScratch(Seed20359, "20359")) + { + using var context = new MyContext20359(); + + var result1 = (from r in context.Root + select new + { + r.B.BValue, + r.A.Sub.AValue + }).FirstOrDefault(); + + var result2 = (from r in context.Root + select new + { + r.A.Sub.AValue, + r.B.BValue, + }).FirstOrDefault(); + + Assert.Equal(result1.BValue, result2.BValue); + } + } + + private static void Seed20359(MyContext20359 context) + { + var root = new Root20359() + { + A = new A20359() { Sub = new ASubClass20359() { AValue = "A Value" } }, + B = new B20359() { BValue = "B Value" } + }; + + context.Add(root); + + context.SaveChanges(); + } + + private class A20359 + { + public int Id { get; set; } + + public ASubClass20359 Sub { get; set; } + } + + private class ASubClass20359 + { + public string AValue { get; set; } + } + + private class B20359 + { + public string BValue { get; set; } + } + + private class Root20359 + { + public int Id { get; set; } + + public A20359 A { get; set; } + public B20359 B { get; set; } + } + + private class MyContext20359 : DbContext + { + public DbSet Root { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider) + .UseInMemoryDatabase("20359"); + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity(builder => + { + builder.OwnsOne(x => x.Sub); + }); + + modelBuilder.Entity(builder => + { + builder.OwnsOne(x => x.B); + }); + } + } + + #endregion + + #region Issue23360 + + [ConditionalFact] + public virtual void Union_with_different_property_name_using_same_anonymous_type() + { + using (CreateScratch(Seed23360, "23360")) + { + using var context = new MyContext23360(); + + var userQuery = context.User + .Select(u => new CommonSelectType23360() + { + // 1. FirstName, 2. LastName + FirstName = u.Forename, + LastName = u.Surname, + }); + + var customerQuery = context.Customer + .Select(c => new CommonSelectType23360() + { + // 1. LastName, 2. FirstName + LastName = c.FamilyName, + FirstName = c.GivenName, + }); + + var result = userQuery.Union(customerQuery).ToList(); + + Assert.Equal("Peter", result[0].FirstName); + Assert.Equal("Smith", result[0].LastName); + Assert.Equal("John", result[1].FirstName); + Assert.Equal("Doe", result[1].LastName); + } + } + + private static void Seed23360(MyContext23360 context) + { + context.User.Add(new User23360() + { + Forename = "Peter", + Surname = "Smith", + }); + + context.Customer.Add(new Customer23360() + { + GivenName = "John", + FamilyName = "Doe", + }); + + context.SaveChanges(); + } + + private class User23360 + { + [Key] + public int Key { get; set; } + + public string Forename { get; set; } + public string Surname { get; set; } + } + + private class Customer23360 + { + [Key] + public int Key { get; set; } + + public string GivenName { get; set; } + public string FamilyName { get; set; } + } + + private class CommonSelectType23360 + { + public string FirstName { get; set; } + public string LastName { get; set; } + } + + private class MyContext23360 : DbContext + { + public virtual DbSet User { get; set; } + public virtual DbSet Customer { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider) + .UseInMemoryDatabase("23360"); + } + } + + #endregion + + #region Issue18394 + + [ConditionalFact] + public virtual void Ordering_of_collection_result_is_correct() + { + using (CreateScratch(Seed18394, "18394")) + { + using var context = new MyContext18394(); + + var myA = context.As + .Where(x => x.Id == 1) + .Select(x => new ADto18394 + { + Id = x.Id, + PropertyB = (x.PropertyB == null) + ? null + : new BDto18394 + { + Id = x.PropertyB.Id, + PropertyCList = x.PropertyB.PropertyCList.Select( + y => new CDto18394 + { + Id = y.Id, + SomeText = y.SomeText + }).ToList() + } + }) + .FirstOrDefault(); + + Assert.Equal("TestText", myA.PropertyB.PropertyCList.First().SomeText); + } + } + + private static void Seed18394(MyContext18394 context) + { + var a = new A18394 + { + PropertyB = new B18394 + { + PropertyCList = new List + { + new C18394 { SomeText = "TestText" } + } + } + }; + context.As.Add(a); + + context.SaveChanges(); + } + + private class ADto18394 + { + public int Id { get; set; } + + public BDto18394 PropertyB { get; set; } + + public int PropertyBId { get; set; } + } + + private class BDto18394 + { + public int Id { get; set; } + + public List PropertyCList { get; set; } + } + + private class CDto18394 + { + public int Id { get; set; } + + public int CId { get; set; } + + public string SomeText { get; set; } + } + + private class A18394 + { + public int Id { get; set; } + + public B18394 PropertyB { get; set; } + + public int PropertyBId { get; set; } + } + + private class B18394 + { + public int Id { get; set; } + + public List PropertyCList { get; set; } + } + + private class C18394 + { + public int Id { get; set; } + + public int BId { get; set; } + + public string SomeText { get; set; } + + + public B18394 B { get; set; } + } + + private class MyContext18394 : DbContext + { + public DbSet As { get; set; } + public DbSet Bs { get; set; } + public DbSet Cs { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider) + .UseInMemoryDatabase("18394"); + } + } + + #endregion + + #region Issue23934 + + [ConditionalFact] + public virtual void Owned_entity_indexes_are_maintained_properly() + { + using (CreateScratch(Seed23934, "23934")) + { + using var context = new MyContext23934(); + + var criteria = new DateTime(2020, 1, 1); + + var data = context.Outers.Where(x => x.OwnedProp.At >= criteria || x.Inner.OwnedProp.At >= criteria).ToList(); + + Assert.Single(data); + } + } + + private static void Seed23934(MyContext23934 context) + { + var inner = new Inner23934 + { + Id = Guid.NewGuid(), + OwnedProp = new OwnedClass23934 { At = new DateTime(2020, 1, 1) } + }; + + var outer = new Outer23934 + { + Id = Guid.NewGuid(), + OwnedProp = new OwnedClass23934 { At = new DateTime(2020, 1, 1) }, + InnerId = inner.Id + }; + + context.Inners.Add(inner); + context.Outers.Add(outer); + + context.SaveChanges(); + } + + private class Outer23934 + { + public Guid Id { get; set; } + public OwnedClass23934 OwnedProp { get; set; } + public Guid InnerId { get; set; } + public Inner23934 Inner { get; set; } + } + + private class Inner23934 + { + public Guid Id { get; set; } + public OwnedClass23934 OwnedProp { get; set; } + } + + [Owned] + private class OwnedClass23934 + { + public DateTime At { get; set; } + } + + private class MyContext23934 : DbContext + { + public DbSet Outers { get; set; } + + public DbSet Inners { get; set; } + + protected override void OnConfiguring(DbContextOptionsBuilder optionsBuilder) + { + optionsBuilder + .UseInternalServiceProvider(InMemoryFixture.DefaultServiceProvider) + .UseInMemoryDatabase("23934"); + } + } + + #endregion + #region SharedHelper private static InMemoryTestStore CreateScratch(Action seed, string databaseName) diff --git a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs index 8e4f6a5ea0e..b151cec0a26 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs @@ -6067,5 +6067,34 @@ await AssertQuery( }, elementSorter: e => e.Id); } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task SelectMany_DefaultIfEmpty_multiple_times_with_joins_projecting_a_collection(bool async) + { + return AssertQuery( + async, + ss => from l4 in ss.Set().SelectMany(l1 => l1.OneToOne_Required_FK1.OneToOne_Optional_FK2.OneToMany_Required3.DefaultIfEmpty()) + join l2 in ss.Set().SelectMany(l4 => l4.OneToOne_Required_FK_Inverse4.OneToOne_Optional_FK_Inverse3.OneToMany_Required_Self2.DefaultIfEmpty()) on l4.Id equals l2.Id + join l3 in ss.Set().SelectMany(l4 => l4.OneToOne_Required_FK_Inverse4.OneToOne_Required_FK_Inverse3.OneToMany_Required2.DefaultIfEmpty()) on l2.Id equals l3.Id into grouping + from l3 in grouping.DefaultIfEmpty() + where l4.OneToMany_Optional_Inverse4.Name != "Foo" + orderby l2.OneToOne_Optional_FK2.Id + select new { Entity = l4, Collection = l2.OneToMany_Optional_Self2.Where(e => e.Id != 42).ToList(), Property = l3.OneToOne_Optional_FK_Inverse3.OneToOne_Required_FK2.Name }, + ss => from l4 in ss.Set().SelectMany(l1 => l1.OneToOne_Required_FK1.OneToOne_Optional_FK2.OneToMany_Required3.DefaultIfEmpty()) + join l2 in ss.Set().SelectMany(l4 => l4.OneToOne_Required_FK_Inverse4.OneToOne_Optional_FK_Inverse3.OneToMany_Required_Self2.DefaultIfEmpty()) on l4.Id equals l2.Id + join l3 in ss.Set().SelectMany(l4 => l4.OneToOne_Required_FK_Inverse4.OneToOne_Required_FK_Inverse3.OneToMany_Required2.DefaultIfEmpty()) on l2.Id equals l3.Id into grouping + from l3 in grouping.DefaultIfEmpty() + where l4.OneToMany_Optional_Inverse4.Name != "Foo" + orderby l2.OneToOne_Optional_FK2.MaybeScalar(e => e.Id) + select new { Entity = l4, Collection = l2.OneToMany_Optional_Self2.Where(e => e.Id != 42).ToList(), Property = l3.OneToOne_Optional_FK_Inverse3.OneToOne_Required_FK2.Name }, + assertOrder: true, + elementAsserter: (e, a) => + { + AssertEqual(e.Entity, a.Entity); + AssertCollection(e.Collection, a.Collection); + AssertEqual(e.Property, a.Property); + }); + } } } diff --git a/test/EFCore.Specification.Tests/Query/ComplexNavigationsSharedTypeQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexNavigationsSharedTypeQueryTestBase.cs index cf93b665cae..4ce86a3203b 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexNavigationsSharedTypeQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexNavigationsSharedTypeQueryTestBase.cs @@ -163,5 +163,11 @@ public override Task Multiple_collection_FirstOrDefault_followed_by_member_acces public override Task Project_shadow_properties(bool async) => Task.CompletedTask; + + public override Task SelectMany_DefaultIfEmpty_multiple_times_with_joins_projecting_a_collection(bool async) + { + // Navigations used are not mapped in shared type. + return Task.CompletedTask; + } } } diff --git a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs index 5d335a86e3e..707802cb90c 100644 --- a/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs @@ -1890,6 +1890,7 @@ public virtual void Include_on_GroupJoin_SelectMany_DefaultIfEmpty_with_coalesce join g2 in context.Gears on g1.LeaderNickname equals g2.Nickname into grouping from g2 in grouping.DefaultIfEmpty() + orderby g1.Nickname select g2 ?? g1; var result = query.ToList(); @@ -1906,6 +1907,7 @@ public virtual void Include_on_GroupJoin_SelectMany_DefaultIfEmpty_with_coalesce join g2 in context.Gears.Include(g => g.Weapons) on g1.LeaderNickname equals g2.Nickname into grouping from g2 in grouping.DefaultIfEmpty() + orderby g1.Nickname select g2 ?? g1; var result = query.ToList(); diff --git a/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs index a72287b4f35..b069da453d3 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs @@ -950,6 +950,23 @@ public virtual Task GroupBy_Constant_with_element_selector_Select_Sum_Min_Key_Ma e => e.Sum); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task GroupBy_constant_with_where_on_grouping_with_aggregate_operators(bool async) + { + return AssertQuery( + async, + ss => ss.Set().GroupBy(o => 1) + .OrderBy(g => g.Key) + .Select( + g => new { + Min = g.Where(i => 1 == g.Key).Min(o => o.OrderDate), + Max = g.Where(i => 1 == g.Key).Max(o => o.OrderDate), + Sum = g.Where(i => 1 == g.Key).Sum(o => o.OrderID), + Average = g.Where(i => 1 == g.Key).Average(o => o.OrderID), + })); + } + [ConditionalTheory] [MemberData(nameof(IsAsyncData))] public virtual Task GroupBy_param_Select_Sum_Min_Key_Max_Avg(bool async) diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index 9b58a899df4..193ca3bd3ca 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs @@ -6189,6 +6189,43 @@ FROM [InheritanceOne] AS [i] FROM [InheritanceLeafTwo] AS [i]"); } + public override async Task SelectMany_DefaultIfEmpty_multiple_times_with_joins_projecting_a_collection(bool async) + { + await base.SelectMany_DefaultIfEmpty_multiple_times_with_joins_projecting_a_collection(async); + + AssertSql( + @"SELECT [l2].[Id], [l2].[Level3_Optional_Id], [l2].[Level3_Required_Id], [l2].[Name], [l2].[OneToMany_Optional_Inverse4Id], [l2].[OneToMany_Optional_Self_Inverse4Id], [l2].[OneToMany_Required_Inverse4Id], [l2].[OneToMany_Required_Self_Inverse4Id], [l2].[OneToOne_Optional_PK_Inverse4Id], [l2].[OneToOne_Optional_Self4Id], [l14].[Name], [l].[Id], [l0].[Id], [l1].[Id], [t].[Id], [t].[Id0], [t].[Id1], [t].[Id2], [t0].[Id], [t0].[Id0], [t0].[Id1], [t0].[Id2], [l11].[Id], [l12].[Id], [l13].[Id], [l14].[Id], [t1].[Id], [t1].[Date], [t1].[Level1_Optional_Id], [t1].[Level1_Required_Id], [t1].[Name], [t1].[OneToMany_Optional_Inverse2Id], [t1].[OneToMany_Optional_Self_Inverse2Id], [t1].[OneToMany_Required_Inverse2Id], [t1].[OneToMany_Required_Self_Inverse2Id], [t1].[OneToOne_Optional_PK_Inverse2Id], [t1].[OneToOne_Optional_Self2Id] +FROM [LevelOne] AS [l] +LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Required_Id] +LEFT JOIN [LevelThree] AS [l1] ON [l0].[Id] = [l1].[Level2_Optional_Id] +LEFT JOIN [LevelFour] AS [l2] ON [l1].[Id] = [l2].[OneToMany_Required_Inverse4Id] +INNER JOIN ( + SELECT [l3].[Id], [l4].[Id] AS [Id0], [l5].[Id] AS [Id1], [l6].[Id] AS [Id2] + FROM [LevelFour] AS [l3] + INNER JOIN [LevelThree] AS [l4] ON [l3].[Level3_Required_Id] = [l4].[Id] + LEFT JOIN [LevelTwo] AS [l5] ON [l4].[Level2_Optional_Id] = [l5].[Id] + LEFT JOIN [LevelTwo] AS [l6] ON [l5].[Id] = [l6].[OneToMany_Required_Self_Inverse2Id] +) AS [t] ON [l2].[Id] = [t].[Id2] +LEFT JOIN ( + SELECT [l7].[Id], [l8].[Id] AS [Id0], [l9].[Id] AS [Id1], [l10].[Id] AS [Id2], [l10].[Level2_Optional_Id] AS [Level2_Optional_Id0] + FROM [LevelFour] AS [l7] + INNER JOIN [LevelThree] AS [l8] ON [l7].[Level3_Required_Id] = [l8].[Id] + INNER JOIN [LevelTwo] AS [l9] ON [l8].[Level2_Required_Id] = [l9].[Id] + LEFT JOIN [LevelThree] AS [l10] ON [l9].[Id] = [l10].[OneToMany_Required_Inverse3Id] +) AS [t0] ON [t].[Id2] = [t0].[Id2] +LEFT JOIN [LevelThree] AS [l11] ON [l2].[OneToMany_Optional_Inverse4Id] = [l11].[Id] +LEFT JOIN [LevelThree] AS [l12] ON [t].[Id2] = [l12].[Level2_Optional_Id] +LEFT JOIN [LevelTwo] AS [l13] ON [t0].[Level2_Optional_Id0] = [l13].[Id] +LEFT JOIN [LevelThree] AS [l14] ON [l13].[Id] = [l14].[Level2_Required_Id] +LEFT JOIN ( + SELECT [l15].[Id], [l15].[Date], [l15].[Level1_Optional_Id], [l15].[Level1_Required_Id], [l15].[Name], [l15].[OneToMany_Optional_Inverse2Id], [l15].[OneToMany_Optional_Self_Inverse2Id], [l15].[OneToMany_Required_Inverse2Id], [l15].[OneToMany_Required_Self_Inverse2Id], [l15].[OneToOne_Optional_PK_Inverse2Id], [l15].[OneToOne_Optional_Self2Id] + FROM [LevelTwo] AS [l15] + WHERE [l15].[Id] <> 42 +) AS [t1] ON [t].[Id2] = [t1].[OneToMany_Optional_Self_Inverse2Id] +WHERE ([l11].[Name] <> N'Foo') OR [l11].[Name] IS NULL +ORDER BY [l12].[Id], [l].[Id], [l0].[Id], [l1].[Id], [l2].[Id], [t].[Id], [t].[Id0], [t].[Id1], [t].[Id2], [t0].[Id], [t0].[Id0], [t0].[Id1], [t0].[Id2], [l11].[Id], [l13].[Id], [l14].[Id], [t1].[Id]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs index 9d82150d1f7..3e7637120ae 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs @@ -572,6 +572,15 @@ public override async Task GroupBy_Constant_with_element_selector_Select_Sum_Min FROM [Orders] AS [o]"); } + public override async Task GroupBy_constant_with_where_on_grouping_with_aggregate_operators(bool async) + { + await base.GroupBy_constant_with_where_on_grouping_with_aggregate_operators(async); + + AssertSql( + @"SELECT MIN([o].[OrderDate]) AS [Min], MAX([o].[OrderDate]) AS [Max], COALESCE(SUM([o].[OrderID]), 0) AS [Sum], AVG(CAST([o].[OrderID] AS float)) AS [Average] +FROM [Orders] AS [o]"); + } + public override async Task GroupBy_param_Select_Sum_Min_Key_Max_Avg(bool async) { await base.GroupBy_param_Select_Sum_Min_Key_Max_Avg(async);