diff --git a/src/EFCore.Relational/Query/ExpressionVisitors/RelationalProjectionExpressionVisitor.cs b/src/EFCore.Relational/Query/ExpressionVisitors/RelationalProjectionExpressionVisitor.cs
index d8f866586e3..9e58edaa0ec 100644
--- a/src/EFCore.Relational/Query/ExpressionVisitors/RelationalProjectionExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/ExpressionVisitors/RelationalProjectionExpressionVisitor.cs
@@ -139,7 +139,8 @@ protected override Expression VisitNew(NewExpression newExpression)
{
Check.NotNull(newExpression, nameof(newExpression));
- if (newExpression.Type == typeof(AnonymousObject))
+ if (newExpression.Type == typeof(AnonymousObject)
+ || newExpression.Type == typeof(MaterializedAnonymousObject))
{
var propertyCallExpressions
= ((NewArrayExpression)newExpression.Arguments.Single()).Expressions;
diff --git a/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs b/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs
index bd2e2982db7..9d8e842f14c 100644
--- a/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs
+++ b/src/EFCore.Relational/Query/ExpressionVisitors/SqlTranslatingExpressionVisitor.cs
@@ -649,7 +649,8 @@ var boundExpression
}
}
- if (AnonymousObject.IsGetValueExpression(methodCallExpression, out var querySourceReferenceExpression))
+ if (AnonymousObject.IsGetValueExpression(methodCallExpression, out var querySourceReferenceExpression)
+ || MaterializedAnonymousObject.IsGetValueExpression(methodCallExpression, out querySourceReferenceExpression))
{
var selectExpression
= _queryModelVisitor.TryGetQuery(querySourceReferenceExpression.ReferencedQuerySource);
@@ -863,7 +864,8 @@ var memberBindings
return Expression.Constant(memberBindings);
}
}
- else if (expression.Type == typeof(AnonymousObject))
+ else if (expression.Type == typeof(AnonymousObject)
+ || expression.Type == typeof(MaterializedAnonymousObject))
{
var propertyCallExpressions
= ((NewArrayExpression)expression.Arguments.Single()).Expressions;
diff --git a/src/EFCore.Relational/Query/Expressions/SelectExpression.cs b/src/EFCore.Relational/Query/Expressions/SelectExpression.cs
index 39832dceff1..8738cb057aa 100644
--- a/src/EFCore.Relational/Query/Expressions/SelectExpression.cs
+++ b/src/EFCore.Relational/Query/Expressions/SelectExpression.cs
@@ -9,6 +9,7 @@
using JetBrains.Annotations;
using Microsoft.EntityFrameworkCore.Internal;
using Microsoft.EntityFrameworkCore.Metadata;
+using Microsoft.EntityFrameworkCore.Query.Expressions.Internal;
using Microsoft.EntityFrameworkCore.Query.Internal;
using Microsoft.EntityFrameworkCore.Query.Sql;
using Microsoft.EntityFrameworkCore.Storage;
@@ -910,13 +911,10 @@ var existingOrdering
private bool OrderingExpressionComparison(Ordering ordering, Expression expressionToMatch)
{
- return _expressionEqualityComparer.Equals(ordering.Expression, expressionToMatch)
- || _expressionEqualityComparer.Equals(
- UnwrapNullableExpression(ordering.Expression.RemoveConvert()).RemoveConvert(),
- expressionToMatch)
- || _expressionEqualityComparer.Equals(
- UnwrapNullableExpression(expressionToMatch.RemoveConvert()).RemoveConvert(),
- ordering.Expression);
+ var unwrappedOrderingExpression = UnwrapNullableExpression(ordering.Expression.RemoveConvert()).RemoveConvert();
+ var unwrappedExpressionToMatch = UnwrapNullableExpression(expressionToMatch.RemoveConvert()).RemoveConvert();
+
+ return _expressionEqualityComparer.Equals(unwrappedOrderingExpression, unwrappedExpressionToMatch);
}
private Expression UnwrapNullableExpression(Expression expression)
@@ -926,6 +924,11 @@ private Expression UnwrapNullableExpression(Expression expression)
return nullableExpression.Operand;
}
+ if (expression is NullConditionalExpression nullConditionalExpression)
+ {
+ return nullConditionalExpression.AccessOperation;
+ }
+
return expression;
}
diff --git a/src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs b/src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs
index 3fb845a1dc4..ebc819fe6c8 100644
--- a/src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs
+++ b/src/EFCore.Relational/Query/RelationalQueryModelVisitor.cs
@@ -1058,6 +1058,54 @@ var sqlOrderingExpression
}
}
+ ///
+ /// Determines whether correlated collections (if any) can be optimized.
+ ///
+ /// True if optimization is allowed, false otherwise.
+ protected override bool CanOptimizeCorrelatedCollections()
+ {
+ if (!base.CanOptimizeCorrelatedCollections())
+ {
+ return false;
+ }
+
+ if (RequiresClientEval
+ || RequiresClientFilter
+ || RequiresClientJoin
+ || RequiresClientOrderBy
+ || RequiresClientSelectMany)
+ {
+ return false;
+ }
+
+ var injectParametersFinder = new InjectParametersFindingVisitor(QueryCompilationContext.QueryMethodProvider.InjectParametersMethod);
+ injectParametersFinder.Visit(Expression);
+
+ return !injectParametersFinder.InjectParametersFound;
+ }
+
+ private class InjectParametersFindingVisitor : ExpressionVisitorBase
+ {
+ private MethodInfo _injectParametersMethod;
+
+ public InjectParametersFindingVisitor(MethodInfo injectParametersMethod)
+ {
+ _injectParametersMethod = injectParametersMethod;
+ }
+
+ public bool InjectParametersFound { get; private set; }
+
+ protected override Expression VisitMethodCall(MethodCallExpression node)
+ {
+ if (node.Method.MethodIsClosedFormOf(_injectParametersMethod))
+ {
+ InjectParametersFound = true;
+ }
+
+ return base.VisitMethodCall(node);
+ }
+ }
+
///
/// Visits nodes.
///
diff --git a/src/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs b/src/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs
index 7093b902adf..f6488171b21 100644
--- a/src/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs
+++ b/src/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs
@@ -3516,7 +3516,7 @@ public virtual void Project_collection_navigation_composed()
AssertQuery(
l1s => from l1 in l1s
where l1.Id < 3
- select new { l1.Id, collection = l1.OneToMany_Optional.Where(l2 => l2.Name != "Foo") },
+ select new { l1.Id, collection = l1.OneToMany_Optional.Where(l2 => l2.Name != "Foo").ToList() },
elementSorter: e => e.Id,
elementAsserter: (e, a) =>
{
diff --git a/src/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs b/src/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs
index 5627f3d919d..a91139f2595 100644
--- a/src/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs
+++ b/src/EFCore.Specification.Tests/Query/GearsOfWarQueryTestBase.cs
@@ -1760,7 +1760,7 @@ public virtual void Select_correlated_filtered_collection()
gs => gs
.Where(g => g.CityOfBirth.Name == "Ephyra" || g.CityOfBirth.Name == "Hanover")
.OrderBy(g => g.Nickname)
- .Select(g => g.Weapons.Where(w => w.Name != "Lancer")),
+ .Select(g => g.Weapons.Where(w => w.Name != "Lancer").ToList()),
assertOrder: true,
elementAsserter: CollectionAsserter(e => e.Id, (e, a) => Assert.Equal(e.Id, a.Id)));
}
@@ -1769,7 +1769,7 @@ public virtual void Select_correlated_filtered_collection()
public virtual void Select_correlated_filtered_collection_with_composite_key()
{
AssertQuery(
- gs => gs.OfType().OrderBy(g => g.Nickname).Select(g => g.Reports.Where(r => r.Nickname != "Dom")),
+ gs => gs.OfType().OrderBy(g => g.Nickname).Select(g => g.Reports.Where(r => r.Nickname != "Dom").ToList()),
assertOrder: true,
elementAsserter: CollectionAsserter(e => e.Nickname, (e, a) => Assert.Equal(e.Nickname, a.Nickname)));
}
@@ -3235,11 +3235,762 @@ public virtual void Enum_ToString_is_client_eval()
.Select(g => g.Rank.ToString()));
}
- protected GearsOfWarContext CreateContext()
+ [ConditionalFact]
+ public virtual void Correlated_collections_basic_projection()
+ {
+ AssertQuery(
+ gs => from g in gs
+ where g.Nickname != "Marcus"
+ orderby g.Nickname
+ select (from w in g.Weapons
+ where w.IsAutomatic || w.Name != "foo"
+ select w).ToList(),
+ assertOrder: true,
+ elementAsserter: CollectionAsserter(e => e.Id, (e, a) => Assert.Equal(e.Id, a.Id)));
+ }
+
+ [ConditionalFact]
+ public virtual void Correlated_collections_basic_projection_explicit_to_list()
+ {
+ AssertQuery(
+ gs => from g in gs
+ where g.Nickname != "Marcus"
+ orderby g.Nickname
+ select (from w in g.Weapons
+ where w.IsAutomatic || w.Name != "foo"
+ select w).ToList(),
+ assertOrder: true,
+ elementAsserter: CollectionAsserter(e => e.Id, (e, a) => Assert.Equal(e.Id, a.Id)));
+ }
+
+ [ConditionalFact]
+ public virtual void Correlated_collections_basic_projection_explicit_to_array()
+ {
+ AssertQuery(
+ gs => from g in gs
+ where g.Nickname != "Marcus"
+ orderby g.Nickname
+ select (from w in g.Weapons
+ where w.IsAutomatic || w.Name != "foo"
+ select w).ToArray(),
+ assertOrder: true,
+ elementAsserter: CollectionAsserter(e => e.Id, (e, a) => Assert.Equal(e.Id, a.Id)));
+ }
+
+ [ConditionalFact]
+ public virtual void Correlated_collections_basic_projection_ordered()
+ {
+ AssertQuery(
+ gs => from g in gs
+ where g.Nickname != "Marcus"
+ orderby g.Nickname
+ select (from w in g.Weapons
+ where w.IsAutomatic || w.Name != "foo"
+ orderby w.Name descending
+ select w).ToList(),
+ assertOrder: true,
+ elementAsserter: CollectionAsserter(elementAsserter: (e, a) => Assert.Equal(e.Id, a.Id)));
+ }
+
+ [ConditionalFact]
+ public virtual void Correlated_collections_basic_projection_composite_key()
+ {
+ AssertQuery(gs =>
+ from o in gs.OfType()
+ where o.Nickname != "Foo"
+ select new
+ {
+ o.Nickname,
+ Collection = (from r in o.Reports
+ where !r.HasSoulPatch
+ select new { r.Nickname, r.FullName }).ToArray()
+ },
+ elementSorter: e => e.Nickname,
+ elementAsserter: (e, a) =>
+ {
+ Assert.Equal(e.Nickname, a.Nickname);
+ CollectionAsserter(elementSorter: ee => ee.FullName)(e.Collection, a.Collection);
+ });
+ }
+
+ [ConditionalFact]
+ public virtual void Correlated_collections_basic_projecting_single_property()
+ {
+ AssertQuery(
+ gs => from g in gs
+ where g.Nickname != "Marcus"
+ orderby g.Nickname
+ select (from w in g.Weapons
+ where w.IsAutomatic || w.Name != "foo"
+ select w.Name).ToList(),
+ assertOrder: true,
+ elementAsserter: CollectionAsserter());
+ }
+
+ [ConditionalFact]
+ public virtual void Correlated_collections_basic_projecting_constant()
+ {
+ AssertQuery(
+ gs => from g in gs
+ where g.Nickname != "Marcus"
+ orderby g.Nickname
+ select (from w in g.Weapons
+ where w.IsAutomatic || w.Name != "foo"
+ select "BFG").ToList(),
+ assertOrder: true,
+ elementAsserter: CollectionAsserter());
+ }
+
+ [ConditionalFact]
+ public virtual void Correlated_collections_projection_of_collection_thru_navigation()
+ {
+ AssertQuery(
+ gs => from g in gs
+ orderby g.FullName
+ where g.Nickname != "Marcus"
+ select g.Squad.Missions.Where(m => m.MissionId != 17).ToList(),
+ assertOrder: true,
+ elementAsserter: CollectionAsserter(
+ e => e.MissionId + " " + e.SquadId,
+ (e, a) =>
+ {
+ Assert.Equal(e.MissionId, a.MissionId);
+ Assert.Equal(e.SquadId, a.SquadId);
+ }));
+ }
+
+ [ConditionalFact]
+ public virtual void Correlated_collections_project_anonymous_collection_result()
+ {
+ AssertQuery(
+ ss => from s in ss
+ where s.Id < 20
+ select new
+ {
+ s.Name,
+ Collection = (from m in s.Members
+ select new { m.FullName, m.Rank }).ToList()
+ },
+ elementSorter: e => e.Name,
+ elementAsserter: (e, a) =>
+ {
+ Assert.Equal(e.Name, a.Name);
+ CollectionAsserter(ee => ee.FullName + " " + ee.Rank)(e.Collection, a.Collection);
+ });
+ }
+
+ [ConditionalFact]
+ public virtual void Correlated_collections_nested()
+ {
+ AssertQuery(
+ ss => from s in ss
+ select (from m in s.Missions
+ where m.MissionId < 42
+ select (from ps in m.Mission.ParticipatingSquads
+ where ps.SquadId < 7
+ select ps).ToList()).ToList(),
+ elementSorter: CollectionSorter