From c7ff76d237ccd6647b070f955d3c72a9118f6b1a Mon Sep 17 00:00:00 2001 From: Maurycy Markowski Date: Thu, 21 Jul 2016 16:33:52 -0700 Subject: [PATCH] Fix to #5672 - Query :: Include with multiple navigations (including optional navigation) fails Problem was that our include logic for groupjoin was expecting outer and inner elements (on which the include was being performed) to be entities. However sometimes those elements would be TransparentIdentifiers (e.g. when the element comes from a result of SelectMany). Fix is to also store (when necessary) the accessor from the outer/inner element to the entity that we want to include, extract the actual entity in runtime and apply include operations on that entity instead of the actual outer/inner element. --- .../Query/AsyncQueryMethodProvider.cs | 20 ++++- .../Internal/IncludeExpressionVisitor.cs | 25 +++++- .../Query/Internal/AsyncGroupJoinInclude.cs | 18 +++++ .../Query/Internal/GroupJoinInclude.cs | 18 +++++ .../Query/QueryMethodProvider.cs | 20 ++++- .../AsyncQueryNavigationsTestBase.cs | 78 +++++++++++++++++++ .../ComplexNavigationsQueryTestBase.cs | 42 ++++++++++ ...tyFrameworkCore.Specification.Tests.csproj | 1 + .../QueryNavigationsTestBase.cs | 10 +++ .../AsyncQueryNavigationsSqlServerTests.cs | 17 ++++ .../ComplexNavigationsQuerySqlServerTest.cs | 29 +++++++ ...eworkCore.SqlServer.FunctionalTests.csproj | 1 + .../QueryNavigationsSqlServerTest.cs | 15 ++++ 13 files changed, 287 insertions(+), 7 deletions(-) create mode 100644 src/Microsoft.EntityFrameworkCore.Specification.Tests/AsyncQueryNavigationsTestBase.cs create mode 100644 test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/AsyncQueryNavigationsSqlServerTests.cs diff --git a/src/Microsoft.EntityFrameworkCore.Relational/Query/AsyncQueryMethodProvider.cs b/src/Microsoft.EntityFrameworkCore.Relational/Query/AsyncQueryMethodProvider.cs index f49e9b73ec3..6f7fbbb7a3d 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational/Query/AsyncQueryMethodProvider.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational/Query/AsyncQueryMethodProvider.cs @@ -418,7 +418,15 @@ var outer if (_groupJoinAsyncEnumerable._outerGroupJoinInclude != null) { - await _groupJoinAsyncEnumerable._outerGroupJoinInclude.IncludeAsync(outer, cancellationToken); + var outerEntityAccessor = _groupJoinAsyncEnumerable._outerGroupJoinInclude.EntityAccessor as Func; + if (outerEntityAccessor != null) + { + await _groupJoinAsyncEnumerable._outerGroupJoinInclude.IncludeAsync(outerEntityAccessor(outer), cancellationToken); + } + else + { + await _groupJoinAsyncEnumerable._outerGroupJoinInclude.IncludeAsync(outer, cancellationToken); + } } var inner @@ -442,7 +450,15 @@ var inner if (_groupJoinAsyncEnumerable._innerGroupJoinInclude != null) { - await _groupJoinAsyncEnumerable._innerGroupJoinInclude.IncludeAsync(inner, cancellationToken); + var innerEntityAccessor = _groupJoinAsyncEnumerable._innerGroupJoinInclude.EntityAccessor as Func; + if (innerEntityAccessor != null) + { + await _groupJoinAsyncEnumerable._innerGroupJoinInclude.IncludeAsync(innerEntityAccessor(inner), cancellationToken); + } + else + { + await _groupJoinAsyncEnumerable._innerGroupJoinInclude.IncludeAsync(inner, cancellationToken); + } } inners.Add(inner); diff --git a/src/Microsoft.EntityFrameworkCore.Relational/Query/ExpressionVisitors/Internal/IncludeExpressionVisitor.cs b/src/Microsoft.EntityFrameworkCore.Relational/Query/ExpressionVisitors/Internal/IncludeExpressionVisitor.cs index 801160f507a..8fb5d36def9 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational/Query/ExpressionVisitors/Internal/IncludeExpressionVisitor.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational/Query/ExpressionVisitors/Internal/IncludeExpressionVisitor.cs @@ -145,12 +145,22 @@ var shaper { var groupJoinIncludeArgumentIndex = shaperArgumentIndex + 4; + var existingGroupJoinIncludeArgument = methodCallExpression.Arguments[groupJoinIncludeArgumentIndex]; + var existingGroupJoinIncludeWithAccessor = existingGroupJoinIncludeArgument as MethodCallExpression; + var withAccessorMethodInfo = _queryCompilationContext.QueryMethodProvider.GroupJoinIncludeType + .GetTypeInfo().GetDeclaredMethod(nameof(GroupJoinInclude.WithEntityAccessor)); + + var existingGroupJoinIncludeExpression = existingGroupJoinIncludeWithAccessor != null + && existingGroupJoinIncludeWithAccessor.Method == withAccessorMethodInfo + ? existingGroupJoinIncludeWithAccessor.Object + : existingGroupJoinIncludeArgument; + var groupJoinInclude = _queryCompilationContext.QueryMethodProvider .CreateGroupJoinInclude( _navigationPath, _querySourceRequiresTracking, - (methodCallExpression.Arguments[groupJoinIncludeArgumentIndex] as ConstantExpression)?.Value, + (existingGroupJoinIncludeExpression as ConstantExpression)?.Value, _createRelatedEntitiesLoadersMethodInfo .MakeGenericMethod(_queryCompilationContext.QueryMethodProvider.RelatedEntitiesLoaderType) .Invoke(this, new object[] @@ -161,9 +171,18 @@ var groupJoinInclude if (groupJoinInclude != null) { - var newArguments = methodCallExpression.Arguments.ToList(); + var groupJoinIncludeExpression = (Expression)Expression.Constant(groupJoinInclude); + var accessorLambda = shaper.GetAccessorExpression(_querySource) as LambdaExpression; + if (accessorLambda != null && accessorLambda.Parameters.Single().Type.GetTypeInfo().IsValueType) + { + groupJoinIncludeExpression = Expression.Call( + groupJoinIncludeExpression, + withAccessorMethodInfo, + shaper.GetAccessorExpression(_querySource)); + } - newArguments[groupJoinIncludeArgumentIndex] = Expression.Constant(groupJoinInclude); + var newArguments = methodCallExpression.Arguments.ToList(); + newArguments[groupJoinIncludeArgumentIndex] = groupJoinIncludeExpression; return methodCallExpression.Update(methodCallExpression.Object, newArguments); } diff --git a/src/Microsoft.EntityFrameworkCore.Relational/Query/Internal/AsyncGroupJoinInclude.cs b/src/Microsoft.EntityFrameworkCore.Relational/Query/Internal/AsyncGroupJoinInclude.cs index 7a26beced76..8592c1f7e9b 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational/Query/Internal/AsyncGroupJoinInclude.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational/Query/Internal/AsyncGroupJoinInclude.cs @@ -24,6 +24,7 @@ public class AsyncGroupJoinInclude : IDisposable private RelationalQueryContext _queryContext; private IAsyncRelatedEntitiesLoader[] _relatedEntitiesLoaders; private AsyncGroupJoinInclude _previous; + private Delegate _entityAccessor; /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used @@ -39,6 +40,23 @@ public AsyncGroupJoinInclude( _querySourceRequiresTracking = querySourceRequiresTracking; } + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public Delegate EntityAccessor => _entityAccessor; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual AsyncGroupJoinInclude WithEntityAccessor([NotNull] Delegate entityAccessor) + { + _entityAccessor = entityAccessor; + + return this; + } + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. diff --git a/src/Microsoft.EntityFrameworkCore.Relational/Query/Internal/GroupJoinInclude.cs b/src/Microsoft.EntityFrameworkCore.Relational/Query/Internal/GroupJoinInclude.cs index 461e93b14b3..29646657eef 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational/Query/Internal/GroupJoinInclude.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational/Query/Internal/GroupJoinInclude.cs @@ -22,6 +22,7 @@ public class GroupJoinInclude : IDisposable private RelationalQueryContext _queryContext; private IRelatedEntitiesLoader[] _relatedEntitiesLoaders; private GroupJoinInclude _previous; + private Delegate _entityAccessor; /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used @@ -37,6 +38,23 @@ public GroupJoinInclude( _querySourceRequiresTracking = querySourceRequiresTracking; } + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public Delegate EntityAccessor => _entityAccessor; + + /// + /// This API supports the Entity Framework Core infrastructure and is not intended to be used + /// directly from your code. This API may change or be removed in future releases. + /// + public virtual GroupJoinInclude WithEntityAccessor([NotNull] Delegate entityAccessor) + { + _entityAccessor = entityAccessor; + + return this; + } + /// /// This API supports the Entity Framework Core infrastructure and is not intended to be used /// directly from your code. This API may change or be removed in future releases. diff --git a/src/Microsoft.EntityFrameworkCore.Relational/Query/QueryMethodProvider.cs b/src/Microsoft.EntityFrameworkCore.Relational/Query/QueryMethodProvider.cs index 82436465f6d..17afb1cf312 100644 --- a/src/Microsoft.EntityFrameworkCore.Relational/Query/QueryMethodProvider.cs +++ b/src/Microsoft.EntityFrameworkCore.Relational/Query/QueryMethodProvider.cs @@ -292,7 +292,15 @@ var outer nextOuter = default(TOuter); - outerGroupJoinInclude?.Include(outer); + var outerAccessor = outerGroupJoinInclude?.EntityAccessor as Func; + if (outerAccessor != null) + { + outerGroupJoinInclude?.Include(outerAccessor(outer)); + } + else + { + outerGroupJoinInclude?.Include(outer); + } var inner = innerShaper.Shape(queryContext, sourceEnumerator.Current); var inners = new List(); @@ -307,7 +315,15 @@ var outer { var currentGroupKey = innerKeySelector(inner); - innerGroupJoinInclude?.Include(inner); + var innerAccessor = innerGroupJoinInclude?.EntityAccessor as Func; + if (innerAccessor != null) + { + innerGroupJoinInclude?.Include(innerAccessor(inner)); + } + else + { + innerGroupJoinInclude?.Include(outer); + } inners.Add(inner); diff --git a/src/Microsoft.EntityFrameworkCore.Specification.Tests/AsyncQueryNavigationsTestBase.cs b/src/Microsoft.EntityFrameworkCore.Specification.Tests/AsyncQueryNavigationsTestBase.cs new file mode 100644 index 00000000000..76715d261fc --- /dev/null +++ b/src/Microsoft.EntityFrameworkCore.Specification.Tests/AsyncQueryNavigationsTestBase.cs @@ -0,0 +1,78 @@ +// 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; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Specification.Tests.TestModels.Northwind; +using Microsoft.EntityFrameworkCore.Specification.Tests.TestUtilities.Xunit; +using Xunit; + +namespace Microsoft.EntityFrameworkCore.Specification.Tests +{ + public abstract class AsyncQueryNavigationsTestBase : IClassFixture + where TFixture : NorthwindQueryFixtureBase, new() + { + [ConditionalFact] + public virtual async Task Include_with_multiple_optional_navigations() + { + await AssertQuery( + ods => ods + .Include(od => od.Order.Customer) + .Where(od => od.Order.Customer.City == "London"), + entryCount: 164); + } + + [ConditionalFact] + public virtual async Task Multiple_include_with_multiple_optional_navigations() + { + await AssertQuery( + ods => ods + .Include(od => od.Order.Customer) + .Include(od => od.Product) + .Where(od => od.Order.Customer.City == "London"), + entryCount: 221); + } + + protected NorthwindContext CreateContext() + { + return Fixture.CreateContext(); + } + + protected AsyncQueryNavigationsTestBase(TFixture fixture) + { + Fixture = fixture; + } + + protected TFixture Fixture { get; } + + protected async Task AssertQuery( + Func, IQueryable> query, + bool assertOrder = false, + int entryCount = 0, + Action, IList> asserter = null) + where TItem : class + => await AssertQuery(query, query, assertOrder, entryCount, asserter); + + protected async Task AssertQuery( + Func, IQueryable> efQuery, + Func, IQueryable> l2oQuery, + bool assertOrder = false, + int entryCount = 0, + Action, IList> asserter = null) + where TItem : class + { + using (var context = CreateContext()) + { + TestHelpers.AssertResults( + l2oQuery(NorthwindData.Set()).ToArray(), + await efQuery(context.Set()).ToArrayAsync(), + assertOrder, + asserter); + + Assert.Equal(entryCount, context.ChangeTracker.Entries().Count()); + } + } + } +} diff --git a/src/Microsoft.EntityFrameworkCore.Specification.Tests/ComplexNavigationsQueryTestBase.cs b/src/Microsoft.EntityFrameworkCore.Specification.Tests/ComplexNavigationsQueryTestBase.cs index a06cbdcf1e4..4e9f0a7579d 100644 --- a/src/Microsoft.EntityFrameworkCore.Specification.Tests/ComplexNavigationsQueryTestBase.cs +++ b/src/Microsoft.EntityFrameworkCore.Specification.Tests/ComplexNavigationsQueryTestBase.cs @@ -2473,6 +2473,48 @@ public virtual void Complex_multi_include_with_order_by_and_paging() } } + [ConditionalFact] + public virtual void Multiple_include_with_multiple_optional_navigations() + { + List expected; + using (var context = CreateContext()) + { + expected = context.LevelOne + .Include(e => e.OneToOne_Required_FK).ThenInclude(e => e.OneToMany_Optional) + .Include(e => e.OneToOne_Required_FK).ThenInclude(e => e.OneToOne_Optional_FK) + .Include(e => e.OneToOne_Optional_FK).ThenInclude(e => e.OneToOne_Optional_FK) + .ToList() + .Where(e => e.OneToOne_Required_FK.OneToOne_Optional_PK?.Name != "Foo") + .OrderBy(e => e.Id) + .ToList(); + } + + ClearLog(); + + using (var context = CreateContext()) + { + var query = context.LevelOne + .Include(e => e.OneToOne_Required_FK).ThenInclude(e => e.OneToMany_Optional) + .Include(e => e.OneToOne_Required_FK).ThenInclude(e => e.OneToOne_Optional_FK) + .Include(e => e.OneToOne_Optional_FK).ThenInclude(e => e.OneToOne_Optional_FK) + .Where(e => e.OneToOne_Required_FK.OneToOne_Optional_PK.Name != "Foo") + .OrderBy(e => e.Id); + + var result = query.ToList(); + Assert.Equal(expected.Count, result.Count); + for (var i = 0; i < result.Count; i++) + { + Assert.True(expected[i].Id == result[i].Id); + Assert.True(expected[i].Name == result[i].Name); + Assert.True(expected[i].OneToOne_Required_FK?.Id == result[i].OneToOne_Required_FK?.Id); + Assert.True(expected[i].OneToOne_Required_FK?.OneToOne_Optional_FK?.Id == result[i].OneToOne_Required_FK?.OneToOne_Optional_FK?.Id); + + Assert.True(expected[i].OneToOne_Optional_FK?.Id == result[i].OneToOne_Optional_FK?.Id); + Assert.True(expected[i].OneToOne_Optional_FK?.OneToOne_Optional_FK?.Id == result[i].OneToOne_Optional_FK?.OneToOne_Optional_FK?.Id); + } + } + } + [ConditionalFact] public virtual void Correlated_subquery_doesnt_project_unnecessary_columns_in_top_level() { diff --git a/src/Microsoft.EntityFrameworkCore.Specification.Tests/Microsoft.EntityFrameworkCore.Specification.Tests.csproj b/src/Microsoft.EntityFrameworkCore.Specification.Tests/Microsoft.EntityFrameworkCore.Specification.Tests.csproj index 0662f8896ae..f131e1faf2c 100644 --- a/src/Microsoft.EntityFrameworkCore.Specification.Tests/Microsoft.EntityFrameworkCore.Specification.Tests.csproj +++ b/src/Microsoft.EntityFrameworkCore.Specification.Tests/Microsoft.EntityFrameworkCore.Specification.Tests.csproj @@ -39,6 +39,7 @@ + diff --git a/src/Microsoft.EntityFrameworkCore.Specification.Tests/QueryNavigationsTestBase.cs b/src/Microsoft.EntityFrameworkCore.Specification.Tests/QueryNavigationsTestBase.cs index 134c5cf978e..e0dd109681a 100644 --- a/src/Microsoft.EntityFrameworkCore.Specification.Tests/QueryNavigationsTestBase.cs +++ b/src/Microsoft.EntityFrameworkCore.Specification.Tests/QueryNavigationsTestBase.cs @@ -230,6 +230,16 @@ public virtual void Select_Where_Navigation_Included() entryCount: 15); } + [ConditionalFact] + public virtual void Include_with_multiple_optional_navigations() + { + AssertQuery( + ods => ods + .Include(od => od.Order.Customer) + .Where(od => od.Order.Customer.City == "London"), + entryCount: 164); + } + [ConditionalFact] public virtual void Select_count_plus_sum() { diff --git a/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/AsyncQueryNavigationsSqlServerTests.cs b/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/AsyncQueryNavigationsSqlServerTests.cs new file mode 100644 index 00000000000..f32c6926154 --- /dev/null +++ b/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/AsyncQueryNavigationsSqlServerTests.cs @@ -0,0 +1,17 @@ +// 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.Threading.Tasks; +using Microsoft.EntityFrameworkCore.Specification.Tests; +using Xunit.Abstractions; + +namespace Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests +{ + public class AsyncQueryNavigationsSqlServerTests : AsyncQueryNavigationsTestBase + { + public AsyncQueryNavigationsSqlServerTests(NorthwindQuerySqlServerFixture fixture, ITestOutputHelper testOutputHelper) + : base(fixture) + { + // TestSqlLoggerFactory.CaptureOutput(testOutputHelper); + } + } +} diff --git a/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/ComplexNavigationsQuerySqlServerTest.cs b/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/ComplexNavigationsQuerySqlServerTest.cs index ff1e5639a0e..beacf61f434 100644 --- a/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/ComplexNavigationsQuerySqlServerTest.cs +++ b/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/ComplexNavigationsQuerySqlServerTest.cs @@ -1316,6 +1316,35 @@ OFFSET @__p_0 ROWS FETCH NEXT @__p_1 ROWS ONLY } } + public override void Multiple_include_with_multiple_optional_navigations() + { + base.Multiple_include_with_multiple_optional_navigations(); + + Assert.Equal( + @"SELECT [e].[Id], [e].[Name], [e].[OneToMany_Optional_Self_InverseId], [e].[OneToMany_Required_Self_InverseId], [e].[OneToOne_Optional_SelfId], [e.OneToOne_Required_FK].[Id], [e.OneToOne_Required_FK].[Level1_Optional_Id], [e.OneToOne_Required_FK].[Level1_Required_Id], [e.OneToOne_Required_FK].[Name], [e.OneToOne_Required_FK].[OneToMany_Optional_InverseId], [e.OneToOne_Required_FK].[OneToMany_Optional_Self_InverseId], [e.OneToOne_Required_FK].[OneToMany_Required_InverseId], [e.OneToOne_Required_FK].[OneToMany_Required_Self_InverseId], [e.OneToOne_Required_FK].[OneToOne_Optional_PK_InverseId], [e.OneToOne_Required_FK].[OneToOne_Optional_SelfId], [e.OneToOne_Required_FK.OneToOne_Optional_PK].[Id], [e.OneToOne_Required_FK.OneToOne_Optional_PK].[Level2_Optional_Id], [e.OneToOne_Required_FK.OneToOne_Optional_PK].[Level2_Required_Id], [e.OneToOne_Required_FK.OneToOne_Optional_PK].[Name], [e.OneToOne_Required_FK.OneToOne_Optional_PK].[OneToMany_Optional_InverseId], [e.OneToOne_Required_FK.OneToOne_Optional_PK].[OneToMany_Optional_Self_InverseId], [e.OneToOne_Required_FK.OneToOne_Optional_PK].[OneToMany_Required_InverseId], [e.OneToOne_Required_FK.OneToOne_Optional_PK].[OneToMany_Required_Self_InverseId], [e.OneToOne_Required_FK.OneToOne_Optional_PK].[OneToOne_Optional_PK_InverseId], [e.OneToOne_Required_FK.OneToOne_Optional_PK].[OneToOne_Optional_SelfId], [l].[Id], [l].[Level1_Optional_Id], [l].[Level1_Required_Id], [l].[Name], [l].[OneToMany_Optional_InverseId], [l].[OneToMany_Optional_Self_InverseId], [l].[OneToMany_Required_InverseId], [l].[OneToMany_Required_Self_InverseId], [l].[OneToOne_Optional_PK_InverseId], [l].[OneToOne_Optional_SelfId], [l2].[Id], [l2].[Level1_Optional_Id], [l2].[Level1_Required_Id], [l2].[Name], [l2].[OneToMany_Optional_InverseId], [l2].[OneToMany_Optional_Self_InverseId], [l2].[OneToMany_Required_InverseId], [l2].[OneToMany_Required_Self_InverseId], [l2].[OneToOne_Optional_PK_InverseId], [l2].[OneToOne_Optional_SelfId], [l3].[Id], [l3].[Level2_Optional_Id], [l3].[Level2_Required_Id], [l3].[Name], [l3].[OneToMany_Optional_InverseId], [l3].[OneToMany_Optional_Self_InverseId], [l3].[OneToMany_Required_InverseId], [l3].[OneToMany_Required_Self_InverseId], [l3].[OneToOne_Optional_PK_InverseId], [l3].[OneToOne_Optional_SelfId], [l4].[Id], [l4].[Level1_Optional_Id], [l4].[Level1_Required_Id], [l4].[Name], [l4].[OneToMany_Optional_InverseId], [l4].[OneToMany_Optional_Self_InverseId], [l4].[OneToMany_Required_InverseId], [l4].[OneToMany_Required_Self_InverseId], [l4].[OneToOne_Optional_PK_InverseId], [l4].[OneToOne_Optional_SelfId], [l5].[Id], [l5].[Level2_Optional_Id], [l5].[Level2_Required_Id], [l5].[Name], [l5].[OneToMany_Optional_InverseId], [l5].[OneToMany_Optional_Self_InverseId], [l5].[OneToMany_Required_InverseId], [l5].[OneToMany_Required_Self_InverseId], [l5].[OneToOne_Optional_PK_InverseId], [l5].[OneToOne_Optional_SelfId] +FROM [Level1] AS [e] +INNER JOIN [Level2] AS [e.OneToOne_Required_FK] ON [e].[Id] = [e.OneToOne_Required_FK].[Level1_Required_Id] +LEFT JOIN [Level3] AS [e.OneToOne_Required_FK.OneToOne_Optional_PK] ON [e.OneToOne_Required_FK].[Id] = [e.OneToOne_Required_FK.OneToOne_Optional_PK].[OneToOne_Optional_PK_InverseId] +LEFT JOIN [Level2] AS [l] ON [l].[Level1_Required_Id] = [e].[Id] +LEFT JOIN [Level2] AS [l2] ON [l2].[Level1_Required_Id] = [e].[Id] +LEFT JOIN [Level3] AS [l3] ON [l3].[Level2_Optional_Id] = [l2].[Id] +LEFT JOIN [Level2] AS [l4] ON [l4].[Level1_Optional_Id] = [e].[Id] +LEFT JOIN [Level3] AS [l5] ON [l5].[Level2_Optional_Id] = [l4].[Id] +ORDER BY [e].[Id], [e.OneToOne_Required_FK].[Id], [l].[Id] + +SELECT [l0].[Id], [l0].[Level2_Optional_Id], [l0].[Level2_Required_Id], [l0].[Name], [l0].[OneToMany_Optional_InverseId], [l0].[OneToMany_Optional_Self_InverseId], [l0].[OneToMany_Required_InverseId], [l0].[OneToMany_Required_Self_InverseId], [l0].[OneToOne_Optional_PK_InverseId], [l0].[OneToOne_Optional_SelfId] +FROM [Level3] AS [l0] +INNER JOIN ( + SELECT DISTINCT [e].[Id], [e.OneToOne_Required_FK].[Id] AS [Id0], [l].[Id] AS [Id1] + FROM [Level1] AS [e] + INNER JOIN [Level2] AS [e.OneToOne_Required_FK] ON [e].[Id] = [e.OneToOne_Required_FK].[Level1_Required_Id] + LEFT JOIN [Level3] AS [e.OneToOne_Required_FK.OneToOne_Optional_PK] ON [e.OneToOne_Required_FK].[Id] = [e.OneToOne_Required_FK.OneToOne_Optional_PK].[OneToOne_Optional_PK_InverseId] + LEFT JOIN [Level2] AS [l] ON [l].[Level1_Required_Id] = [e].[Id] +) AS [l1] ON [l0].[OneToMany_Optional_InverseId] = [l1].[Id1] +ORDER BY [l1].[Id], [l1].[Id0], [l1].[Id1]", + Sql); + } + public override void Correlated_subquery_doesnt_project_unnecessary_columns_in_top_level() { base.Correlated_subquery_doesnt_project_unnecessary_columns_in_top_level(); diff --git a/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests.csproj b/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests.csproj index 745fa56087f..43a320f784a 100644 --- a/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests.csproj +++ b/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests.csproj @@ -41,6 +41,7 @@ + diff --git a/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/QueryNavigationsSqlServerTest.cs b/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/QueryNavigationsSqlServerTest.cs index 3fb3425b1d7..e4a1176923c 100644 --- a/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/QueryNavigationsSqlServerTest.cs +++ b/test/Microsoft.EntityFrameworkCore.SqlServer.FunctionalTests/QueryNavigationsSqlServerTest.cs @@ -195,6 +195,21 @@ FROM [Orders] AS [o] Sql); } + public override void Include_with_multiple_optional_navigations() + { + base.Include_with_multiple_optional_navigations(); + + Assert.Equal( + @"SELECT [od].[OrderID], [od].[ProductID], [od].[Discount], [od].[Quantity], [od].[UnitPrice], [od.Order].[OrderID], [od.Order].[CustomerID], [od.Order].[EmployeeID], [od.Order].[OrderDate], [od.Order.Customer].[CustomerID], [od.Order.Customer].[Address], [od.Order.Customer].[City], [od.Order.Customer].[CompanyName], [od.Order.Customer].[ContactName], [od.Order.Customer].[ContactTitle], [od.Order.Customer].[Country], [od.Order.Customer].[Fax], [od.Order.Customer].[Phone], [od.Order.Customer].[PostalCode], [od.Order.Customer].[Region], [o].[OrderID], [o].[CustomerID], [o].[EmployeeID], [o].[OrderDate], [c].[CustomerID], [c].[Address], [c].[City], [c].[CompanyName], [c].[ContactName], [c].[ContactTitle], [c].[Country], [c].[Fax], [c].[Phone], [c].[PostalCode], [c].[Region] +FROM [Order Details] AS [od] +INNER JOIN [Orders] AS [od.Order] ON [od].[OrderID] = [od.Order].[OrderID] +LEFT JOIN [Customers] AS [od.Order.Customer] ON [od.Order].[CustomerID] = [od.Order.Customer].[CustomerID] +INNER JOIN [Orders] AS [o] ON [od].[OrderID] = [o].[OrderID] +LEFT JOIN [Customers] AS [c] ON [o].[CustomerID] = [c].[CustomerID] +ORDER BY [od.Order].[CustomerID]", + Sql); + } + public override void Select_Navigation() { base.Select_Navigation();