diff --git a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs index a191af0042c..04e01ded214 100644 --- a/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs +++ b/src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs @@ -27,6 +27,8 @@ public sealed partial class SelectExpression : TableExpressionBase { internal const string DiscriminatorColumnAlias = "Discriminator"; private static readonly IdentifierComparer IdentifierComparerInstance = new(); + private static readonly bool UseOldBehavior39220 = + AppContext.TryGetSwitch("Microsoft.EntityFrameworkCore.Issue39220", out var enabled) && enabled; private readonly List _projection = []; private readonly List _tables = []; @@ -3432,7 +3434,8 @@ static void GetPartitions(SelectExpression select, SqlExpression sqlExpression, foreach (var innerColumn in innerKeyColumns) { - if (!projectionColumns.Contains(innerColumn)) + if (!projectionColumns.Contains(innerColumn) + && (UseOldBehavior39220 || !projectionColumns.Contains(innerColumn.MakeNullable()))) { return null; } diff --git a/test/EFCore.Relational.Specification.Tests/Query/NorthwindGroupByQueryRelationalTestBase.cs b/test/EFCore.Relational.Specification.Tests/Query/NorthwindGroupByQueryRelationalTestBase.cs index b18107443c9..3a0c1b11207 100644 --- a/test/EFCore.Relational.Specification.Tests/Query/NorthwindGroupByQueryRelationalTestBase.cs +++ b/test/EFCore.Relational.Specification.Tests/Query/NorthwindGroupByQueryRelationalTestBase.cs @@ -1,7 +1,59 @@ // Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. +using Microsoft.EntityFrameworkCore.TestModels.Northwind; + namespace Microsoft.EntityFrameworkCore.Query; public abstract class NorthwindGroupByQueryRelationalTestBase(TFixture fixture) : NorthwindGroupByQueryTestBase(fixture) - where TFixture : NorthwindQueryFixtureBase, new(); + where TFixture : NorthwindQueryFixtureBase, new() +{ + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task SelectMany_DefaultIfEmpty_over_GroupBy(bool async) + => AssertQuery( + async, + ss => from p in ss.Set() + from t in ss.Set() + .Where(od => od.ProductID < 10) + .GroupBy(od => od.ProductID, od => od.Quantity) + .Select((IGrouping g) => new { g.Key, Sum = g.Sum(int (short q) => q) }) + .Where(t => t.Key == p.ProductID) + .DefaultIfEmpty() + select new { p.ProductID, Key = (int?)t!.Key, Sum = (int?)t.Sum ?? 0 }, + expectedQuery: ss => from p in ss.Set() + from t in ss.Set() + .Where(od => od.ProductID < 10) + .GroupBy(od => od.ProductID, od => od.Quantity) + .Select((IGrouping g) => new { g.Key, Sum = g.Sum(int (short q) => q) }) + .Where(t => t.Key == p.ProductID) + .DefaultIfEmpty() + select new { p.ProductID, Key = t == null ? (int?)null : t.Key, Sum = t == null ? 0 : t.Sum }, + elementSorter: e => e.ProductID); + + [Theory, MemberData(nameof(IsAsyncData))] + public virtual Task SelectMany_DefaultIfEmpty_over_Distinct(bool async) + => AssertQuery( + async, + ss => from p in ss.Set() + from t in ss.Set() + .Where(od => od.ProductID < 10) + .Select(od => new { Key = od.ProductID, od.Quantity }) + .Distinct() + .Where(t => t.Key == p.ProductID) + .DefaultIfEmpty() + select new { p.ProductID, Key = (int?)t!.Key, Quantity = (int?)t.Quantity ?? 0 }, + expectedQuery: ss => from p in ss.Set() + from t in ss.Set() + .Where(od => od.ProductID < 10) + .Select(od => new { Key = od.ProductID, od.Quantity }) + .Distinct() + .Where(t => t.Key == p.ProductID) + .DefaultIfEmpty() + select new + { + p.ProductID, + Key = t == null ? (int?)null : t.Key, + Quantity = t == null ? 0 : (int)t.Quantity + }, + elementSorter: e => (e.ProductID, e.Quantity)); +} diff --git a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs index 3d0c1a89f9e..99199f88ffb 100644 --- a/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs +++ b/test/EFCore.SqlServer.FunctionalTests/Query/NorthwindGroupByQuerySqlServerTest.cs @@ -19,6 +19,39 @@ public NorthwindGroupByQuerySqlServerTest( public virtual void Check_all_tests_overridden() => TestHelpers.AssertAllMethodsOverridden(GetType()); + public override async Task SelectMany_DefaultIfEmpty_over_GroupBy(bool async) + { + await base.SelectMany_DefaultIfEmpty_over_GroupBy(async); + + AssertSql( + """ +SELECT [p].[ProductID], [o0].[Key], ISNULL([o0].[Sum], 0) AS [Sum] +FROM [Products] AS [p] +LEFT JOIN ( + SELECT [o].[ProductID] AS [Key], ISNULL(SUM(CAST([o].[Quantity] AS int)), 0) AS [Sum], [o].[ProductID] + FROM [Order Details] AS [o] + WHERE [o].[ProductID] < 10 + GROUP BY [o].[ProductID] +) AS [o0] ON [p].[ProductID] = [o0].[ProductID] +"""); + } + + public override async Task SelectMany_DefaultIfEmpty_over_Distinct(bool async) + { + await base.SelectMany_DefaultIfEmpty_over_Distinct(async); + + AssertSql( + """ +SELECT [p].[ProductID], [o0].[Key], COALESCE([o0].[Quantity], CAST(0 AS smallint)) AS [Quantity] +FROM [Products] AS [p] +LEFT JOIN ( + SELECT DISTINCT [o].[ProductID] AS [Key], [o].[Quantity], 1 AS [marker], [o].[ProductID] + FROM [Order Details] AS [o] + WHERE [o].[ProductID] < 10 +) AS [o0] ON [p].[ProductID] = [o0].[ProductID] +"""); + } + public override async Task GroupBy_ValueTuple_projection_joined_on_tuple_member(bool async) { await base.GroupBy_ValueTuple_projection_joined_on_tuple_member(async); diff --git a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindGroupByQuerySqliteTest.cs b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindGroupByQuerySqliteTest.cs index aef1772eecd..53cbff96260 100644 --- a/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindGroupByQuerySqliteTest.cs +++ b/test/EFCore.Sqlite.FunctionalTests/Query/NorthwindGroupByQuerySqliteTest.cs @@ -15,6 +15,39 @@ public NorthwindGroupByQuerySqliteTest(NorthwindQuerySqliteFixture AssertApplyNotSupported(() => base.Select_uncorrelated_collection_with_groupby_multiple_collections_work(async));