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()