From 44bd2751ccb351044c92e1520d582f33706be669 Mon Sep 17 00:00:00 2001 From: maumar Date: Wed, 4 Mar 2020 21:42:52 -0800 Subject: [PATCH] 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. Applying multiple filtered includes on the same navigation is not supported. --- src/EFCore/Properties/CoreStrings.Designer.cs | 16 + src/EFCore/Properties/CoreStrings.resx | 6 + ...ingExpressionVisitor.ExpressionVisitors.cs | 23 +- ...nExpandingExpressionVisitor.Expressions.cs | 10 +- .../NavigationExpandingExpressionVisitor.cs | 53 +++- .../Query/ComplexNavigationsQueryTestBase.cs | 279 +++++++++++++++++ .../ComplexNavigationsWeakQueryTestBase.cs | 5 + .../TestUtilities/ExpectedFilteredInclude.cs | 23 ++ .../IncludeQueryResultAsserter.cs | 22 ++ .../ComplexNavigationsQuerySqlServerTest.cs | 281 ++++++++++++++++++ .../ComplexNavigationsQuerySqliteTest.cs | 16 + .../ComplexNavigationsWeakQuerySqliteTest.cs | 14 + 12 files changed, 736 insertions(+), 12 deletions(-) create mode 100644 test/EFCore.Specification.Tests/TestUtilities/ExpectedFilteredInclude.cs diff --git a/src/EFCore/Properties/CoreStrings.Designer.cs b/src/EFCore/Properties/CoreStrings.Designer.cs index 6402d3956f7..dada221cd84 100644 --- a/src/EFCore/Properties/CoreStrings.Designer.cs +++ b/src/EFCore/Properties/CoreStrings.Designer.cs @@ -2410,6 +2410,14 @@ public static string InvalidIncludePath([CanBeNull] object navigationChain, [Can GetString("InvalidIncludePath", nameof(navigationChain), nameof(navigationName)), navigationChain, navigationName); + /// + /// Operation '{operationName}' is not supported inside Include method. + /// + public static string FilteredIncludeOperationNotSupported([CanBeNull] object operationName) + => string.Format( + GetString("FilteredIncludeOperationNotSupported", nameof(operationName)), + operationName); + /// /// Lambda expression used inside Include is not valid. /// @@ -2422,6 +2430,14 @@ public static string InvalidLambdaExpressionInsideInclude public static string IncludeOnNonEntity => GetString("IncludeOnNonEntity"); + /// + /// Multiple filtered include operations on the same navigation '{navigationName}' are not supported. + /// + public static string MultipleFilteredIncludesOnSameNavigation([CanBeNull] object navigationName) + => string.Format( + GetString("MultipleFilteredIncludesOnSameNavigation", nameof(navigationName)), + navigationName); + /// /// Unable to convert queryable method to enumerable method. /// diff --git a/src/EFCore/Properties/CoreStrings.resx b/src/EFCore/Properties/CoreStrings.resx index 9701fad6b53..b62be2d5b37 100644 --- a/src/EFCore/Properties/CoreStrings.resx +++ b/src/EFCore/Properties/CoreStrings.resx @@ -1290,6 +1290,12 @@ Include has been used on non entity queryable. + + Multiple filtered include operations on the same navigation '{navigationName}' are not supported. + + + Operation '{operationName}' is not supported inside Include method. + Unable to convert queryable method to enumerable method. diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs index c129c668529..55ba7373213 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.ExpressionVisitors.cs @@ -151,6 +151,7 @@ protected Expression ExpandNavigation( return expansion; } + var filterExpression = default(LambdaExpression); var targetType = navigation.TargetEntityType; if (targetType.HasDefiningNavigation() || targetType.IsOwned()) @@ -159,16 +160,21 @@ protected Expression ExpandNavigation( ownedEntityReference.MarkAsOptional(); if (entityReference.IncludePaths.ContainsKey(navigation)) { - ownedEntityReference.SetIncludePaths(entityReference.IncludePaths[navigation]); + var innerIncludeTreeNode = entityReference.IncludePaths[navigation]; + filterExpression = innerIncludeTreeNode.FilterExpression; + ownedEntityReference.SetIncludePaths(innerIncludeTreeNode); } var ownedExpansion = new OwnedNavigationReference(root, navigation, ownedEntityReference); if (navigation.IsCollection) { var elementType = ownedExpansion.Type.TryGetSequenceType(); - var subquery = Expression.Call( - QueryableMethods.AsQueryable.MakeGenericMethod(elementType), - ownedExpansion); + + var subquery = filterExpression != null + ? ReplacingExpressionVisitor.Replace(filterExpression.Parameters[0], ownedExpansion, filterExpression.Body) + : Expression.Call( + QueryableMethods.AsQueryable.MakeGenericMethod(elementType), + ownedExpansion); return new MaterializeCollectionNavigationExpression(subquery, navigation); } @@ -179,9 +185,11 @@ protected Expression ExpandNavigation( var innerQueryable = new QueryRootExpression(targetType); var innerSource = (NavigationExpansionExpression)_navigationExpandingExpressionVisitor.Visit(innerQueryable); + if (entityReference.IncludePaths.ContainsKey(navigation)) { var innerIncludeTreeNode = entityReference.IncludePaths[navigation]; + filterExpression = innerIncludeTreeNode.FilterExpression; var innerEntityReference = (EntityReference)((NavigationTreeExpression)innerSource.PendingSelector).Value; innerEntityReference.SetIncludePaths(innerIncludeTreeNode); } @@ -248,13 +256,18 @@ protected Expression ExpandNavigation( Expression.Equal(outerKey, innerKey)) : Expression.Equal(outerKey, innerKey); - var subquery = Expression.Call( + var subquery = (Expression)Expression.Call( QueryableMethods.Where.MakeGenericMethod(innerSourceSequenceType), innerSource, Expression.Quote( Expression.Lambda( predicateBody, innerParameter))); + if (filterExpression != null) + { + subquery = ReplacingExpressionVisitor.Replace(filterExpression.Parameters[0], subquery, filterExpression.Body); + } + return new MaterializeCollectionNavigationExpression(subquery, navigation); } diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs index 8cee5661892..7014e784670 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.Expressions.cs @@ -84,6 +84,8 @@ protected class IncludeTreeNode : Dictionary { private EntityReference _entityReference; + public virtual LambdaExpression FilterExpression { get; set; } + public IncludeTreeNode(IEntityType entityType, EntityReference entityReference) { EntityType = entityType; @@ -92,10 +94,16 @@ public IncludeTreeNode(IEntityType entityType, EntityReference entityReference) public virtual IEntityType EntityType { get; private set; } - public virtual IncludeTreeNode AddNavigation(INavigation navigation) + public virtual IncludeTreeNode AddNavigation(INavigation navigation, bool withFilter) { if (TryGetValue(navigation, out var existingValue)) { + if (existingValue.FilterExpression != null + && withFilter) + { + throw new NotSupportedException(CoreStrings.MultipleFilteredIncludesOnSameNavigation(navigation.Name)); + } + return existingValue; } diff --git a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs index 6d5e6871b16..b7381fc325c 100644 --- a/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs +++ b/src/EFCore/Query/Internal/NavigationExpandingExpressionVisitor.cs @@ -32,6 +32,19 @@ private static readonly PropertyInfo _queryContextContextPropertyInfo { QueryableMethods.LastWithPredicate, QueryableMethods.LastWithoutPredicate }, { QueryableMethods.LastOrDefaultWithPredicate, QueryableMethods.LastOrDefaultWithoutPredicate } }; + + private static readonly List _supportedFilteredIncludeOperations = new List + { + QueryableMethods.Where, + QueryableMethods.OrderBy, + QueryableMethods.OrderByDescending, + QueryableMethods.ThenBy, + QueryableMethods.ThenByDescending, + QueryableMethods.Skip, + QueryableMethods.Take, + QueryableMethods.AsQueryable + }; + private readonly QueryTranslationPreprocessor _queryTranslationPreprocessor; private readonly QueryCompilationContext _queryCompilationContext; private readonly PendingSelectorExpandingExpressionVisitor _pendingSelectorExpandingExpressionVisitor; @@ -767,7 +780,7 @@ private NavigationExpansionExpression ProcessInclude(NavigationExpansionExpressi var currentNode = includeTreeNodes.Dequeue(); foreach (var navigation in FindNavigations(currentNode.EntityType, navigationName)) { - var addedNode = currentNode.AddNavigation(navigation); + var addedNode = currentNode.AddNavigation(navigation, withFilter: false); // This is to add eager Loaded navigations when owner type is included. PopulateEagerLoadedNavigations(addedNode); includeTreeNodes.Enqueue(addedNode); @@ -786,7 +799,7 @@ private NavigationExpansionExpression ProcessInclude(NavigationExpansionExpressi ? entityReference.LastIncludeTreeNode : entityReference.IncludePaths; var includeLambda = expression.UnwrapLambdaFromQuote(); - var lastIncludeTree = PopulateIncludeTree(currentIncludeTreeNode, includeLambda.Body); + var lastIncludeTree = PopulateIncludeTree(currentIncludeTreeNode, includeLambda.Body, withFilter: false); if (lastIncludeTree == null) { throw new InvalidOperationException(CoreStrings.InvalidLambdaExpressionInsideInclude); @@ -1445,12 +1458,12 @@ var outboundNavigations foreach (var navigation in outboundNavigations) { - var addedIncludeTreeNode = includeTreeNode.AddNavigation(navigation); + var addedIncludeTreeNode = includeTreeNode.AddNavigation(navigation, withFilter: false); PopulateEagerLoadedNavigations(addedIncludeTreeNode); } } - private IncludeTreeNode PopulateIncludeTree(IncludeTreeNode includeTreeNode, Expression expression) + private IncludeTreeNode PopulateIncludeTree(IncludeTreeNode includeTreeNode, Expression expression, bool withFilter) { switch (expression) { @@ -1459,7 +1472,7 @@ private IncludeTreeNode PopulateIncludeTree(IncludeTreeNode includeTreeNode, Exp case MemberExpression memberExpression: var innerExpression = memberExpression.Expression.UnwrapTypeConversion(out var convertedType); - var innerIncludeTreeNode = PopulateIncludeTree(includeTreeNode, innerExpression); + var innerIncludeTreeNode = PopulateIncludeTree(includeTreeNode, innerExpression, withFilter: false); var entityType = innerIncludeTreeNode.EntityType; if (convertedType != null) { @@ -1473,13 +1486,41 @@ private IncludeTreeNode PopulateIncludeTree(IncludeTreeNode includeTreeNode, Exp var navigation = entityType.FindNavigation(memberExpression.Member); if (navigation != null) { - var addedNode = innerIncludeTreeNode.AddNavigation(navigation); + var addedNode = innerIncludeTreeNode.AddNavigation(navigation, withFilter); + if (withFilter) + { + var prm = Expression.Parameter(expression.Type); + addedNode.FilterExpression = Expression.Lambda(prm, prm); + } + // This is to add eager Loaded navigations when owner type is included. PopulateEagerLoadedNavigations(addedNode); + return addedNode; } break; + + case MethodCallExpression methodCallExpression + when methodCallExpression.Method.DeclaringType == typeof(Queryable): + { + if (!methodCallExpression.Method.IsGenericMethod + || !_supportedFilteredIncludeOperations.Contains(methodCallExpression.Method.GetGenericMethodDefinition())) + { + throw new NotSupportedException(CoreStrings.FilteredIncludeOperationNotSupported(methodCallExpression.Method.Name)); + } + + var result = PopulateIncludeTree(includeTreeNode, methodCallExpression.Arguments[0], withFilter: true); + + var arguments = new List(); + arguments.Add(result.FilterExpression.Body); + arguments.AddRange(methodCallExpression.Arguments.Skip(1)); + result.FilterExpression = Expression.Lambda( + methodCallExpression.Update(methodCallExpression.Object, arguments), + result.FilterExpression.Parameters); + + return result; + } } return null; diff --git a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs index 2f9a7699522..3f1aef3f1ea 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexNavigationsQueryTestBase.cs @@ -4980,5 +4980,284 @@ public virtual Task Contains_over_optional_navigation_with_null_entity_reference }), elementSorter: e => (e.Name, e.OptionalName, e.Contains)); } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Filtered_include_basic_Where(bool async) + { + return AssertIncludeQuery( + async, + ss => ss.Set().Include(l1 => l1.OneToMany_Optional1.Where(l2 => l2.Id > 5)), + new List + { + new ExpectedFilteredInclude( + e => e.OneToMany_Optional1, + "OneToMany_Optional1", + includeFilter: x => x.Where(l2 => l2.Id > 5)) + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Filtered_include_basic_OrderBy_Take(bool async) + { + return AssertIncludeQuery( + async, + ss => ss.Set().Include(l1 => l1.OneToMany_Optional1.OrderBy(x => x.Name).Take(3)), + new List + { + new ExpectedFilteredInclude( + e => e.OneToMany_Optional1, + "OneToMany_Optional1", + includeFilter: x => x.OrderBy(x => x.Name).Take(3)) + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Filtered_include_basic_OrderBy_Skip(bool async) + { + return AssertIncludeQuery( + async, + ss => ss.Set().Include(l1 => l1.OneToMany_Optional1.OrderBy(x => x.Name).Skip(1)), + new List + { + new ExpectedFilteredInclude( + e => e.OneToMany_Optional1, + "OneToMany_Optional1", + includeFilter: x => x.OrderBy(x => x.Name).Skip(1)) + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Filtered_include_basic_OrderBy_Skip_Take(bool async) + { + return AssertIncludeQuery( + async, + ss => ss.Set().Include(l1 => l1.OneToMany_Optional1.OrderBy(x => x.Name).Skip(1).Take(3)), + new List + { + new ExpectedFilteredInclude( + e => e.OneToMany_Optional1, + "OneToMany_Optional1", + includeFilter: x => x.OrderBy(x => x.Name).Skip(1).Take(3)) + }); + } + + [ConditionalFact] + public virtual void Filtered_include_Skip_without_OrderBy() + { + using var ctx = CreateContext(); + var query = ctx.LevelOne.Include(l1 => l1.OneToMany_Optional1.Skip(1)); + var result = query.ToList(); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Filtered_include_on_ThenInclude(bool async) + { + return AssertIncludeQuery( + async, + ss => ss.Set() + .Include(l1 => l1.OneToOne_Optional_FK1) + .ThenInclude(l2 => l2.OneToMany_Optional2.Where(x => x.Name != "Foo").OrderBy(x => x.Name).Skip(1).Take(3)), + new List + { + new ExpectedInclude(e => e.OneToOne_Optional_FK1, "OneToOne_Optional_FK1"), + new ExpectedFilteredInclude( + e => e.OneToMany_Optional2, + "OneToMany_Optional2", + "OneToOne_Optional_FK1", + x => x.Where(x => x.Name != "Foo").OrderBy(x => x.Name).Skip(1).Take(3)) + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Filtered_include_after_reference_navigation(bool async) + { + return AssertIncludeQuery( + async, + ss => ss.Set() + .Include(l1 => l1.OneToOne_Optional_FK1.OneToMany_Optional2.Where(x => x.Name != "Foo").OrderBy(x => x.Name).Skip(1).Take(3)), + new List + { + new ExpectedInclude(e => e.OneToOne_Optional_FK1, "OneToOne_Optional_FK1"), + new ExpectedFilteredInclude( + e => e.OneToMany_Optional2, + "OneToMany_Optional2", + "OneToOne_Optional_FK1", + x => x.Where(x => x.Name != "Foo").OrderBy(x => x.Name).Skip(1).Take(3)) + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Filtered_include_after_different_filtered_include_same_level(bool async) + { + return AssertIncludeQuery( + async, + ss => ss.Set() + .Include(l1 => l1.OneToMany_Optional1.Where(x => x.Name != "Foo").OrderBy(x => x.Name).Take(3)) + .Include(l1 => l1.OneToMany_Required1.Where(x => x.Name != "Bar").OrderByDescending(x => x.Name).Skip(1)), + new List + { + new ExpectedFilteredInclude( + e => e.OneToMany_Optional1, + "OneToMany_Optional1", + includeFilter: x => x.Where(x => x.Name != "Foo").OrderBy(x => x.Name).Take(3)), + new ExpectedFilteredInclude( + e => e.OneToMany_Required1, + "OneToMany_Required1", + includeFilter: x => x.Where(x => x.Name != "Bar").OrderByDescending(x => x.Name).Skip(1)) + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Filtered_include_after_different_filtered_include_different_level(bool async) + { + return AssertIncludeQuery( + async, + ss => ss.Set() + .Include(l1 => l1.OneToMany_Optional1.Where(x => x.Name != "Foo").OrderBy(x => x.Name).Take(3)) + .ThenInclude(l2 => l2.OneToMany_Required2.Where(x => x.Name != "Bar").OrderByDescending(x => x.Name).Skip(1)), + new List + { + new ExpectedFilteredInclude( + e => e.OneToMany_Optional1, + "OneToMany_Optional1", + includeFilter: x => x.Where(x => x.Name != "Foo").OrderBy(x => x.Name).Take(3)), + new ExpectedFilteredInclude( + e => e.OneToMany_Required2, + "OneToMany_Required2", + "OneToMany_Optional1", + includeFilter: x => x.Where(x => x.Name != "Bar").OrderByDescending(x => x.Name).Skip(1)) + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Filtered_include_filter_set_on_same_navigation_twice(bool async) + { + var message = (await Assert.ThrowsAsync( + () => AssertQuery( + async, + ss => ss.Set() + .Include(l1 => l1.OneToMany_Optional1.Where(x => x.Name != "Foo").OrderBy(x => x.Id).Take(3)) + .Include(l1 => l1.OneToMany_Optional1.Where(x => x.Name != "Bar").OrderByDescending(x => x.Name).Take(3))))).Message; + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Filtered_include_and_non_filtered_include_on_same_navigation1(bool async) + { + return AssertIncludeQuery( + async, + ss => ss.Set() + .Include(l1 => l1.OneToMany_Optional1) + .Include(l1 => l1.OneToMany_Optional1.Where(x => x.Name != "Foo").OrderBy(x => x.Id).Take(3)), + new List + { + new ExpectedFilteredInclude( + e => e.OneToMany_Optional1, + "OneToMany_Optional1", + includeFilter: x => x.Where(x => x.Name != "Foo").OrderBy(x => x.Id).Take(3)) + }); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual Task Filtered_include_and_non_filtered_include_on_same_navigation2(bool async) + { + return AssertIncludeQuery( + async, + ss => ss.Set() + .Include(l1 => l1.OneToMany_Optional1.Where(x => x.Name != "Foo").OrderBy(x => x.Id).Take(3)) + .Include(l1 => l1.OneToMany_Optional1), + new List + { + new ExpectedFilteredInclude( + e => e.OneToMany_Optional1, + "OneToMany_Optional1", + includeFilter: x => x.Where(x => x.Name != "Foo").OrderBy(x => x.Id).Take(3)) + }); + } + + [ConditionalFact] + public virtual void Filtered_include_variable_used_inside_filter() + { + using var ctx = CreateContext(); + var prm = "Foo"; + var query = ctx.LevelOne + .Include(l1 => l1.OneToMany_Optional1.Where(x => x.Name != prm).OrderBy(x => x.Id).Take(3)); + var result = query.ToList(); + } + + [ConditionalFact] + public virtual void Filtered_include_context_accessed_inside_filter() + { + using var ctx = CreateContext(); + var query = ctx.LevelOne + .Include(l1 => l1.OneToMany_Optional1.Where(x => ctx.LevelOne.Count() > 7).OrderBy(x => x.Id).Take(3)); + var result = query.ToList(); + } + + [ConditionalFact] + public virtual void Filtered_include_context_accessed_inside_filter_correlated() + { + using var ctx = CreateContext(); + var query = ctx.LevelOne + .Include(l1 => l1.OneToMany_Optional1.Where(x => ctx.LevelOne.Count(xx => xx.Id != x.Id) > 1).OrderBy(x => x.Id).Take(3)); + var result = query.ToList(); + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Filtered_include_include_parameter_used_inside_filter_throws(bool async) + { + await Assert.ThrowsAsync( + () => AssertQuery( + async, + ss => ss.Set() + .Select(l1 => ss.Set().Include(l2 => l2.OneToMany_Optional2.Where(x => x.Id != l2.Id))))); + } + + [ConditionalFact] + public virtual void Filtered_include_outer_parameter_used_inside_filter() + { + // TODO: needs #18191 for result verification + using var ctx = CreateContext(); + var query = ctx.LevelOne.Select(l1 => new + { + l1.Id, + FullInclude = ctx.LevelTwo.Include(l2 => l2.OneToMany_Optional2).ToList(), + FilteredInclude = ctx.LevelTwo.Include(l2 => l2.OneToMany_Optional2.Where(x => x.Id != l1.Id)).ToList() }); + var result = query.ToList(); + } + + [ConditionalFact] + public virtual void Filtered_include_is_considered_loaded() + { + using var ctx = CreateContext(); + var query = ctx.LevelOne.AsTracking().Include(l1 => l1.OneToMany_Optional1.OrderBy(x => x.Id).Take(1)); + var result = query.ToList(); + foreach (var resultElement in result) + { + var entry = ctx.Entry(resultElement); + Assert.True(entry.Navigation("OneToMany_Optional1").IsLoaded); + } + } + + [ConditionalTheory] + [MemberData(nameof(IsAsyncData))] + public virtual async Task Filtered_include_with_Distinct_throws(bool async) + { + var message = (await Assert.ThrowsAsync( + () => AssertQuery( + async, + ss => ss.Set().Include(l1 => l1.OneToMany_Optional1.Distinct())))).Message; + } } } diff --git a/test/EFCore.Specification.Tests/Query/ComplexNavigationsWeakQueryTestBase.cs b/test/EFCore.Specification.Tests/Query/ComplexNavigationsWeakQueryTestBase.cs index 9d1212c727e..fb47a9c9812 100644 --- a/test/EFCore.Specification.Tests/Query/ComplexNavigationsWeakQueryTestBase.cs +++ b/test/EFCore.Specification.Tests/Query/ComplexNavigationsWeakQueryTestBase.cs @@ -169,5 +169,10 @@ public override Task Include_inside_subquery(bool async) { return base.Include_inside_subquery(async); } + + public override void Filtered_include_outer_parameter_used_inside_filter() + { + // TODO: this test can be ran with weak entities once #18191 is fixed and we can use query test infra properly + } } } diff --git a/test/EFCore.Specification.Tests/TestUtilities/ExpectedFilteredInclude.cs b/test/EFCore.Specification.Tests/TestUtilities/ExpectedFilteredInclude.cs new file mode 100644 index 00000000000..d00bbab0511 --- /dev/null +++ b/test/EFCore.Specification.Tests/TestUtilities/ExpectedFilteredInclude.cs @@ -0,0 +1,23 @@ +// 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.Collections.Generic; + +namespace Microsoft.EntityFrameworkCore.TestUtilities +{ + public class ExpectedFilteredInclude : ExpectedInclude + { + public Func, IEnumerable> IncludeFilter { get; } + + public ExpectedFilteredInclude( + Func> include, + string includedName, + string navigationPath = "", + Func, IEnumerable> includeFilter = null) + : base(include, includedName, navigationPath) + { + IncludeFilter = includeFilter; + } + } +} diff --git a/test/EFCore.Specification.Tests/TestUtilities/IncludeQueryResultAsserter.cs b/test/EFCore.Specification.Tests/TestUtilities/IncludeQueryResultAsserter.cs index e675ae80e0a..0402786177e 100644 --- a/test/EFCore.Specification.Tests/TestUtilities/IncludeQueryResultAsserter.cs +++ b/test/EFCore.Specification.Tests/TestUtilities/IncludeQueryResultAsserter.cs @@ -2,7 +2,9 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System; +using System.Collections; using System.Collections.Generic; +using System.Globalization; using System.Linq; using System.Reflection; using Xunit; @@ -15,6 +17,7 @@ public class IncludeQueryResultAsserter private readonly MethodInfo _assertCollectionMethodInfo; private readonly Dictionary _entitySorters; private readonly Dictionary _entityAsserters; + private readonly MethodInfo _filterMethodInfo; private List _path; private Stack _fullPath; @@ -28,6 +31,7 @@ public IncludeQueryResultAsserter( _assertElementMethodInfo = typeof(IncludeQueryResultAsserter).GetTypeInfo().GetDeclaredMethod(nameof(AssertElement)); _assertCollectionMethodInfo = typeof(IncludeQueryResultAsserter).GetTypeInfo().GetDeclaredMethod(nameof(AssertCollection)); + _filterMethodInfo = typeof(IncludeQueryResultAsserter).GetTypeInfo().GetDeclaredMethod(nameof(Filter)); } public virtual void AssertResult(object expected, object actual, IEnumerable expectedIncludes) @@ -149,9 +153,22 @@ protected virtual void AssertCollection( protected void ProcessIncludes(TEntity expected, TEntity actual, IEnumerable expectedIncludes) { var currentPath = string.Join(".", _path); + foreach (var expectedInclude in expectedIncludes.OfType>().Where(i => i.NavigationPath == currentPath)) { var expectedIncludedNavigation = expectedInclude.Include(expected); + if (expectedInclude.GetType().BaseType != typeof(object)) + { + var includedType = expectedInclude.GetType().GetGenericArguments()[1]; + var filterTypedMethod = _filterMethodInfo.MakeGenericMethod(typeof(TEntity), includedType); + expectedIncludedNavigation = filterTypedMethod.Invoke( + this, + BindingFlags.NonPublic, + null, + new object[] { expectedIncludedNavigation, expectedInclude }, + CultureInfo.CurrentCulture); + } + var actualIncludedNavigation = expectedInclude.Include(actual); _path.Add(expectedInclude.IncludedName); @@ -164,6 +181,11 @@ protected void ProcessIncludes(TEntity expected, TEntity actual, IEnume } } + private IEnumerable Filter( + IEnumerable expected, + ExpectedFilteredInclude expectedFilteredInclude) + => expectedFilteredInclude.IncludeFilter(expected); + // for debugging purposes protected string FullPath => string.Join(string.Empty, _fullPath.Reverse()); } diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs index 570051b9f4b..dc294107579 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/ComplexNavigationsQuerySqlServerTest.cs @@ -4476,6 +4476,287 @@ FROM [LevelOne] AS [l1] LEFT JOIN [LevelTwo] AS [l3] ON [l1].[Id] = [l3].[OneToOne_Optional_PK_Inverse2Id]"); } + public override async Task Filtered_include_basic_Where(bool async) + { + await base.Filtered_include_basic_Where(async); + + AssertSql( + @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id], [t].[Id], [t].[Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Optional_Self_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToMany_Required_Self_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [t].[OneToOne_Optional_Self2Id] +FROM [LevelOne] AS [l] +LEFT JOIN ( + SELECT [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] + FROM [LevelTwo] AS [l0] + WHERE [l0].[Id] > 5 +) AS [t] ON [l].[Id] = [t].[OneToMany_Optional_Inverse2Id] +ORDER BY [l].[Id], [t].[Id]"); + } + + public override async Task Filtered_include_basic_OrderBy_Take(bool async) + { + await base.Filtered_include_basic_OrderBy_Take(async); + + AssertSql( + @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id], [t].[Id], [t].[Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Optional_Self_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToMany_Required_Self_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [t].[OneToOne_Optional_Self2Id] +FROM [LevelOne] AS [l] +OUTER APPLY ( + SELECT TOP(3) [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] + FROM [LevelTwo] AS [l0] + WHERE [l].[Id] = [l0].[OneToMany_Optional_Inverse2Id] + ORDER BY [l0].[Name] +) AS [t] +ORDER BY [l].[Id], [t].[Name], [t].[Id]"); + } + + public override async Task Filtered_include_basic_OrderBy_Skip(bool async) + { + await base.Filtered_include_basic_OrderBy_Skip(async); + + AssertSql( + @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id], [t].[Id], [t].[Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Optional_Self_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToMany_Required_Self_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [t].[OneToOne_Optional_Self2Id] +FROM [LevelOne] AS [l] +OUTER APPLY ( + SELECT [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] + FROM [LevelTwo] AS [l0] + WHERE [l].[Id] = [l0].[OneToMany_Optional_Inverse2Id] + ORDER BY [l0].[Name] + OFFSET 1 ROWS +) AS [t] +ORDER BY [l].[Id], [t].[Name], [t].[Id]"); + } + + public override async Task Filtered_include_basic_OrderBy_Skip_Take(bool async) + { + await base.Filtered_include_basic_OrderBy_Skip_Take(async); + + AssertSql( + @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id], [t].[Id], [t].[Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Optional_Self_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToMany_Required_Self_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [t].[OneToOne_Optional_Self2Id] +FROM [LevelOne] AS [l] +OUTER APPLY ( + SELECT [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] + FROM [LevelTwo] AS [l0] + WHERE [l].[Id] = [l0].[OneToMany_Optional_Inverse2Id] + ORDER BY [l0].[Name] + OFFSET 1 ROWS FETCH NEXT 3 ROWS ONLY +) AS [t] +ORDER BY [l].[Id], [t].[Name], [t].[Id]"); + } + + public override void Filtered_include_Skip_without_OrderBy() + { + base.Filtered_include_Skip_without_OrderBy(); + + AssertSql( + @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id], [t].[Id], [t].[Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Optional_Self_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToMany_Required_Self_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [t].[OneToOne_Optional_Self2Id] +FROM [LevelOne] AS [l] +OUTER APPLY ( + SELECT [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] + FROM [LevelTwo] AS [l0] + WHERE [l].[Id] = [l0].[OneToMany_Optional_Inverse2Id] + ORDER BY (SELECT 1) + OFFSET 1 ROWS +) AS [t] +ORDER BY [l].[Id], [t].[Id]"); + } + + public override async Task Filtered_include_on_ThenInclude(bool async) + { + await base.Filtered_include_on_ThenInclude(async); + + AssertSql( + @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id], [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id], [t].[Id], [t].[Level2_Optional_Id], [t].[Level2_Required_Id], [t].[Name], [t].[OneToMany_Optional_Inverse3Id], [t].[OneToMany_Optional_Self_Inverse3Id], [t].[OneToMany_Required_Inverse3Id], [t].[OneToMany_Required_Self_Inverse3Id], [t].[OneToOne_Optional_PK_Inverse3Id], [t].[OneToOne_Optional_Self3Id] +FROM [LevelOne] AS [l] +LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Optional_Id] +OUTER APPLY ( + SELECT [l1].[Id], [l1].[Level2_Optional_Id], [l1].[Level2_Required_Id], [l1].[Name], [l1].[OneToMany_Optional_Inverse3Id], [l1].[OneToMany_Optional_Self_Inverse3Id], [l1].[OneToMany_Required_Inverse3Id], [l1].[OneToMany_Required_Self_Inverse3Id], [l1].[OneToOne_Optional_PK_Inverse3Id], [l1].[OneToOne_Optional_Self3Id] + FROM [LevelThree] AS [l1] + WHERE ([l0].[Id] IS NOT NULL AND ([l0].[Id] = [l1].[OneToMany_Optional_Inverse3Id])) AND (([l1].[Name] <> N'Foo') OR [l1].[Name] IS NULL) + ORDER BY [l1].[Name] + OFFSET 1 ROWS FETCH NEXT 3 ROWS ONLY +) AS [t] +ORDER BY [l].[Id], [t].[Name], [t].[Id]"); + } + + public override async Task Filtered_include_after_reference_navigation(bool async) + { + await base.Filtered_include_after_reference_navigation(async); + + AssertSql( + @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id], [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id], [t].[Id], [t].[Level2_Optional_Id], [t].[Level2_Required_Id], [t].[Name], [t].[OneToMany_Optional_Inverse3Id], [t].[OneToMany_Optional_Self_Inverse3Id], [t].[OneToMany_Required_Inverse3Id], [t].[OneToMany_Required_Self_Inverse3Id], [t].[OneToOne_Optional_PK_Inverse3Id], [t].[OneToOne_Optional_Self3Id] +FROM [LevelOne] AS [l] +LEFT JOIN [LevelTwo] AS [l0] ON [l].[Id] = [l0].[Level1_Optional_Id] +OUTER APPLY ( + SELECT [l1].[Id], [l1].[Level2_Optional_Id], [l1].[Level2_Required_Id], [l1].[Name], [l1].[OneToMany_Optional_Inverse3Id], [l1].[OneToMany_Optional_Self_Inverse3Id], [l1].[OneToMany_Required_Inverse3Id], [l1].[OneToMany_Required_Self_Inverse3Id], [l1].[OneToOne_Optional_PK_Inverse3Id], [l1].[OneToOne_Optional_Self3Id] + FROM [LevelThree] AS [l1] + WHERE ([l0].[Id] IS NOT NULL AND ([l0].[Id] = [l1].[OneToMany_Optional_Inverse3Id])) AND (([l1].[Name] <> N'Foo') OR [l1].[Name] IS NULL) + ORDER BY [l1].[Name] + OFFSET 1 ROWS FETCH NEXT 3 ROWS ONLY +) AS [t] +ORDER BY [l].[Id], [t].[Name], [t].[Id]"); + } + + public override async Task Filtered_include_after_different_filtered_include_same_level(bool async) + { + await base.Filtered_include_after_different_filtered_include_same_level(async); + + AssertSql( + @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id], [t].[Id], [t].[Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Optional_Self_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToMany_Required_Self_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [t].[OneToOne_Optional_Self2Id], [t0].[Id], [t0].[Date], [t0].[Level1_Optional_Id], [t0].[Level1_Required_Id], [t0].[Name], [t0].[OneToMany_Optional_Inverse2Id], [t0].[OneToMany_Optional_Self_Inverse2Id], [t0].[OneToMany_Required_Inverse2Id], [t0].[OneToMany_Required_Self_Inverse2Id], [t0].[OneToOne_Optional_PK_Inverse2Id], [t0].[OneToOne_Optional_Self2Id] +FROM [LevelOne] AS [l] +OUTER APPLY ( + SELECT TOP(3) [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] + FROM [LevelTwo] AS [l0] + WHERE ([l].[Id] = [l0].[OneToMany_Optional_Inverse2Id]) AND (([l0].[Name] <> N'Foo') OR [l0].[Name] IS NULL) + ORDER BY [l0].[Name] +) AS [t] +OUTER APPLY ( + SELECT [l1].[Id], [l1].[Date], [l1].[Level1_Optional_Id], [l1].[Level1_Required_Id], [l1].[Name], [l1].[OneToMany_Optional_Inverse2Id], [l1].[OneToMany_Optional_Self_Inverse2Id], [l1].[OneToMany_Required_Inverse2Id], [l1].[OneToMany_Required_Self_Inverse2Id], [l1].[OneToOne_Optional_PK_Inverse2Id], [l1].[OneToOne_Optional_Self2Id] + FROM [LevelTwo] AS [l1] + WHERE ([l].[Id] = [l1].[OneToMany_Required_Inverse2Id]) AND (([l1].[Name] <> N'Bar') OR [l1].[Name] IS NULL) + ORDER BY [l1].[Name] DESC + OFFSET 1 ROWS +) AS [t0] +ORDER BY [l].[Id], [t].[Name], [t].[Id], [t0].[Name] DESC, [t0].[Id]"); + } + + public override async Task Filtered_include_after_different_filtered_include_different_level(bool async) + { + await base.Filtered_include_after_different_filtered_include_different_level(async); + + AssertSql( + @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id], [t1].[Id], [t1].[Date], [t1].[Level1_Optional_Id], [t1].[Level1_Required_Id], [t1].[Name], [t1].[OneToMany_Optional_Inverse2Id], [t1].[OneToMany_Optional_Self_Inverse2Id], [t1].[OneToMany_Required_Inverse2Id], [t1].[OneToMany_Required_Self_Inverse2Id], [t1].[OneToOne_Optional_PK_Inverse2Id], [t1].[OneToOne_Optional_Self2Id], [t1].[Id0], [t1].[Level2_Optional_Id], [t1].[Level2_Required_Id], [t1].[Name0], [t1].[OneToMany_Optional_Inverse3Id], [t1].[OneToMany_Optional_Self_Inverse3Id], [t1].[OneToMany_Required_Inverse3Id], [t1].[OneToMany_Required_Self_Inverse3Id], [t1].[OneToOne_Optional_PK_Inverse3Id], [t1].[OneToOne_Optional_Self3Id] +FROM [LevelOne] AS [l] +OUTER APPLY ( + SELECT [t].[Id], [t].[Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Optional_Self_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToMany_Required_Self_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [t].[OneToOne_Optional_Self2Id], [t0].[Id] AS [Id0], [t0].[Level2_Optional_Id], [t0].[Level2_Required_Id], [t0].[Name] AS [Name0], [t0].[OneToMany_Optional_Inverse3Id], [t0].[OneToMany_Optional_Self_Inverse3Id], [t0].[OneToMany_Required_Inverse3Id], [t0].[OneToMany_Required_Self_Inverse3Id], [t0].[OneToOne_Optional_PK_Inverse3Id], [t0].[OneToOne_Optional_Self3Id] + FROM ( + SELECT TOP(3) [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] + FROM [LevelTwo] AS [l0] + WHERE ([l].[Id] = [l0].[OneToMany_Optional_Inverse2Id]) AND (([l0].[Name] <> N'Foo') OR [l0].[Name] IS NULL) + ORDER BY [l0].[Name] + ) AS [t] + OUTER APPLY ( + SELECT [l1].[Id], [l1].[Level2_Optional_Id], [l1].[Level2_Required_Id], [l1].[Name], [l1].[OneToMany_Optional_Inverse3Id], [l1].[OneToMany_Optional_Self_Inverse3Id], [l1].[OneToMany_Required_Inverse3Id], [l1].[OneToMany_Required_Self_Inverse3Id], [l1].[OneToOne_Optional_PK_Inverse3Id], [l1].[OneToOne_Optional_Self3Id] + FROM [LevelThree] AS [l1] + WHERE ([t].[Id] = [l1].[OneToMany_Required_Inverse3Id]) AND (([l1].[Name] <> N'Bar') OR [l1].[Name] IS NULL) + ORDER BY [l1].[Name] DESC + OFFSET 1 ROWS + ) AS [t0] +) AS [t1] +ORDER BY [l].[Id], [t1].[Name], [t1].[Id], [t1].[Name0] DESC, [t1].[Id0]"); + } + + public override async Task Filtered_include_and_non_filtered_include_on_same_navigation1(bool async) + { + await base.Filtered_include_and_non_filtered_include_on_same_navigation1(async); + + AssertSql( + @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id], [t].[Id], [t].[Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Optional_Self_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToMany_Required_Self_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [t].[OneToOne_Optional_Self2Id] +FROM [LevelOne] AS [l] +OUTER APPLY ( + SELECT TOP(3) [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] + FROM [LevelTwo] AS [l0] + WHERE ([l].[Id] = [l0].[OneToMany_Optional_Inverse2Id]) AND (([l0].[Name] <> N'Foo') OR [l0].[Name] IS NULL) + ORDER BY [l0].[Id] +) AS [t] +ORDER BY [l].[Id], [t].[Id]"); + } + + public override async Task Filtered_include_and_non_filtered_include_on_same_navigation2(bool async) + { + await base.Filtered_include_and_non_filtered_include_on_same_navigation2(async); + + AssertSql( + @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id], [t].[Id], [t].[Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Optional_Self_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToMany_Required_Self_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [t].[OneToOne_Optional_Self2Id] +FROM [LevelOne] AS [l] +OUTER APPLY ( + SELECT TOP(3) [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] + FROM [LevelTwo] AS [l0] + WHERE ([l].[Id] = [l0].[OneToMany_Optional_Inverse2Id]) AND (([l0].[Name] <> N'Foo') OR [l0].[Name] IS NULL) + ORDER BY [l0].[Id] +) AS [t] +ORDER BY [l].[Id], [t].[Id]"); + } + + public override void Filtered_include_variable_used_inside_filter() + { + base.Filtered_include_variable_used_inside_filter(); + + AssertSql( + @"@__prm_0='Foo' (Size = 4000) + +SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id], [t].[Id], [t].[Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Optional_Self_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToMany_Required_Self_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [t].[OneToOne_Optional_Self2Id] +FROM [LevelOne] AS [l] +OUTER APPLY ( + SELECT TOP(3) [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] + FROM [LevelTwo] AS [l0] + WHERE ([l].[Id] = [l0].[OneToMany_Optional_Inverse2Id]) AND (([l0].[Name] <> @__prm_0) OR [l0].[Name] IS NULL) + ORDER BY [l0].[Id] +) AS [t] +ORDER BY [l].[Id], [t].[Id]"); + } + + public override void Filtered_include_context_accessed_inside_filter() + { + base.Filtered_include_context_accessed_inside_filter(); + + AssertSql( + @"SELECT COUNT(*) +FROM [LevelOne] AS [l]", + // + @"@__p_0='True' + +SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id], [t].[Id], [t].[Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Optional_Self_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToMany_Required_Self_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [t].[OneToOne_Optional_Self2Id] +FROM [LevelOne] AS [l] +OUTER APPLY ( + SELECT TOP(3) [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] + FROM [LevelTwo] AS [l0] + WHERE ([l].[Id] = [l0].[OneToMany_Optional_Inverse2Id]) AND (@__p_0 = CAST(1 AS bit)) + ORDER BY [l0].[Id] +) AS [t] +ORDER BY [l].[Id], [t].[Id]"); + } + + public override void Filtered_include_context_accessed_inside_filter_correlated() + { + base.Filtered_include_context_accessed_inside_filter_correlated(); + + AssertSql( + @"SELECT [l].[Id], [l].[Date], [l].[Name], [l].[OneToMany_Optional_Self_Inverse1Id], [l].[OneToMany_Required_Self_Inverse1Id], [l].[OneToOne_Optional_Self1Id], [t].[Id], [t].[Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Optional_Self_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToMany_Required_Self_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [t].[OneToOne_Optional_Self2Id] +FROM [LevelOne] AS [l] +OUTER APPLY ( + SELECT TOP(3) [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id] + FROM [LevelTwo] AS [l0] + WHERE ([l].[Id] = [l0].[OneToMany_Optional_Inverse2Id]) AND (( + SELECT COUNT(*) + FROM [LevelOne] AS [l1] + WHERE [l1].[Id] <> [l0].[Id]) > 1) + ORDER BY [l0].[Id] +) AS [t] +ORDER BY [l].[Id], [t].[Id]"); + } + + public override void Filtered_include_outer_parameter_used_inside_filter() + { + base.Filtered_include_outer_parameter_used_inside_filter(); + + AssertSql( + @"SELECT [l].[Id], [t].[Id], [t].[Date], [t].[Level1_Optional_Id], [t].[Level1_Required_Id], [t].[Name], [t].[OneToMany_Optional_Inverse2Id], [t].[OneToMany_Optional_Self_Inverse2Id], [t].[OneToMany_Required_Inverse2Id], [t].[OneToMany_Required_Self_Inverse2Id], [t].[OneToOne_Optional_PK_Inverse2Id], [t].[OneToOne_Optional_Self2Id], [t].[Id0], [t].[Level2_Optional_Id], [t].[Level2_Required_Id], [t].[Name0], [t].[OneToMany_Optional_Inverse3Id], [t].[OneToMany_Optional_Self_Inverse3Id], [t].[OneToMany_Required_Inverse3Id], [t].[OneToMany_Required_Self_Inverse3Id], [t].[OneToOne_Optional_PK_Inverse3Id], [t].[OneToOne_Optional_Self3Id], [t1].[Id], [t1].[Date], [t1].[Level1_Optional_Id], [t1].[Level1_Required_Id], [t1].[Name], [t1].[OneToMany_Optional_Inverse2Id], [t1].[OneToMany_Optional_Self_Inverse2Id], [t1].[OneToMany_Required_Inverse2Id], [t1].[OneToMany_Required_Self_Inverse2Id], [t1].[OneToOne_Optional_PK_Inverse2Id], [t1].[OneToOne_Optional_Self2Id], [t1].[Id0], [t1].[Level2_Optional_Id], [t1].[Level2_Required_Id], [t1].[Name0], [t1].[OneToMany_Optional_Inverse3Id], [t1].[OneToMany_Optional_Self_Inverse3Id], [t1].[OneToMany_Required_Inverse3Id], [t1].[OneToMany_Required_Self_Inverse3Id], [t1].[OneToOne_Optional_PK_Inverse3Id], [t1].[OneToOne_Optional_Self3Id] +FROM [LevelOne] AS [l] +OUTER APPLY ( + SELECT [l0].[Id], [l0].[Date], [l0].[Level1_Optional_Id], [l0].[Level1_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_Inverse2Id], [l0].[OneToMany_Optional_Self_Inverse2Id], [l0].[OneToMany_Required_Inverse2Id], [l0].[OneToMany_Required_Self_Inverse2Id], [l0].[OneToOne_Optional_PK_Inverse2Id], [l0].[OneToOne_Optional_Self2Id], [l1].[Id] AS [Id0], [l1].[Level2_Optional_Id], [l1].[Level2_Required_Id], [l1].[Name] AS [Name0], [l1].[OneToMany_Optional_Inverse3Id], [l1].[OneToMany_Optional_Self_Inverse3Id], [l1].[OneToMany_Required_Inverse3Id], [l1].[OneToMany_Required_Self_Inverse3Id], [l1].[OneToOne_Optional_PK_Inverse3Id], [l1].[OneToOne_Optional_Self3Id] + FROM [LevelTwo] AS [l0] + LEFT JOIN [LevelThree] AS [l1] ON [l0].[Id] = [l1].[OneToMany_Optional_Inverse3Id] +) AS [t] +OUTER APPLY ( + SELECT [l2].[Id], [l2].[Date], [l2].[Level1_Optional_Id], [l2].[Level1_Required_Id], [l2].[Name], [l2].[OneToMany_Optional_Inverse2Id], [l2].[OneToMany_Optional_Self_Inverse2Id], [l2].[OneToMany_Required_Inverse2Id], [l2].[OneToMany_Required_Self_Inverse2Id], [l2].[OneToOne_Optional_PK_Inverse2Id], [l2].[OneToOne_Optional_Self2Id], [t0].[Id] AS [Id0], [t0].[Level2_Optional_Id], [t0].[Level2_Required_Id], [t0].[Name] AS [Name0], [t0].[OneToMany_Optional_Inverse3Id], [t0].[OneToMany_Optional_Self_Inverse3Id], [t0].[OneToMany_Required_Inverse3Id], [t0].[OneToMany_Required_Self_Inverse3Id], [t0].[OneToOne_Optional_PK_Inverse3Id], [t0].[OneToOne_Optional_Self3Id] + FROM [LevelTwo] AS [l2] + LEFT JOIN ( + SELECT [l3].[Id], [l3].[Level2_Optional_Id], [l3].[Level2_Required_Id], [l3].[Name], [l3].[OneToMany_Optional_Inverse3Id], [l3].[OneToMany_Optional_Self_Inverse3Id], [l3].[OneToMany_Required_Inverse3Id], [l3].[OneToMany_Required_Self_Inverse3Id], [l3].[OneToOne_Optional_PK_Inverse3Id], [l3].[OneToOne_Optional_Self3Id] + FROM [LevelThree] AS [l3] + WHERE [l3].[Id] <> [l].[Id] + ) AS [t0] ON [l2].[Id] = [t0].[OneToMany_Optional_Inverse3Id] +) AS [t1] +ORDER BY [l].[Id], [t].[Id], [t].[Id0], [t1].[Id], [t1].[Id0]"); + } + private void AssertSql(params string[] expected) => Fixture.TestSqlLoggerFactory.AssertBaseline(expected); } } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsQuerySqliteTest.cs index f82f06b6a3c..8d8152c3905 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsQuerySqliteTest.cs @@ -2,6 +2,7 @@ // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.TestModels.TransportationModel; using Xunit; namespace Microsoft.EntityFrameworkCore.Query @@ -34,5 +35,20 @@ public override Task Include_inside_subquery(bool async) // Sqlite does not support cross/outer apply public override Task SelectMany_with_outside_reference_to_joined_table_correctly_translated_to_apply(bool async) => null; public override Task Nested_SelectMany_correlated_with_join_table_correctly_translated_to_apply(bool async) => null; + public override void Filtered_include_Skip_without_OrderBy() { } + public override Task Filtered_include_after_different_filtered_include_same_level(bool async) => null; + public override Task Filtered_include_after_different_filtered_include_different_level(bool async) => null; + public override Task Filtered_include_after_reference_navigation(bool async) => null; + public override Task Filtered_include_and_non_filtered_include_on_same_navigation1(bool async) => null; + public override Task Filtered_include_and_non_filtered_include_on_same_navigation2(bool async) => null; + public override Task Filtered_include_basic_OrderBy_Take(bool async) => null; + public override Task Filtered_include_basic_OrderBy_Skip(bool async) => null; + public override Task Filtered_include_basic_OrderBy_Skip_Take(bool async) => null; + public override void Filtered_include_context_accessed_inside_filter() { } + public override void Filtered_include_context_accessed_inside_filter_correlated() { } + public override Task Filtered_include_on_ThenInclude(bool async) => null; + public override void Filtered_include_outer_parameter_used_inside_filter() { } + public override void Filtered_include_variable_used_inside_filter() { } + public override void Filtered_include_is_considered_loaded() { } } } diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsWeakQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsWeakQuerySqliteTest.cs index b33bff236a4..694fe54e2ce 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsWeakQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/ComplexNavigationsWeakQuerySqliteTest.cs @@ -29,5 +29,19 @@ public override Task Project_collection_navigation_nested_with_take(bool async) // Sqlite does not support cross/outer apply public override Task SelectMany_with_outside_reference_to_joined_table_correctly_translated_to_apply(bool async) => null; public override Task Nested_SelectMany_correlated_with_join_table_correctly_translated_to_apply(bool async) => null; + public override void Filtered_include_Skip_without_OrderBy() { } + public override Task Filtered_include_after_different_filtered_include_same_level(bool async) => null; + public override Task Filtered_include_after_different_filtered_include_different_level(bool async) => null; + public override Task Filtered_include_after_reference_navigation(bool async) => null; + public override Task Filtered_include_and_non_filtered_include_on_same_navigation1(bool async) => null; + public override Task Filtered_include_and_non_filtered_include_on_same_navigation2(bool async) => null; + public override Task Filtered_include_basic_OrderBy_Take(bool async) => null; + public override Task Filtered_include_basic_OrderBy_Skip(bool async) => null; + public override Task Filtered_include_basic_OrderBy_Skip_Take(bool async) => null; + public override void Filtered_include_context_accessed_inside_filter() { } + public override void Filtered_include_context_accessed_inside_filter_correlated() { } + public override Task Filtered_include_on_ThenInclude(bool async) => null; + public override void Filtered_include_variable_used_inside_filter() { } + public override void Filtered_include_is_considered_loaded() { } } }