Skip to content

Commit bb11d83

Browse files
committed
Fix to #1833 - Support filtered Include
Allows for additional operations to be specified inside Include/ThenInclude expression when the navigation is a collection: - Where, - OrderBy(Descending)/ThenBy(Descending), - Skip, - Take. Those additional operations are treated like any other within the query, so translation restrictions apply. Collections included using new filter operations are considered to be loaded. Only one filter is allowed per navigation. In cases where same navigation is included multiple times (e.g. Include(A).ThenInclude(A_B).Include(A).ThenInclude(A_C)) filter should only be applied once. Alternatively the same exact filter should be applied to all.
1 parent 9b1f0a0 commit bb11d83

13 files changed

+1104
-11
lines changed

src/EFCore/Properties/CoreStrings.Designer.cs

+12-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/EFCore/Properties/CoreStrings.resx

+5-2
Original file line numberDiff line numberDiff line change
@@ -747,8 +747,8 @@
747747
<data name="AmbiguousForeignKeyPropertyCandidates" xml:space="preserve">
748748
<value>Both relationships between '{firstDependentToPrincipalNavigationSpecification}' and '{firstPrincipalToDependentNavigationSpecification}' and between '{secondDependentToPrincipalNavigationSpecification}' and '{secondPrincipalToDependentNavigationSpecification}' could use {foreignKeyProperties} as the foreign key. To resolve this configure the foreign key properties explicitly on at least one of the relationships.</value>
749749
</data>
750-
<data name="InvalidIncludeLambdaExpression" xml:space="preserve">
751-
<value>The {methodName} property lambda expression '{includeLambdaExpression}' is invalid. The expression should represent a property access: 't =&gt; t.MyProperty'. To target navigations declared on derived types, specify an explicitly typed lambda parameter of the target type, E.g. '(Derived d) =&gt; d.MyProperty'. For more information on including related data, see http://go.microsoft.com/fwlink/?LinkID=746393.</value>
750+
<data name="InvalidIncludeExpression" xml:space="preserve">
751+
<value>The expression '{expression}' is invalid inside Include operation. The expression should represent a property access: 't =&gt; t.MyProperty'. To target navigations declared on derived types use cast, e.g. 't =&gt; ((Derived)t).MyProperty' or 'as' operator, e.g. 't =&gt; (t as Derived).MyProperty'. Collection navigation access can be filtered by composing Where, OrderBy(Descending), ThenBy(Descending), Skip or Take operations. For more information on including related data, see http://go.microsoft.com/fwlink/?LinkID=746393.</value>
752752
</data>
753753
<data name="AbstractLeafEntityType" xml:space="preserve">
754754
<value>The corresponding CLR type for entity type '{entityType}' is not instantiable and there is no derived entity type in the model that corresponds to a concrete CLR type.</value>
@@ -1290,6 +1290,9 @@
12901290
<data name="IncludeOnNonEntity" xml:space="preserve">
12911291
<value>Include has been used on non entity queryable.</value>
12921292
</data>
1293+
<data name="MultipleFilteredIncludesOnSameNavigation" xml:space="preserve">
1294+
<value>Different filters: '{filter1}' and '{filter2}' have been applied on the same included navigation. Only one unique filter per navigation is allowed. For more information on including related data, see http://go.microsoft.com/fwlink/?LinkID=746393.</value>
1295+
</data>
12931296
<data name="CannotConvertQueryableToEnumerableMethod" xml:space="preserve">
12941297
<value>Unable to convert queryable method to enumerable method.</value>
12951298
</data>

src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs

+15-2
Original file line numberDiff line numberDiff line change
@@ -181,9 +181,8 @@ protected Expression ExpandNavigation(
181181
var innerSource = (NavigationExpansionExpression)_navigationExpandingExpressionVisitor.Visit(innerQueryable);
182182
if (entityReference.IncludePaths.ContainsKey(navigation))
183183
{
184-
var innerIncludeTreeNode = entityReference.IncludePaths[navigation];
185184
var innerEntityReference = (EntityReference)((NavigationTreeExpression)innerSource.PendingSelector).Value;
186-
innerEntityReference.SetIncludePaths(innerIncludeTreeNode);
185+
innerEntityReference.SetIncludePaths(entityReference.IncludePaths[navigation]);
187186
}
188187

189188
var innerSourceSequenceType = innerSource.Type.GetSequenceType();
@@ -532,6 +531,20 @@ private Expression ExpandIncludesHelper(Expression root, EntityReference entityR
532531
included = ExpandIncludesHelper(included, innerEntityReference);
533532
}
534533

534+
if (included is MaterializeCollectionNavigationExpression materializeCollectionNavigation)
535+
{
536+
var filterExpression = entityReference.IncludePaths[navigation].FilterExpression;
537+
if (filterExpression != null)
538+
{
539+
var subquery = ReplacingExpressionVisitor.Replace(
540+
filterExpression.Parameters[0],
541+
materializeCollectionNavigation.Subquery,
542+
filterExpression.Body);
543+
544+
included = materializeCollectionNavigation.Update(subquery);
545+
}
546+
}
547+
535548
result = new IncludeExpression(result, included, navigation);
536549
}
537550

src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs

+2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ protected class IncludeTreeNode : Dictionary<INavigation, IncludeTreeNode>
8484
{
8585
private EntityReference _entityReference;
8686

87+
public virtual LambdaExpression FilterExpression { get; set; }
88+
8789
public IncludeTreeNode(IEntityType entityType, EntityReference entityReference)
8890
{
8991
EntityType = entityType;

src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs

+90-1
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ private static readonly PropertyInfo _queryContextContextPropertyInfo
3232
{ QueryableMethods.LastWithPredicate, QueryableMethods.LastWithoutPredicate },
3333
{ QueryableMethods.LastOrDefaultWithPredicate, QueryableMethods.LastOrDefaultWithoutPredicate }
3434
};
35+
36+
private static readonly List<MethodInfo> _supportedFilteredIncludeOperations = new List<MethodInfo>
37+
{
38+
QueryableMethods.Where,
39+
QueryableMethods.OrderBy,
40+
QueryableMethods.OrderByDescending,
41+
QueryableMethods.ThenBy,
42+
QueryableMethods.ThenByDescending,
43+
QueryableMethods.Skip,
44+
QueryableMethods.Take,
45+
QueryableMethods.AsQueryable
46+
};
47+
3548
private readonly QueryTranslationPreprocessor _queryTranslationPreprocessor;
3649
private readonly QueryCompilationContext _queryCompilationContext;
3750
private readonly PendingSelectorExpandingExpressionVisitor _pendingSelectorExpandingExpressionVisitor;
@@ -768,6 +781,7 @@ private NavigationExpansionExpression ProcessInclude(NavigationExpansionExpressi
768781
foreach (var navigation in FindNavigations(currentNode.EntityType, navigationName))
769782
{
770783
var addedNode = currentNode.AddNavigation(navigation);
784+
771785
// This is to add eager Loaded navigations when owner type is included.
772786
PopulateEagerLoadedNavigations(addedNode);
773787
includeTreeNodes.Enqueue(addedNode);
@@ -786,19 +800,92 @@ private NavigationExpansionExpression ProcessInclude(NavigationExpansionExpressi
786800
? entityReference.LastIncludeTreeNode
787801
: entityReference.IncludePaths;
788802
var includeLambda = expression.UnwrapLambdaFromQuote();
789-
var lastIncludeTree = PopulateIncludeTree(currentIncludeTreeNode, includeLambda.Body);
803+
804+
var (result, filterExpression) = ExtractIncludeFilter(includeLambda.Body, includeLambda.Body);
805+
var lastIncludeTree = PopulateIncludeTree(currentIncludeTreeNode, result);
790806
if (lastIncludeTree == null)
791807
{
792808
throw new InvalidOperationException(CoreStrings.InvalidLambdaExpressionInsideInclude);
793809
}
794810

811+
if (filterExpression != null)
812+
{
813+
if (lastIncludeTree.FilterExpression != null
814+
&& !ExpressionEqualityComparer.Instance.Equals(filterExpression, lastIncludeTree.FilterExpression))
815+
{
816+
throw new InvalidOperationException(
817+
CoreStrings.MultipleFilteredIncludesOnSameNavigation(
818+
FormatFilter(filterExpression.Body).Print(),
819+
FormatFilter(lastIncludeTree.FilterExpression.Body).Print()));
820+
}
821+
822+
lastIncludeTree.FilterExpression = filterExpression;
823+
}
824+
795825
entityReference.SetLastInclude(lastIncludeTree);
796826
}
797827

798828
return source;
799829
}
800830

801831
throw new InvalidOperationException(CoreStrings.IncludeOnNonEntity);
832+
833+
static (Expression result, LambdaExpression filterExpression) ExtractIncludeFilter(Expression currentExpression, Expression includeExpression)
834+
{
835+
if (currentExpression is MemberExpression)
836+
{
837+
return (currentExpression, default(LambdaExpression));
838+
}
839+
840+
if (currentExpression is MethodCallExpression methodCallExpression)
841+
{
842+
if (!methodCallExpression.Method.IsGenericMethod
843+
|| !_supportedFilteredIncludeOperations.Contains(methodCallExpression.Method.GetGenericMethodDefinition()))
844+
{
845+
throw new InvalidOperationException(CoreStrings.InvalidIncludeExpression(includeExpression));
846+
}
847+
848+
var (result, filterExpression) = ExtractIncludeFilter(methodCallExpression.Arguments[0], includeExpression);
849+
if (filterExpression == null)
850+
{
851+
var prm = Expression.Parameter(result.Type);
852+
filterExpression = Expression.Lambda(prm, prm);
853+
}
854+
855+
var arguments = new List<Expression>();
856+
arguments.Add(filterExpression.Body);
857+
arguments.AddRange(methodCallExpression.Arguments.Skip(1));
858+
filterExpression = Expression.Lambda(
859+
methodCallExpression.Update(methodCallExpression.Object, arguments),
860+
filterExpression.Parameters);
861+
862+
return (result, filterExpression);
863+
}
864+
865+
throw new InvalidOperationException(CoreStrings.InvalidIncludeExpression(includeExpression));
866+
}
867+
868+
static Expression FormatFilter(Expression expression)
869+
{
870+
if (expression is MethodCallExpression methodCallExpression
871+
&& methodCallExpression.Method.IsGenericMethod
872+
&& _supportedFilteredIncludeOperations.Contains(methodCallExpression.Method.GetGenericMethodDefinition()))
873+
{
874+
if (methodCallExpression.Method.GetGenericMethodDefinition() == QueryableMethods.AsQueryable)
875+
{
876+
return Expression.Parameter(expression.Type, "navigation");
877+
}
878+
879+
var arguments = new List<Expression>();
880+
var source = FormatFilter(methodCallExpression.Arguments[0]);
881+
arguments.Add(source);
882+
arguments.AddRange(methodCallExpression.Arguments.Skip(1));
883+
884+
return methodCallExpression.Update(methodCallExpression.Object, arguments);
885+
}
886+
887+
return expression;
888+
}
802889
}
803890

804891
private NavigationExpansionExpression ProcessJoin(
@@ -1474,8 +1561,10 @@ private IncludeTreeNode PopulateIncludeTree(IncludeTreeNode includeTreeNode, Exp
14741561
if (navigation != null)
14751562
{
14761563
var addedNode = innerIncludeTreeNode.AddNavigation(navigation);
1564+
14771565
// This is to add eager Loaded navigations when owner type is included.
14781566
PopulateEagerLoadedNavigations(addedNode);
1567+
14791568
return addedNode;
14801569
}
14811570

0 commit comments

Comments
 (0)