diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs index 5b10b4e372c..4bd104b36eb 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryQueryableMethodTranslatingExpressionVisitor.cs @@ -1358,14 +1358,6 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp ?? methodCallExpression.Update(null!, new[] { source, methodCallExpression.Arguments[1] }); } - if (methodCallExpression.TryGetEFPropertyArguments(out source, out navigationName)) - { - source = Visit(source); - - return TryExpand(source, MemberIdentity.Create(navigationName)) - ?? methodCallExpression.Update(source, new[] { methodCallExpression.Arguments[0] }); - } - return base.VisitMethodCall(methodCallExpression); } diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 9adff93a033..8d35a34c53c 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; using System.Linq; using System.Linq.Expressions; using System.Reflection; @@ -1141,11 +1142,14 @@ private sealed class SharedTypeEntityExpandingExpressionVisitor : ExpressionVisi { private static readonly MethodInfo _objectEqualsMethodInfo = typeof(object).GetRequiredRuntimeMethod(nameof(object.Equals), typeof(object), typeof(object)); + private static readonly bool _useOldBehavior = AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue26592", out var enabled) + && enabled; private readonly RelationalSqlTranslatingExpressionVisitor _sqlTranslator; private readonly ISqlExpressionFactory _sqlExpressionFactory; private SelectExpression _selectExpression; + private DeferredOwnedExpansionRemovingVisitor _visitor; public SharedTypeEntityExpandingExpressionVisitor( RelationalSqlTranslatingExpressionVisitor sqlTranslator, @@ -1154,13 +1158,21 @@ public SharedTypeEntityExpandingExpressionVisitor( _sqlTranslator = sqlTranslator; _sqlExpressionFactory = sqlExpressionFactory; _selectExpression = null!; + _visitor = null!; } public Expression Expand(SelectExpression selectExpression, Expression lambdaBody) { _selectExpression = selectExpression; - return Visit(lambdaBody); + if (_useOldBehavior) + { + return Visit(lambdaBody); + } + + _visitor = new(_selectExpression); + + return _visitor.Visit(Visit(lambdaBody)); } protected override Expression VisitMember(MemberExpression memberExpression) @@ -1185,14 +1197,6 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp ?? methodCallExpression.Update(null!, new[] { source, methodCallExpression.Arguments[1] }); } - if (methodCallExpression.TryGetEFPropertyArguments(out source, out navigationName)) - { - source = Visit(source); - - return TryExpand(source, MemberIdentity.Create(navigationName)) - ?? methodCallExpression.Update(source, new[] { methodCallExpression.Arguments[1] }); - } - return base.VisitMethodCall(methodCallExpression); } @@ -1208,179 +1212,459 @@ protected override Expression VisitExtension(Expression extensionExpression) private Expression? TryExpand(Expression? source, MemberIdentity member) { - source = source.UnwrapTypeConversion(out var convertedType); - if (source is not EntityShaperExpression entityShaperExpression) + if (_useOldBehavior) { - return null; - } + source = source.UnwrapTypeConversion(out var convertedType); + if (source is not EntityShaperExpression entityShaperExpression) + { + return null; + } - var entityType = entityShaperExpression.EntityType; - if (convertedType != null) - { - entityType = entityType.GetRootType().GetDerivedTypesInclusive() - .FirstOrDefault(et => et.ClrType == convertedType); + var entityType = entityShaperExpression.EntityType; + if (convertedType != null) + { + entityType = entityType.GetRootType().GetDerivedTypesInclusive() + .FirstOrDefault(et => et.ClrType == convertedType); - if (entityType == null) + if (entityType == null) + { + return null; + } + } + + var navigation = member.MemberInfo != null + ? entityType.FindNavigation(member.MemberInfo) + : entityType.FindNavigation(member.Name!); + + if (navigation == null) { return null; } - } - var navigation = member.MemberInfo != null - ? entityType.FindNavigation(member.MemberInfo) - : entityType.FindNavigation(member.Name!); + var targetEntityType = navigation.TargetEntityType; + if (targetEntityType == null + || !targetEntityType.IsOwned()) + { + return null; + } - if (navigation == null) - { - return null; - } + var foreignKey = navigation.ForeignKey; + if (navigation.IsCollection) + { + var innerShapedQuery = CreateShapedQueryExpression( + targetEntityType, _sqlExpressionFactory.Select(targetEntityType)); - var targetEntityType = navigation.TargetEntityType; - if (targetEntityType == null - || !targetEntityType.IsOwned()) - { - return null; - } + var makeNullable = foreignKey.PrincipalKey.Properties + .Concat(foreignKey.Properties) + .Select(p => p.ClrType) + .Any(t => t.IsNullableType()); - var foreignKey = navigation.ForeignKey; - if (navigation.IsCollection) - { - var innerShapedQuery = CreateShapedQueryExpression( - targetEntityType, _sqlExpressionFactory.Select(targetEntityType)); - - var makeNullable = foreignKey.PrincipalKey.Properties - .Concat(foreignKey.Properties) - .Select(p => p.ClrType) - .Any(t => t.IsNullableType()); - - var innerSequenceType = innerShapedQuery.Type.GetSequenceType(); - var correlationPredicateParameter = Expression.Parameter(innerSequenceType); - - var outerKey = entityShaperExpression.CreateKeyValuesExpression( - navigation.IsOnDependent - ? foreignKey.Properties - : foreignKey.PrincipalKey.Properties, - makeNullable); - var innerKey = correlationPredicateParameter.CreateKeyValuesExpression( - navigation.IsOnDependent - ? foreignKey.PrincipalKey.Properties - : foreignKey.Properties, - makeNullable); - - var keyComparison = Expression.Call( - _objectEqualsMethodInfo, AddConvertToObject(outerKey), AddConvertToObject(innerKey)); - - var predicate = makeNullable - ? Expression.AndAlso( - outerKey is NewArrayExpression newArrayExpression - ? newArrayExpression.Expressions - .Select( - e => + var innerSequenceType = innerShapedQuery.Type.GetSequenceType(); + var correlationPredicateParameter = Expression.Parameter(innerSequenceType); + + var outerKey = entityShaperExpression.CreateKeyValuesExpression( + navigation.IsOnDependent + ? foreignKey.Properties + : foreignKey.PrincipalKey.Properties, + makeNullable); + var innerKey = correlationPredicateParameter.CreateKeyValuesExpression( + navigation.IsOnDependent + ? foreignKey.PrincipalKey.Properties + : foreignKey.Properties, + makeNullable); + + var keyComparison = Expression.Call( + _objectEqualsMethodInfo, AddConvertToObject(outerKey), AddConvertToObject(innerKey)); + + var predicate = makeNullable + ? Expression.AndAlso( + outerKey is NewArrayExpression newArrayExpression + ? newArrayExpression.Expressions + .Select( + e => { var left = (e as UnaryExpression)?.Operand ?? e; return Expression.NotEqual(left, Expression.Constant(null, left.Type)); }) - .Aggregate((l, r) => Expression.AndAlso(l, r)) - : Expression.NotEqual(outerKey, Expression.Constant(null, outerKey.Type)), - keyComparison) - : (Expression)keyComparison; + .Aggregate((l, r) => Expression.AndAlso(l, r)) + : Expression.NotEqual(outerKey, Expression.Constant(null, outerKey.Type)), + keyComparison) + : (Expression)keyComparison; - var correlationPredicate = Expression.Lambda(predicate, correlationPredicateParameter); + var correlationPredicate = Expression.Lambda(predicate, correlationPredicateParameter); - return Expression.Call( - QueryableMethods.Where.MakeGenericMethod(innerSequenceType), - innerShapedQuery, - Expression.Quote(correlationPredicate)); - } + return Expression.Call( + QueryableMethods.Where.MakeGenericMethod(innerSequenceType), + innerShapedQuery, + Expression.Quote(correlationPredicate)); + } - var entityProjectionExpression = (EntityProjectionExpression) - (entityShaperExpression.ValueBufferExpression is ProjectionBindingExpression projectionBindingExpression + var entityProjectionExpression = (EntityProjectionExpression) + (entityShaperExpression.ValueBufferExpression is ProjectionBindingExpression projectionBindingExpression ? _selectExpression.GetProjection(projectionBindingExpression) : entityShaperExpression.ValueBufferExpression); + var innerShaper = entityProjectionExpression.BindNavigation(navigation); + if (innerShaper == null) + { + // Owned types don't support inheritance See https://github.com/dotnet/efcore/issues/9630 + // So there is no handling for dependent having TPT + // If navigation is defined on derived type and entity type is part of TPT then we need to get ITableBase for derived type. + // TODO: The following code should also handle Function and SqlQuery mappings + var table = navigation.DeclaringEntityType.BaseType == null + || entityType.FindDiscriminatorProperty() != null + ? navigation.DeclaringEntityType.GetViewOrTableMappings().Single().Table + : navigation.DeclaringEntityType.GetViewOrTableMappings().Select(tm => tm.Table) + .Except(navigation.DeclaringEntityType.BaseType.GetViewOrTableMappings().Select(tm => tm.Table)) + .Single(); + if (table.GetReferencingRowInternalForeignKeys(foreignKey.PrincipalEntityType)?.Contains(foreignKey) == true) + { + // Mapped to same table + // We get identifying column to figure out tableExpression to pull columns from and nullability of most principal side + var identifyingColumn = entityProjectionExpression.BindProperty(entityType.FindPrimaryKey()!.Properties.First()); + var principalNullable = identifyingColumn.IsNullable + // Also make nullable if navigation is on derived type and and principal is TPT + // Since identifying PK would be non-nullable but principal can still be null + // Derived owned navigation does not de-dupe the PK column which for principal is from base table + // and for dependent on derived table + || (entityType.FindDiscriminatorProperty() == null + && navigation.DeclaringEntityType.IsStrictlyDerivedFrom(entityShaperExpression.EntityType)); + + var entityProjection = _selectExpression.GenerateWeakEntityProjectionExpression( + targetEntityType, table, identifyingColumn.Name, identifyingColumn.Table, principalNullable); + + if (entityProjection != null) + { + innerShaper = new RelationalEntityShaperExpression(targetEntityType, entityProjection, principalNullable); + } + } - var innerShaper = entityProjectionExpression.BindNavigation(navigation); - if (innerShaper == null) + if (innerShaper == null) + { + // InnerShaper is still null if either it is not table sharing or we failed to find table to pick data from + // So we find the table it is mapped to and generate join with it. + // Owned types don't support inheritance See https://github.com/dotnet/efcore/issues/9630 + // So there is no handling for dependent having TPT + table = targetEntityType.GetViewOrTableMappings().Single().Table; + var innerSelectExpression = _sqlExpressionFactory.Select(targetEntityType); + var innerShapedQuery = CreateShapedQueryExpression(targetEntityType, innerSelectExpression); + + var makeNullable = foreignKey.PrincipalKey.Properties + .Concat(foreignKey.Properties) + .Select(p => p.ClrType) + .Any(t => t.IsNullableType()); + + var outerKey = entityShaperExpression.CreateKeyValuesExpression( + navigation.IsOnDependent + ? foreignKey.Properties + : foreignKey.PrincipalKey.Properties, + makeNullable); + var innerKey = innerShapedQuery.ShaperExpression.CreateKeyValuesExpression( + navigation.IsOnDependent + ? foreignKey.PrincipalKey.Properties + : foreignKey.Properties, + makeNullable); + + var joinPredicate = _sqlTranslator.Translate(Expression.Equal(outerKey, innerKey))!; + _selectExpression.AddLeftJoin(innerSelectExpression, joinPredicate); + var leftJoinTable = _selectExpression.Tables.Last(); + + innerShaper = new RelationalEntityShaperExpression( + targetEntityType, + _selectExpression.GenerateWeakEntityProjectionExpression( + targetEntityType, table, null, leftJoinTable, nullable: true)!, + nullable: true); + } + + entityProjectionExpression.AddNavigationBinding(navigation, innerShaper); + } + + return innerShaper; + } + else { - // Owned types don't support inheritance See https://github.com/dotnet/efcore/issues/9630 - // So there is no handling for dependent having TPT - // If navigation is defined on derived type and entity type is part of TPT then we need to get ITableBase for derived type. - // TODO: The following code should also handle Function and SqlQuery mappings - var table = navigation.DeclaringEntityType.BaseType == null - || entityType.FindDiscriminatorProperty() != null - ? navigation.DeclaringEntityType.GetViewOrTableMappings().Single().Table - : navigation.DeclaringEntityType.GetViewOrTableMappings().Select(tm => tm.Table) - .Except(navigation.DeclaringEntityType.BaseType.GetViewOrTableMappings().Select(tm => tm.Table)) - .Single(); - if (table.GetReferencingRowInternalForeignKeys(foreignKey.PrincipalEntityType)?.Contains(foreignKey) == true) + source = source.UnwrapTypeConversion(out var convertedType); + var doee = source as DeferredOwnedExpansionExpression; + if (doee is not null) + { + source = _visitor.UnwrapDeferredEntityProjectionExpression(doee); + } + if (source is not EntityShaperExpression entityShaperExpression) + { + return null; + } + + var entityType = entityShaperExpression.EntityType; + if (convertedType != null) { - // Mapped to same table - // We get identifying column to figure out tableExpression to pull columns from and nullability of most principal side - var identifyingColumn = entityProjectionExpression.BindProperty(entityType.FindPrimaryKey()!.Properties.First()); - var principalNullable = identifyingColumn.IsNullable - // Also make nullable if navigation is on derived type and and principal is TPT - // Since identifying PK would be non-nullable but principal can still be null - // Derived owned navigation does not de-dupe the PK column which for principal is from base table - // and for dependent on derived table - || (entityType.FindDiscriminatorProperty() == null - && navigation.DeclaringEntityType.IsStrictlyDerivedFrom(entityShaperExpression.EntityType)); - - var entityProjection = _selectExpression.GenerateWeakEntityProjectionExpression( - targetEntityType, table, identifyingColumn.Name, identifyingColumn.Table, principalNullable); - - if (entityProjection != null) + entityType = entityType.GetRootType().GetDerivedTypesInclusive() + .FirstOrDefault(et => et.ClrType == convertedType); + + if (entityType == null) { - innerShaper = new RelationalEntityShaperExpression(targetEntityType, entityProjection, principalNullable); + return null; } } - if (innerShaper == null) + var navigation = member.MemberInfo != null + ? entityType.FindNavigation(member.MemberInfo) + : entityType.FindNavigation(member.Name!); + + if (navigation == null) { - // InnerShaper is still null if either it is not table sharing or we failed to find table to pick data from - // So we find the table it is mapped to and generate join with it. - // Owned types don't support inheritance See https://github.com/dotnet/efcore/issues/9630 - // So there is no handling for dependent having TPT - table = targetEntityType.GetViewOrTableMappings().Single().Table; - var innerSelectExpression = _sqlExpressionFactory.Select(targetEntityType); - var innerShapedQuery = CreateShapedQueryExpression(targetEntityType, innerSelectExpression); + return null; + } + + var targetEntityType = navigation.TargetEntityType; + if (targetEntityType == null + || !targetEntityType.IsOwned()) + { + return null; + } + + var foreignKey = navigation.ForeignKey; + if (navigation.IsCollection) + { + var innerShapedQuery = CreateShapedQueryExpression( + targetEntityType, _sqlExpressionFactory.Select(targetEntityType)); var makeNullable = foreignKey.PrincipalKey.Properties .Concat(foreignKey.Properties) .Select(p => p.ClrType) .Any(t => t.IsNullableType()); + var innerSequenceType = innerShapedQuery.Type.GetSequenceType(); + var correlationPredicateParameter = Expression.Parameter(innerSequenceType); + var outerKey = entityShaperExpression.CreateKeyValuesExpression( navigation.IsOnDependent ? foreignKey.Properties : foreignKey.PrincipalKey.Properties, makeNullable); - var innerKey = innerShapedQuery.ShaperExpression.CreateKeyValuesExpression( + var innerKey = correlationPredicateParameter.CreateKeyValuesExpression( navigation.IsOnDependent ? foreignKey.PrincipalKey.Properties : foreignKey.Properties, makeNullable); - var joinPredicate = _sqlTranslator.Translate(Expression.Equal(outerKey, innerKey))!; - _selectExpression.AddLeftJoin(innerSelectExpression, joinPredicate); - var leftJoinTable = _selectExpression.Tables.Last(); + var keyComparison = Expression.Call( + _objectEqualsMethodInfo, AddConvertToObject(outerKey), AddConvertToObject(innerKey)); + + var predicate = makeNullable + ? Expression.AndAlso( + outerKey is NewArrayExpression newArrayExpression + ? newArrayExpression.Expressions + .Select( + e => + { + var left = (e as UnaryExpression)?.Operand ?? e; + + return Expression.NotEqual(left, Expression.Constant(null, left.Type)); + }) + .Aggregate((l, r) => Expression.AndAlso(l, r)) + : Expression.NotEqual(outerKey, Expression.Constant(null, outerKey.Type)), + keyComparison) + : (Expression)keyComparison; + + var correlationPredicate = Expression.Lambda(predicate, correlationPredicateParameter); + + return Expression.Call( + QueryableMethods.Where.MakeGenericMethod(innerSequenceType), + innerShapedQuery, + Expression.Quote(correlationPredicate)); + } + + var entityProjectionExpression = GetEntityProjectionExpression(entityShaperExpression); + var innerShaper = entityProjectionExpression.BindNavigation(navigation); + if (innerShaper == null) + { + // Owned types don't support inheritance See https://github.com/dotnet/efcore/issues/9630 + // So there is no handling for dependent having TPT + // If navigation is defined on derived type and entity type is part of TPT then we need to get ITableBase for derived type. + // TODO: The following code should also handle Function and SqlQuery mappings + var table = navigation.DeclaringEntityType.BaseType == null + || entityType.FindDiscriminatorProperty() != null + ? navigation.DeclaringEntityType.GetViewOrTableMappings().Single().Table + : navigation.DeclaringEntityType.GetViewOrTableMappings().Select(tm => tm.Table) + .Except(navigation.DeclaringEntityType.BaseType.GetViewOrTableMappings().Select(tm => tm.Table)) + .Single(); + if (table.GetReferencingRowInternalForeignKeys(foreignKey.PrincipalEntityType)?.Contains(foreignKey) == true) + { + // Mapped to same table + // We get identifying column to figure out tableExpression to pull columns from and nullability of most principal side + var identifyingColumn = entityProjectionExpression.BindProperty(entityType.FindPrimaryKey()!.Properties.First()); + var principalNullable = identifyingColumn.IsNullable + // Also make nullable if navigation is on derived type and and principal is TPT + // Since identifying PK would be non-nullable but principal can still be null + // Derived owned navigation does not de-dupe the PK column which for principal is from base table + // and for dependent on derived table + || (entityType.FindDiscriminatorProperty() == null + && navigation.DeclaringEntityType.IsStrictlyDerivedFrom(entityShaperExpression.EntityType)); + + var entityProjection = _selectExpression.GenerateWeakEntityProjectionExpression( + targetEntityType, table, identifyingColumn.Name, identifyingColumn.Table, principalNullable); + + if (entityProjection != null) + { + innerShaper = new RelationalEntityShaperExpression(targetEntityType, entityProjection, principalNullable); + } + } + + if (innerShaper == null) + { + // InnerShaper is still null if either it is not table sharing or we failed to find table to pick data from + // So we find the table it is mapped to and generate join with it. + // Owned types don't support inheritance See https://github.com/dotnet/efcore/issues/9630 + // So there is no handling for dependent having TPT + table = targetEntityType.GetViewOrTableMappings().Single().Table; + var innerSelectExpression = _sqlExpressionFactory.Select(targetEntityType); + var innerShapedQuery = CreateShapedQueryExpression(targetEntityType, innerSelectExpression); + + var makeNullable = foreignKey.PrincipalKey.Properties + .Concat(foreignKey.Properties) + .Select(p => p.ClrType) + .Any(t => t.IsNullableType()); + + var outerKey = entityShaperExpression.CreateKeyValuesExpression( + navigation.IsOnDependent + ? foreignKey.Properties + : foreignKey.PrincipalKey.Properties, + makeNullable); + var innerKey = innerShapedQuery.ShaperExpression.CreateKeyValuesExpression( + navigation.IsOnDependent + ? foreignKey.PrincipalKey.Properties + : foreignKey.Properties, + makeNullable); + + var joinPredicate = _sqlTranslator.Translate(Expression.Equal(outerKey, innerKey))!; + // Following conditions should match conditions for pushdown on outer during SelectExpression.AddJoin method + var pushdownRequired = _selectExpression.Limit != null + || _selectExpression.Offset != null + || _selectExpression.IsDistinct + || _selectExpression.GroupBy.Count > 0; + _selectExpression.AddLeftJoin(innerSelectExpression, joinPredicate); + + // If pushdown was required on SelectExpression then we need to fetch the updated entity projection + if (pushdownRequired) + { + if (doee is not null) + { + entityShaperExpression = _visitor.UnwrapDeferredEntityProjectionExpression(doee); + } + entityProjectionExpression = GetEntityProjectionExpression(entityShaperExpression); + } + + var leftJoinTable = _selectExpression.Tables.Last(); - innerShaper = new RelationalEntityShaperExpression( - targetEntityType, - _selectExpression.GenerateWeakEntityProjectionExpression( - targetEntityType, table, null, leftJoinTable, nullable: true)!, - nullable: true); + innerShaper = new RelationalEntityShaperExpression( + targetEntityType, + _selectExpression.GenerateWeakEntityProjectionExpression( + targetEntityType, table, null, leftJoinTable, nullable: true)!, + nullable: true); + } + + entityProjectionExpression.AddNavigationBinding(navigation, innerShaper); } - entityProjectionExpression.AddNavigationBinding(navigation, innerShaper); + return doee is not null + ? doee.AddNavigation(targetEntityType, navigation) + : new DeferredOwnedExpansionExpression(targetEntityType, + (ProjectionBindingExpression)entityShaperExpression.ValueBufferExpression, + navigation); } - - return innerShaper; } private static Expression AddConvertToObject(Expression expression) => expression.Type.IsValueType ? Expression.Convert(expression, typeof(object)) : expression; + + private EntityProjectionExpression GetEntityProjectionExpression(EntityShaperExpression entityShaperExpression) + => entityShaperExpression.ValueBufferExpression switch + { + ProjectionBindingExpression projectionBindingExpression + => (EntityProjectionExpression)_selectExpression.GetProjection(projectionBindingExpression), + EntityProjectionExpression entityProjectionExpression => entityProjectionExpression, + _ => throw new InvalidOperationException(), + }; + + private sealed class DeferredOwnedExpansionExpression : Expression + { + private readonly IEntityType _entityType; + + public DeferredOwnedExpansionExpression( + IEntityType entityType, + ProjectionBindingExpression projectionBindingExpression, + INavigation navigation) + { + _entityType = entityType; + ProjectionBindingExpression = projectionBindingExpression; + NavigationChain = new List { navigation }; + } + + private DeferredOwnedExpansionExpression( + IEntityType entityType, + ProjectionBindingExpression projectionBindingExpression, + List navigationChain) + { + _entityType = entityType; + ProjectionBindingExpression = projectionBindingExpression; + NavigationChain = navigationChain; + } + + public ProjectionBindingExpression ProjectionBindingExpression { get; } + public List NavigationChain { get; } + + public DeferredOwnedExpansionExpression AddNavigation(IEntityType entityType, INavigation navigation) + { + var navigationChain = new List(NavigationChain.Count + 1); + navigationChain.AddRange(NavigationChain); + navigationChain.Add(navigation); + + return new DeferredOwnedExpansionExpression( + entityType, + ProjectionBindingExpression, + navigationChain); + } + + public override Type Type => _entityType.ClrType; + + public override ExpressionType NodeType => ExpressionType.Extension; + } + + private sealed class DeferredOwnedExpansionRemovingVisitor : ExpressionVisitor + { + private readonly SelectExpression _selectExpression; + + public DeferredOwnedExpansionRemovingVisitor(SelectExpression selectExpression) + { + _selectExpression = selectExpression; + } + + [return: NotNullIfNotNull("expression")] + public override Expression? Visit(Expression? expression) + => expression switch + { + DeferredOwnedExpansionExpression doee => UnwrapDeferredEntityProjectionExpression(doee), + // For the source entity shaper or owned collection expansion + EntityShaperExpression _ or ShapedQueryExpression _ => expression, + _ => base.Visit(expression), + }; + + public EntityShaperExpression UnwrapDeferredEntityProjectionExpression(DeferredOwnedExpansionExpression doee) + { + var entityProjection = (EntityProjectionExpression)_selectExpression.GetProjection(doee.ProjectionBindingExpression); + var entityShaper = entityProjection.BindNavigation(doee.NavigationChain[0])!; + + for (var i = 1; i < doee.NavigationChain.Count; i++) + { + entityProjection = (EntityProjectionExpression)entityShaper.ValueBufferExpression; + entityShaper = entityProjection.BindNavigation(doee.NavigationChain[i])!; + } + + return entityShaper; + } + } } private ShapedQueryExpression TranslateTwoParameterSelector(ShapedQueryExpression source, LambdaExpression resultSelector) diff --git a/test/EFCore.InMemory.FunctionalTests/Query/OwnedEntityQueryInMemoryTest.cs b/test/EFCore.InMemory.FunctionalTests/Query/OwnedEntityQueryInMemoryTest.cs index 8e220067c2f..b83637b7243 100644 --- a/test/EFCore.InMemory.FunctionalTests/Query/OwnedEntityQueryInMemoryTest.cs +++ b/test/EFCore.InMemory.FunctionalTests/Query/OwnedEntityQueryInMemoryTest.cs @@ -72,5 +72,33 @@ protected class Foo public virtual Bar? Bar { get; set; } } #nullable disable + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Owned_references_on_same_level_expanded_at_different_times_around_take(bool async) + { + var contextFactory = await InitializeAsync(seed: c => c.Seed()); + using var context = contextFactory.CreateContext(); + + await base.Owned_references_on_same_level_expanded_at_different_times_around_take_helper(context, async); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Owned_references_on_same_level_nested_expanded_at_different_times_around_take(bool async) + { + var contextFactory = await InitializeAsync(seed: c => c.Seed()); + using var context = contextFactory.CreateContext(); + + await base.Owned_references_on_same_level_nested_expanded_at_different_times_around_take_helper(context, async); + } + + protected class MyContext26592 : MyContext26592Base + { + public MyContext26592(DbContextOptions options) + : base(options) + { + } + } } } diff --git a/test/EFCore.Relational.Specification.Tests/Query/OwnedEntityQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/OwnedEntityQueryRelationalTestBase.cs index a4f604e6b12..26fab1ed17c 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/OwnedEntityQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/OwnedEntityQueryRelationalTestBase.cs @@ -164,5 +164,54 @@ protected class PublishTokenType25680 public string TokenGroupId { get; set; } public string IssuerName { get; set; } } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Owned_reference_mapped_to_different_table_updated_correctly_after_subquery_pushdown(bool async) + { + var contextFactory = await InitializeAsync(seed: c => c.Seed()); + using var context = contextFactory.CreateContext(); + + await base.Owned_references_on_same_level_expanded_at_different_times_around_take_helper(context, async); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Owned_reference_mapped_to_different_table_nested_updated_correctly_after_subquery_pushdown(bool async) + { + var contextFactory = await InitializeAsync(seed: c => c.Seed()); + using var context = contextFactory.CreateContext(); + + await base.Owned_references_on_same_level_nested_expanded_at_different_times_around_take_helper(context, async); + } + + protected class MyContext26592 : MyContext26592Base + { + public MyContext26592(DbContextOptions options) + : base(options) + { + } + + protected override void OnModelCreating(ModelBuilder modelBuilder) + { + modelBuilder.Entity( + b => + { + b.OwnsOne(e => e.CustomerData).ToTable("CustomerData"); + b.OwnsOne(e => e.SupplierData).ToTable("SupplierData"); + }); + + modelBuilder.Entity( + b => + { + b.OwnsOne(e => e.OwnedEntity, o => + { + o.ToTable("IntermediateOwnedEntity"); + o.OwnsOne(e => e.CustomerData).ToTable("IM_CustomerData"); + o.OwnsOne(e => e.SupplierData).ToTable("IM_SupplierData"); + }); + }); + } + } } } diff --git a/test/EFCore.Specification.Tests/Query/OwnedEntityQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/OwnedEntityQueryTestBase.cs index f549de2e367..3fe276e7ce2 100644 --- a/test/EFCore.Specification.Tests/Query/OwnedEntityQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/OwnedEntityQueryTestBase.cs @@ -308,5 +308,116 @@ protected class WarehouseModel public ICollection DestinationCountryCodes { get; set; } } + protected virtual async Task Owned_references_on_same_level_expanded_at_different_times_around_take_helper(MyContext26592Base context, bool async) + { + var query = context.Companies.Where(e => e.CustomerData != null).OrderBy(e => e.Id).Take(10); + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var company = Assert.Single(result); + Assert.Equal("Acme Inc.", company.Name); + Assert.Equal("Regular", company.CustomerData.AdditionalCustomerData); + Assert.Equal("Free shipping", company.SupplierData.AdditionalSupplierData); + } + + protected virtual async Task Owned_references_on_same_level_nested_expanded_at_different_times_around_take_helper( + MyContext26592Base context, bool async) + { + var query = context.Owners.Where(e => e.OwnedEntity.CustomerData != null).OrderBy(e => e.Id).Take(10); + var result = async + ? await query.ToListAsync() + : query.ToList(); + + var owner = Assert.Single(result); + Assert.Equal("Owner1", owner.Name); + Assert.Equal("Intermediate1", owner.OwnedEntity.Name); + Assert.Equal("IM Regular", owner.OwnedEntity.CustomerData.AdditionalCustomerData); + Assert.Equal("IM Free shipping", owner.OwnedEntity.SupplierData.AdditionalSupplierData); + } + + protected abstract class MyContext26592Base : DbContext + { + protected MyContext26592Base(DbContextOptions options) + : base(options) + { + } + + public DbSet Companies { get; set; } + public DbSet Owners { get; set; } + + public void Seed() + { + Add(new Company + { + Name = "Acme Inc.", + CustomerData = new CustomerData + { + AdditionalCustomerData = "Regular" + }, + SupplierData = new SupplierData + { + AdditionalSupplierData = "Free shipping" + } + }); + + Add(new Owner + { + Name = "Owner1", + OwnedEntity = new IntermediateOwnedEntity + { + Name = "Intermediate1", + CustomerData = new CustomerData + { + AdditionalCustomerData = "IM Regular" + }, + SupplierData = new SupplierData + { + AdditionalSupplierData = "IM Free shipping" + } + } + }); + + SaveChanges(); + } + } + + protected class Company + { + public int Id { get; set; } + public string Name { get; set; } + public CustomerData CustomerData { get; set; } + public SupplierData SupplierData { get; set; } + } + + [Owned] + protected class CustomerData + { + public int Id { get; set; } + public string AdditionalCustomerData { get; set; } + } + + [Owned] + protected class SupplierData + { + public int Id { get; set; } + public string AdditionalSupplierData { get; set; } + } + + protected class Owner + { + public int Id { get; set; } + public string Name { get; set; } + public IntermediateOwnedEntity OwnedEntity { get; set; } + } + + [Owned] + protected class IntermediateOwnedEntity + { + public int Id { get; set; } + public string Name { get; set; } + public CustomerData CustomerData { get; set; } + public SupplierData SupplierData { get; set; } + } } } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedEntityQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedEntityQuerySqlServerTest.cs index e30e0f64966..fb3ed254543 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/OwnedEntityQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/OwnedEntityQuerySqlServerTest.cs @@ -99,5 +99,44 @@ FROM [Location25680] AS [l] WHERE [l].[Id] = @__id_0 ORDER BY [l].[Id]"); } + + public override async Task Owned_reference_mapped_to_different_table_updated_correctly_after_subquery_pushdown(bool async) + { + await base.Owned_reference_mapped_to_different_table_updated_correctly_after_subquery_pushdown(async); + + AssertSql( + @"@__p_0='10' + +SELECT [t].[Id], [t].[Name], [t].[CompanyId], [t].[AdditionalCustomerData], [t].[Id0], [s].[CompanyId], [s].[AdditionalSupplierData], [s].[Id] +FROM ( + SELECT TOP(@__p_0) [c].[Id], [c].[Name], [c0].[CompanyId], [c0].[AdditionalCustomerData], [c0].[Id] AS [Id0] + FROM [Companies] AS [c] + LEFT JOIN [CustomerData] AS [c0] ON [c].[Id] = [c0].[CompanyId] + WHERE [c0].[CompanyId] IS NOT NULL + ORDER BY [c].[Id] +) AS [t] +LEFT JOIN [SupplierData] AS [s] ON [t].[Id] = [s].[CompanyId] +ORDER BY [t].[Id]"); + } + + public override async Task Owned_reference_mapped_to_different_table_nested_updated_correctly_after_subquery_pushdown(bool async) + { + await base.Owned_reference_mapped_to_different_table_nested_updated_correctly_after_subquery_pushdown(async); + + AssertSql( + @"@__p_0='10' + +SELECT [t].[Id], [t].[Name], [t].[OwnerId], [t].[Id0], [t].[Name0], [t].[IntermediateOwnedEntityOwnerId], [t].[AdditionalCustomerData], [t].[Id1], [i1].[IntermediateOwnedEntityOwnerId], [i1].[AdditionalSupplierData], [i1].[Id] +FROM ( + SELECT TOP(@__p_0) [o].[Id], [o].[Name], [i].[OwnerId], [i].[Id] AS [Id0], [i].[Name] AS [Name0], [i0].[IntermediateOwnedEntityOwnerId], [i0].[AdditionalCustomerData], [i0].[Id] AS [Id1] + FROM [Owners] AS [o] + LEFT JOIN [IntermediateOwnedEntity] AS [i] ON [o].[Id] = [i].[OwnerId] + LEFT JOIN [IM_CustomerData] AS [i0] ON [i].[OwnerId] = [i0].[IntermediateOwnedEntityOwnerId] + WHERE [i0].[IntermediateOwnedEntityOwnerId] IS NOT NULL + ORDER BY [o].[Id] +) AS [t] +LEFT JOIN [IM_SupplierData] AS [i1] ON [t].[OwnerId] = [i1].[IntermediateOwnedEntityOwnerId] +ORDER BY [t].[Id]"); + } } }