diff --git a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs index 6f148ac7df2..8a9f0de5a15 100644 --- a/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs +++ b/src/EFCore.InMemory/Query/Internal/InMemoryExpressionTranslatingExpressionVisitor.cs @@ -276,11 +276,14 @@ protected override Expression VisitExtension(Expression extensionExpression) case EntityShaperExpression entityShaperExpression: return new EntityReferenceExpression(entityShaperExpression); - case ProjectionBindingExpression projectionBindingExpression: - return projectionBindingExpression.ProjectionMember != null - ? ((InMemoryQueryExpression)projectionBindingExpression.QueryExpression) - .GetMappedProjection(projectionBindingExpression.ProjectionMember) - : QueryCompilationContext.NotTranslatedExpression; + case ProjectionBindingExpression projectionBindingExpression + when projectionBindingExpression.ProjectionMember != null: + return ((InMemoryQueryExpression)projectionBindingExpression.QueryExpression) + .GetMappedProjection(projectionBindingExpression.ProjectionMember); + + //case ProjectionBindingExpression projectionBindingExpression + // when projectionBindingExpression.Index is int index: + // return ((InMemoryQueryExpression)projectionBindingExpression.QueryExpression).Projection[index]; case InMemoryGroupByShaperExpression inMemoryGroupByShaperExpression: return new GroupingElementExpression( diff --git a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs index 0094f29d76b..698cc218679 100644 --- a/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs @@ -1441,8 +1441,13 @@ outerKey is NewArrayExpression newArrayExpression || (entityType.FindDiscriminatorProperty() == null && navigation.DeclaringEntityType.IsStrictlyDerivedFrom(entityShaperExpression.EntityType)); - innerShaper = _selectExpression.GenerateWeakEntityShaper( + var entityProjection = _selectExpression.GenerateWeakEntityProjectionExpression( targetEntityType, table, identifyingColumn.Name, identifyingColumn.Table, principalNullable); + + if (entityProjection != null) + { + innerShaper = new RelationalEntityShaperExpression(targetEntityType, entityProjection, principalNullable); + } } if (innerShaper == null) @@ -1475,8 +1480,11 @@ outerKey is NewArrayExpression newArrayExpression _selectExpression.AddLeftJoin(innerSelectExpression, joinPredicate); var leftJoinTable = ((LeftJoinExpression)_selectExpression.Tables.Last()).Table; - innerShaper = _selectExpression.GenerateWeakEntityShaper( - targetEntityType, table, null, leftJoinTable, makeNullable: true)!; + innerShaper = new RelationalEntityShaperExpression( + targetEntityType, + _selectExpression.GenerateWeakEntityProjectionExpression( + targetEntityType, table, null, leftJoinTable, nullable: true)!, + nullable: true); } entityProjectionExpression.AddNavigationBinding(navigation, innerShaper); diff --git a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs index e83b641f1de..66b2e4c860f 100644 --- a/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs +++ b/src/EFCore.Relational/Query/RelationalSqlTranslatingExpressionVisitor.cs @@ -409,11 +409,14 @@ protected override Expression VisitExtension(Expression extensionExpression) case EntityShaperExpression entityShaperExpression: return new EntityReferenceExpression(entityShaperExpression); - case ProjectionBindingExpression projectionBindingExpression: - return projectionBindingExpression.ProjectionMember != null - ? ((SelectExpression)projectionBindingExpression.QueryExpression) - .GetMappedProjection(projectionBindingExpression.ProjectionMember) - : QueryCompilationContext.NotTranslatedExpression; + case ProjectionBindingExpression projectionBindingExpression + when projectionBindingExpression.ProjectionMember != null: + return ((SelectExpression)projectionBindingExpression.QueryExpression) + .GetMappedProjection(projectionBindingExpression.ProjectionMember); + + //case ProjectionBindingExpression projectionBindingExpression + // when projectionBindingExpression.Index is int index: + // return ((SelectExpression)projectionBindingExpression.QueryExpression).Projection[index].Expression; case GroupByShaperExpression groupByShaperExpression: return new GroupingElementExpression(groupByShaperExpression.ElementSelector); diff --git a/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs index 6a24c5f5f20..729b23a9fc6 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/ColumnExpression.cs @@ -3,10 +3,6 @@ using System; using System.Diagnostics; -using System.Linq; -using System.Linq.Expressions; -using Microsoft.EntityFrameworkCore.Infrastructure; -using Microsoft.EntityFrameworkCore.Metadata; using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; @@ -20,101 +16,47 @@ namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions /// This type is typically used by database providers (and other extensions). It is generally /// not used in application code. /// - /// - /// This class is not publicly constructable. If this is a problem for your application or provider, then please file - /// an issue at https://github.com/dotnet/efcore. - /// /// [DebuggerDisplay("{DebuggerDisplay(),nq}")] - // Class is sealed because there are no public/protected constructors. Can be unsealed if this is changed. - public sealed class ColumnExpression : SqlExpression + public abstract class ColumnExpression : SqlExpression { - private readonly TableReferenceExpression _table; - - internal ColumnExpression(IProperty property, IColumnBase column, TableReferenceExpression table, bool nullable) - : this( - column.Name, - table, - property.ClrType.UnwrapNullableType(), - column.PropertyMappings.First(m => m.Property == property).TypeMapping, - nullable || column.IsNullable) - { - } - - internal ColumnExpression(ProjectionExpression subqueryProjection, TableReferenceExpression table) - : this( - subqueryProjection.Alias, table, - subqueryProjection.Type, subqueryProjection.Expression.TypeMapping!, - IsNullableProjection(subqueryProjection)) - { - } - - private static bool IsNullableProjection(ProjectionExpression projectionExpression) - => projectionExpression.Expression switch - { - ColumnExpression columnExpression => columnExpression.IsNullable, - SqlConstantExpression sqlConstantExpression => sqlConstantExpression.Value == null, - _ => true, - }; - - private ColumnExpression(string name, TableReferenceExpression table, Type type, RelationalTypeMapping typeMapping, bool nullable) + /// + /// Creates a new instance of the class. + /// + /// The of the expression. + /// The associated with the expression. + protected ColumnExpression(Type type, RelationalTypeMapping? typeMapping) : base(type, typeMapping) { - Check.NotEmpty(name, nameof(name)); - Check.NotNull(table, nameof(table)); - Check.NotEmpty(table.Alias, $"{nameof(table)}.{nameof(table.Alias)}"); - - Name = name; - _table = table; - IsNullable = nullable; } /// /// The name of the column. /// - public string Name { get; } + public abstract string Name { get; } /// /// The table from which column is being referenced. /// - public TableExpressionBase Table => _table.Table; + public abstract TableExpressionBase Table { get; } /// /// The alias of the table from which column is being referenced. /// - public string TableAlias => _table.Alias; + public abstract string TableAlias { get; } /// /// The bool value indicating if this column can have null values. /// - public bool IsNullable { get; } - - /// - protected override Expression VisitChildren(ExpressionVisitor visitor) - { - Check.NotNull(visitor, nameof(visitor)); - - return this; - } + public abstract bool IsNullable { get; } /// /// Makes this column nullable. /// /// A new expression which has property set to true. - public ColumnExpression MakeNullable() - => new(Name, _table, Type, TypeMapping!, true); + public abstract ColumnExpression MakeNullable(); - /// - /// 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. - /// - [EntityFrameworkInternal] - public void UpdateTableReference(SelectExpression oldSelect, SelectExpression newSelect) - => _table.UpdateTableReference(oldSelect, newSelect); - - /// + /// protected override void Print(ExpressionPrinter expressionPrinter) { Check.NotNull(expressionPrinter, nameof(expressionPrinter)); @@ -123,23 +65,6 @@ protected override void Print(ExpressionPrinter expressionPrinter) expressionPrinter.Append(Name); } - /// - public override bool Equals(object? obj) - => obj != null - && (ReferenceEquals(this, obj) - || obj is ColumnExpression columnExpression - && Equals(columnExpression)); - - private bool Equals(ColumnExpression columnExpression) - => base.Equals(columnExpression) - && Name == columnExpression.Name - && _table.Equals(columnExpression._table) - && IsNullable == columnExpression.IsNullable; - - /// - public override int GetHashCode() - => HashCode.Combine(base.GetHashCode(), Name, _table, IsNullable); - private string DebuggerDisplay() => $"{TableAlias}.{Name}"; } diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs index 983f1d81e6e..8a963f49dad 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.Helper.cs @@ -4,9 +4,11 @@ using System; using System.Collections.Generic; using System.Diagnostics.CodeAnalysis; +using System.Linq; using System.Linq.Expressions; using Microsoft.EntityFrameworkCore.ChangeTracking; using Microsoft.EntityFrameworkCore.Metadata; +using Microsoft.EntityFrameworkCore.Storage; using Microsoft.EntityFrameworkCore.Utilities; namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions @@ -194,7 +196,7 @@ when _mappings.TryGetValue(sqlExpression, out var outer): when _subquery.ContainsTableReference(columnExpression): var index = _subquery.AddToProjection(columnExpression); var projectionExpression = _subquery._projection[index]; - return new ColumnExpression(projectionExpression, _tableReferenceExpression); + return new ConcreteColumnExpression(projectionExpression, _tableReferenceExpression); default: return base.Visit(expression); @@ -296,7 +298,7 @@ public TableReferenceUpdatingExpressionVisitor(SelectExpression oldSelect, Selec [return: NotNullIfNotNull("expression")] public override Expression? Visit(Expression? expression) { - if (expression is ColumnExpression columnExpression) + if (expression is ConcreteColumnExpression columnExpression) { columnExpression.UpdateTableReference(_oldSelect, _newSelect); } @@ -338,5 +340,134 @@ public AliasUniquefier(HashSet usedAliases) return base.Visit(expression); } } + + private sealed class TableReferenceExpression : Expression + { + private SelectExpression _selectExpression; + + public TableReferenceExpression(SelectExpression selectExpression, string alias) + { + _selectExpression = selectExpression; + Alias = alias; + } + + public TableExpressionBase Table + => _selectExpression.Tables.Single( + e => string.Equals((e as JoinExpressionBase)?.Table.Alias ?? e.Alias, Alias, StringComparison.OrdinalIgnoreCase)); + + public string Alias { get; internal set; } + + public override Type Type => typeof(object); + + public override ExpressionType NodeType => ExpressionType.Extension; + public void UpdateTableReference(SelectExpression oldSelect, SelectExpression newSelect) + { + if (ReferenceEquals(oldSelect, _selectExpression)) + { + _selectExpression = newSelect; + } + } + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is TableReferenceExpression tableReferenceExpression + && Equals(tableReferenceExpression)); + + // Since table reference is owned by SelectExpression, the select expression should be the same reference if they are matching. + // That means we also don't need to compute the hashcode for it. + // This allows us to break the cycle in computation when traversing this graph. + private bool Equals(TableReferenceExpression tableReferenceExpression) + => string.Equals(Alias, tableReferenceExpression.Alias, StringComparison.OrdinalIgnoreCase) + && ReferenceEquals(_selectExpression, tableReferenceExpression._selectExpression); + + /// + public override int GetHashCode() + => Alias.GetHashCode(); + } + + private sealed class ConcreteColumnExpression : ColumnExpression + { + private readonly TableReferenceExpression _table; + + public ConcreteColumnExpression(IProperty property, IColumnBase column, TableReferenceExpression table, bool nullable) + : this( + column.Name, + table, + property.ClrType.UnwrapNullableType(), + column.PropertyMappings.First(m => m.Property == property).TypeMapping, + nullable || column.IsNullable) + { + } + + public ConcreteColumnExpression(ProjectionExpression subqueryProjection, TableReferenceExpression table) + : this( + subqueryProjection.Alias, table, + subqueryProjection.Type, subqueryProjection.Expression.TypeMapping!, + IsNullableProjection(subqueryProjection)) + { + } + + private static bool IsNullableProjection(ProjectionExpression projectionExpression) + => projectionExpression.Expression switch + { + ColumnExpression columnExpression => columnExpression.IsNullable, + SqlConstantExpression sqlConstantExpression => sqlConstantExpression.Value == null, + _ => true, + }; + + private ConcreteColumnExpression( + string name, TableReferenceExpression table, Type type, RelationalTypeMapping typeMapping, bool nullable) + : base(type, typeMapping) + { + Check.NotEmpty(name, nameof(name)); + Check.NotNull(table, nameof(table)); + Check.NotEmpty(table.Alias, $"{nameof(table)}.{nameof(table.Alias)}"); + + Name = name; + _table = table; + IsNullable = nullable; + } + + public override string Name { get; } + + public override TableExpressionBase Table => _table.Table; + + public override string TableAlias => _table.Alias; + + public override bool IsNullable { get; } + + /// + protected override Expression VisitChildren(ExpressionVisitor visitor) + { + Check.NotNull(visitor, nameof(visitor)); + + return this; + } + + public override ConcreteColumnExpression MakeNullable() + => new(Name, _table, Type, TypeMapping!, true); + + public void UpdateTableReference(SelectExpression oldSelect, SelectExpression newSelect) + => _table.UpdateTableReference(oldSelect, newSelect); + + /// + public override bool Equals(object? obj) + => obj != null + && (ReferenceEquals(this, obj) + || obj is ConcreteColumnExpression concreteColumnExpression + && Equals(concreteColumnExpression)); + + private bool Equals(ConcreteColumnExpression concreteColumnExpression) + => base.Equals(concreteColumnExpression) + && Name == concreteColumnExpression.Name + && _table.Equals(concreteColumnExpression._table) + && IsNullable == concreteColumnExpression.IsNullable; + + /// + public override int GetHashCode() + => HashCode.Combine(base.GetHashCode(), Name, _table, IsNullable); + } } } diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index 258de2dd521..a0cabe3856b 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -1279,7 +1279,7 @@ private void ApplySetOperation(SetOperationType setOperationType, SelectExpressi var innerProjection2 = new ProjectionExpression(innerColumn2, alias); select1._projection.Add(innerProjection1); select2._projection.Add(innerProjection2); - var outerProjection = new ColumnExpression(innerProjection1, tableReferenceExpression); + var outerProjection = new ConcreteColumnExpression(innerProjection1, tableReferenceExpression); if (IsNullableProjection(innerProjection1) || IsNullableProjection(innerProjection2)) @@ -1337,7 +1337,7 @@ void HandleEntityProjection( var innerProjection = new ProjectionExpression(column1, alias); select1._projection.Add(innerProjection); select2._projection.Add(new ProjectionExpression(column2, alias)); - var outerExpression = new ColumnExpression(innerProjection, tableReferenceExpression); + var outerExpression = new ConcreteColumnExpression(innerProjection, tableReferenceExpression); if (column1.IsNullable || column2.IsNullable) { @@ -1374,7 +1374,7 @@ void HandleEntityProjection( var innerProjection = new ProjectionExpression(projection1.DiscriminatorExpression, alias); select1._projection.Add(innerProjection); select2._projection.Add(new ProjectionExpression(projection2.DiscriminatorExpression, alias)); - discriminatorExpression = new ColumnExpression(innerProjection, tableReferenceExpression); + discriminatorExpression = new ConcreteColumnExpression(innerProjection, tableReferenceExpression); } _projectionMapping[projectionMember] = new EntityProjectionExpression( @@ -1477,8 +1477,8 @@ public void ApplyDefaultIfEmpty(ISqlExpressionFactory sqlExpressionFactory) /// doing so can result in application failures when updating to a new Entity Framework Core release. /// [EntityFrameworkInternal] - internal RelationalEntityShaperExpression? GenerateWeakEntityShaper( - IEntityType entityType, ITableBase table, string? columnName, TableExpressionBase tableExpressionBase, bool makeNullable = true) + public EntityProjectionExpression? GenerateWeakEntityProjectionExpression( + IEntityType entityType, ITableBase table, string? columnName, TableExpressionBase tableExpressionBase, bool nullable = true) { if (columnName == null) { @@ -1486,18 +1486,16 @@ public void ApplyDefaultIfEmpty(ISqlExpressionFactory sqlExpressionFactory) var propertyExpressions = GetPropertyExpressionsFromJoinedTable( entityType, table, FindTableReference(this, tableExpressionBase)); - return new RelationalEntityShaperExpression( - entityType, new EntityProjectionExpression(entityType, propertyExpressions), makeNullable); + return new EntityProjectionExpression(entityType, propertyExpressions); } else { var propertyExpressions = GetPropertyExpressionFromSameTable( - entityType, table, this, tableExpressionBase, columnName, makeNullable); + entityType, table, this, tableExpressionBase, columnName, nullable); return propertyExpressions == null ? null - : new RelationalEntityShaperExpression( - entityType, new EntityProjectionExpression(entityType, propertyExpressions), makeNullable); + : new EntityProjectionExpression(entityType, propertyExpressions); } static TableReferenceExpression FindTableReference(SelectExpression selectExpression, TableExpressionBase tableExpression) @@ -1531,7 +1529,7 @@ static TableReferenceExpression FindTableReference(SelectExpression selectExpres .GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive()) .SelectMany(t => t.GetDeclaredProperties())) { - propertyExpressions[property] = new ColumnExpression( + propertyExpressions[property] = new ConcreteColumnExpression( property, table.FindColumn(property)!, tableReferenceExpression, nullable || !property.IsPrimaryKey()); } @@ -1555,7 +1553,7 @@ static TableReferenceExpression FindTableReference(SelectExpression selectExpres var tableReferenceExpression = FindTableReference(selectExpression, subquery); foreach (var item in subqueryPropertyExpressions) { - newPropertyExpressions[item.Key] = new ColumnExpression( + newPropertyExpressions[item.Key] = new ConcreteColumnExpression( subquery.Projection[subquery.AddToProjection(item.Value)], tableReferenceExpression); } @@ -1575,7 +1573,7 @@ static IReadOnlyDictionary GetPropertyExpressionsFr .GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive()) .SelectMany(t => t.GetDeclaredProperties())) { - propertyExpressions[property] = new ColumnExpression( + propertyExpressions[property] = new ConcreteColumnExpression( property, table.FindColumn(property)!, tableReferenceExpression, nullable: true); } @@ -2727,16 +2725,16 @@ private static IEnumerable GetAllPropertiesInHierarchy(IEntityType en => entityType.GetAllBaseTypes().Concat(entityType.GetDerivedTypesInclusive()) .SelectMany(t => t.GetDeclaredProperties()); - private static ColumnExpression CreateColumnExpression( + private static ConcreteColumnExpression CreateColumnExpression( IProperty property, ITableBase table, TableReferenceExpression tableExpression, bool nullable) => new(property, table.FindColumn(property)!, tableExpression, nullable); - private ColumnExpression GenerateOuterColumn( + private ConcreteColumnExpression GenerateOuterColumn( TableReferenceExpression tableReferenceExpression, SqlExpression projection, string? alias = null) { var index = AddToProjection(projection, alias); - return new ColumnExpression(_projection[index], tableReferenceExpression); + return new ConcreteColumnExpression(_projection[index], tableReferenceExpression); } private bool ContainsTableReference(ColumnExpression column) diff --git a/src/EFCore.Relational/Query/SqlExpressions/TableReferenceExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/TableReferenceExpression.cs deleted file mode 100644 index 022f3785a2b..00000000000 --- a/src/EFCore.Relational/Query/SqlExpressions/TableReferenceExpression.cs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. - -using System; -using System.Linq; -using System.Linq.Expressions; - -namespace Microsoft.EntityFrameworkCore.Query.SqlExpressions -{ -#pragma warning disable CS1591 - // TODO: Make this nested inside SelectExpression and same for ColumnExpression - public class TableReferenceExpression : Expression - { - private SelectExpression _selectExpression; - - public TableReferenceExpression(SelectExpression selectExpression, string alias) - { - _selectExpression = selectExpression; - Alias = alias; - } - - public virtual TableExpressionBase Table - => _selectExpression.Tables.Single( - e => string.Equals((e as JoinExpressionBase)?.Table.Alias ?? e.Alias, Alias, StringComparison.OrdinalIgnoreCase)); - - public virtual string Alias { get; internal set; } - - public override Type Type => typeof(object); - - public override ExpressionType NodeType => ExpressionType.Extension; - public virtual void UpdateTableReference(SelectExpression oldSelect, SelectExpression newSelect) - { - if (ReferenceEquals(oldSelect, _selectExpression)) - { - _selectExpression = newSelect; - } - } - - /// - public override bool Equals(object? obj) - => obj != null - && (ReferenceEquals(this, obj) - || obj is TableReferenceExpression tableReferenceExpression - && Equals(tableReferenceExpression)); - - // Since table reference is owned by SelectExpression, the select expression should be the same reference if they are matching. - // That means we also don't need to compute the hashcode for it. - // This allows us to break the cycle in computation when traversing this graph. - private bool Equals(TableReferenceExpression tableReferenceExpression) - => string.Equals(Alias, tableReferenceExpression.Alias, StringComparison.OrdinalIgnoreCase) - && ReferenceEquals(_selectExpression, tableReferenceExpression._selectExpression); - - /// - public override int GetHashCode() - => Alias.GetHashCode(); - } -} diff --git a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs index 6389b8da12f..a76f6b9efb5 100644 --- a/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs +++ b/test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs @@ -3929,6 +3929,12 @@ public override void Select_DTO_constructor_distinct_with_navigation_translated_ base.Select_DTO_constructor_distinct_with_navigation_translated_to_server(); } + [ConditionalFact(Skip = "Issue #17246")] + public override void Select_DTO_constructor_distinct_with_collection_projection_translated_to_server() + { + base.Select_DTO_constructor_distinct_with_collection_projection_translated_to_server(); + } + [ConditionalTheory(Skip = "Issue #17246")] public override Task Select_Property_when_shadow_unconstrained_generic_method(bool async) { diff --git a/test/EFCore.Relational.Specification.Tests/Query/NorthwindGroupByQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NorthwindGroupByQueryRelationalTestBase.cs index bb07f8d91fa..87ef6d941a4 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NorthwindGroupByQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NorthwindGroupByQueryRelationalTestBase.cs @@ -27,6 +27,14 @@ public override async Task Complex_query_with_groupBy_in_subquery4(bool async) Assert.Equal(RelationalStrings.InsufficientInformationToIdentifyOuterElementOfCollectionJoin, message); } + public override async Task Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes_to_complex(bool async) + { + var message = (await Assert.ThrowsAsync( + () => base.Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes_to_complex(async))).Message; + + Assert.Equal(RelationalStrings.InsufficientInformationToIdentifyOuterElementOfCollectionJoin, message); + } + protected virtual bool CanExecuteQueryString => false; diff --git a/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs index b069da453d3..c8c4293741c 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindGroupByQueryTestBase.cs @@ -3104,6 +3104,78 @@ public virtual Task Select_uncorrelated_collection_with_groupby_when_outer_is_di }); } + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Select_correlated_collection_after_GroupBy_aggregate_when_identifier_does_not_change(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .GroupBy(e => e.CustomerID) + .Where(g => g.Key.StartsWith("F")) + .Select(e => e.Key) + .Select(c => new + { + c, + Orders = ss.Set().Where(o => o.CustomerID == c).ToList() + }), + elementSorter: e => e.c, + elementAsserter: (e, a) => + { + AssertEqual(e.c, a.c); + AssertCollection(e.Orders, a.Orders); + }, + entryCount: 63); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .GroupBy(e => e.CustomerID) + .Where(g => g.Key.StartsWith("F")) + .Select(e => e.Key) + .Select(c => new + { + c, + Orders = ss.Set().Where(o => o.CustomerID == c).ToList() + }), + elementSorter: e => e.c, + elementAsserter: (e, a) => + { + AssertEqual(e.c, a.c); + AssertCollection(e.Orders, a.Orders); + }, + entryCount: 63); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes_to_complex(bool async) + { + return AssertQuery( + async, + ss => ss.Set() + .GroupBy(e => e.CustomerID + "A") + .Where(g => g.Key.StartsWith("F")) + .Select(e => e.Key) + .Select(c => new + { + c, + Orders = ss.Set().Where(o => o.CustomerID + "A" == c).ToList() + }), + elementSorter: e => e.c, + elementAsserter: (e, a) => + { + AssertEqual(e.c, a.c); + AssertCollection(e.Orders, a.Orders); + }, + entryCount: 63); + } + #endregion } } diff --git a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs index 25ab14570fa..337e7bbe72f 100644 --- a/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs @@ -1440,6 +1440,40 @@ public virtual void Select_DTO_constructor_distinct_with_navigation_translated_t } } + [ConditionalFact(Skip = "Issue#24478")] + public virtual void Select_DTO_constructor_distinct_with_collection_projection_translated_to_server() + { + using var context = CreateContext(); + var actual = context.Set() + .Where(o => o.OrderID < 10300) + .Select(o => new { A = new OrderCountDTO(o.CustomerID), o.CustomerID }) + .Distinct() + .Select(e => new + { + e.A, + Orders = context.Set().Where(o => o.CustomerID == e.CustomerID).ToList() + }) + .ToList().OrderBy(e => e.A.Id).ToList(); + + var expected = Fixture.GetExpectedData().Set() + .Where(o => o.OrderID < 10300) + .Select(o => new { A = new OrderCountDTO(o.CustomerID), o.CustomerID }) + .Distinct() + .Select(e => new + { + e.A, + Orders = Fixture.GetExpectedData().Set().Where(o => o.CustomerID == e.CustomerID).ToList() + }) + .ToList().OrderBy(e => e.A.Id).ToList(); + + Assert.Equal(expected.Count, actual.Count); + for (var i = 0; i < expected.Count; i++) + { + Assert.Equal(expected[i].A.Id, actual[i].A.Id); + Assert.True(expected[i].Orders?.SequenceEqual(actual[i].Orders) ?? true); + } + } + [ConditionalFact] public virtual void Select_DTO_with_member_init_distinct_translated_to_server() { diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs index 3e7637120ae..20d66927a8d 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs @@ -2542,6 +2542,45 @@ GROUP BY [p0].[ProductID] ORDER BY [t].[City], [t0].[ProductID], [t1].[ProductID]"); } + public override async Task Select_correlated_collection_after_GroupBy_aggregate_when_identifier_does_not_change(bool async) + { + await base.Select_correlated_collection_after_GroupBy_aggregate_when_identifier_does_not_change(async); + + AssertSql( + @"SELECT [t].[CustomerID], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate] +FROM ( + SELECT [c].[CustomerID] + FROM [Customers] AS [c] + GROUP BY [c].[CustomerID] + HAVING [c].[CustomerID] LIKE N'F%' +) AS [t] +LEFT JOIN [Orders] AS [o] ON [t].[CustomerID] = [o].[CustomerID] +ORDER BY [t].[CustomerID], [o].[OrderID]"); + } + + public override async Task Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes(bool async) + { + await base.Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes(async); + + AssertSql( + @"SELECT [t].[CustomerID], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM ( + SELECT [o].[CustomerID] + FROM [Orders] AS [o] + GROUP BY [o].[CustomerID] + HAVING [o].[CustomerID] IS NOT NULL AND ([o].[CustomerID] LIKE N'F%') +) AS [t] +LEFT JOIN [Orders] AS [o0] ON [t].[CustomerID] = [o0].[CustomerID] +ORDER BY [t].[CustomerID], [o0].[OrderID]"); + } + + public override async Task Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes_to_complex(bool async) + { + await base.Select_correlated_collection_after_GroupBy_aggregate_when_identifier_changes_to_complex(async); + + //AssertSql(" "); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs index e1ac00e7f48..3b2c0b50956 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs @@ -2294,6 +2294,21 @@ FROM [Orders] AS [o] WHERE [o].[OrderID] < 10300"); } + public override void Select_DTO_constructor_distinct_with_collection_projection_translated_to_server() + { + base.Select_DTO_constructor_distinct_with_collection_projection_translated_to_server(); + + AssertSql( + @"SELECT [t].[CustomerID], [o0].[OrderID], [o0].[CustomerID], [o0].[EmployeeID], [o0].[OrderDate] +FROM ( + SELECT DISTINCT [o].[CustomerID] + FROM [Orders] AS [o] + WHERE [o].[OrderID] < 10300 +) AS [t] +LEFT JOIN [Orders] AS [o0] ON [t].[CustomerID] = [o0].[CustomerID] +ORDER BY [t].[CustomerID], [o0].[OrderID]"); + } + public override void Select_DTO_with_member_init_distinct_translated_to_server() { base.Select_DTO_with_member_init_distinct_translated_to_server(); diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs index ff6b7fa9843..3939bafb6ff 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/QueryBugsTest.cs @@ -2873,7 +2873,6 @@ WHERE [t4].[MaumarEntity11818_Name] IS NOT NULL using (var context = contextFactory.CreateContext()) { - ClearLog(); var query = (from e in context.Set() join a in context.Set()