Skip to content

Commit defd3f9

Browse files
committed
Fix DefaultIfEmpty and nullability within SelectMany selector
Fixes #35950
1 parent 82d8bbf commit defd3f9

File tree

5 files changed

+76
-3
lines changed

5 files changed

+76
-3
lines changed

src/EFCore.Relational/Query/RelationalQueryableMethodTranslatingExpressionVisitor.cs

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
// Licensed to the .NET Foundation under one or more agreements.
22
// The .NET Foundation licenses this file to you under the MIT license.
33

4+
using System;
45
using System.Diagnostics.CodeAnalysis;
56
using Microsoft.EntityFrameworkCore.Internal;
67
using Microsoft.EntityFrameworkCore.Query.Internal;
@@ -231,6 +232,16 @@ [new FromSqlExpression(alias, sqlQueryRootExpression.Sql, sqlQueryRootExpression
231232
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
232233
{
233234
var method = methodCallExpression.Method;
235+
236+
if (method.DeclaringType == typeof(RelationalQueryableMethodTranslatingExpressionVisitor)
237+
&& method.IsGenericMethod
238+
&& method.GetGenericMethodDefinition() == _fakeDefaultIfEmptyMethodInfo.Value
239+
&& Visit(methodCallExpression.Arguments[0]) is ShapedQueryExpression source)
240+
{
241+
((SelectExpression)source.QueryExpression).MakeProjectionNullable();
242+
return source.UpdateShaperExpression(MarkShaperNullable(source.ShaperExpression));
243+
}
244+
234245
var translated = base.VisitMethodCall(methodCallExpression);
235246

236247
// For Contains over a collection parameter, if the provider hasn't implemented TranslateCollection (e.g. OPENJSON on SQL
@@ -1189,11 +1200,15 @@ protected override Expression VisitParameter(ParameterExpression parameterExpres
11891200

11901201
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
11911202
{
1192-
if (methodCallExpression.Method.IsGenericMethod
1193-
&& methodCallExpression.Method.GetGenericMethodDefinition() == QueryableMethods.DefaultIfEmptyWithoutArgument)
1203+
var method = methodCallExpression.Method;
1204+
if (method.IsGenericMethod
1205+
&& method.GetGenericMethodDefinition() == QueryableMethods.DefaultIfEmptyWithoutArgument)
11941206
{
11951207
_defaultIfEmpty = true;
1196-
return Visit(methodCallExpression.Arguments[0]);
1208+
1209+
return Expression.Call(
1210+
_fakeDefaultIfEmptyMethodInfo.Value.MakeGenericMethod(method.GetGenericArguments()[0]),
1211+
Visit(methodCallExpression.Arguments[0]));
11971212
}
11981213

11991214
return base.VisitMethodCall(methodCallExpression);
@@ -2202,6 +2217,13 @@ private ShapedQueryExpression CreateShapedQueryExpressionForValuesExpression(
22022217
return new ShapedQueryExpression(selectExpression, shaperExpression);
22032218
}
22042219

2220+
private static IQueryable<TSource?> FakeDefaultIfEmpty<TSource>(IQueryable<TSource> source)
2221+
=> throw new UnreachableException();
2222+
2223+
private static readonly Lazy<MethodInfo> _fakeDefaultIfEmptyMethodInfo = new(
2224+
() => typeof(RelationalQueryableMethodTranslatingExpressionVisitor)
2225+
.GetMethod(nameof(FakeDefaultIfEmpty), BindingFlags.NonPublic | BindingFlags.Static)!);
2226+
22052227
/// <summary>
22062228
/// This visitor has been obsoleted; Extend RelationalTypeMappingPostprocessor instead, and invoke it from
22072229
/// <see cref="RelationalQueryTranslationPostprocessor.ProcessTypeMappings" />.

src/EFCore.Relational/Query/SqlExpressions/SelectExpression.cs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2378,6 +2378,18 @@ [new ProjectionExpression(nullSqlExpression, "empty")],
23782378
_tables.Add(dummySelectExpression);
23792379
_tables.Add(joinTable);
23802380

2381+
MakeProjectionNullable();
2382+
}
2383+
2384+
/// <summary>
2385+
/// This is an internal API that supports the Entity Framework Core infrastructure and not subject to
2386+
/// the same compatibility standards as public APIs. It may be changed or removed without notice in
2387+
/// any release. You should only use it directly in your code with extreme caution and knowing that
2388+
/// doing so can result in application failures when updating to a new Entity Framework Core release.
2389+
/// </summary>
2390+
[EntityFrameworkInternal]
2391+
public void MakeProjectionNullable()
2392+
{
23812393
var projectionMapping = new Dictionary<ProjectionMember, Expression>();
23822394
foreach (var projection in _projectionMapping)
23832395
{

test/EFCore.Cosmos.FunctionalTests/Query/NorthwindMiscellaneousQueryCosmosTest.cs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3542,6 +3542,14 @@ public override async Task SelectMany_correlated_subquery_simple(bool async)
35423542
AssertSql();
35433543
}
35443544

3545+
public override async Task SelectMany_correlated_with_DefaultIfEmpty_and_value_type_in_selector(bool async)
3546+
{
3547+
// Cosmos client evaluation. Issue #17246.
3548+
await AssertTranslationFailed(() => base.SelectMany_correlated_with_DefaultIfEmpty_and_value_type_in_selector(async));
3549+
3550+
AssertSql();
3551+
}
3552+
35453553
public override async Task SelectMany_correlated_subquery_hard(bool async)
35463554
{
35473555
// Cosmos client evaluation. Issue #17246.

test/EFCore.Specification.Tests/Query/NorthwindMiscellaneousQueryTestBase.cs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2036,6 +2036,21 @@ from e in ss.Set<Employee>().Where(e => e.City == c.City)
20362036
select new { c, e },
20372037
assertOrder: true);
20382038

2039+
[ConditionalTheory] // #35950
2040+
[MemberData(nameof(IsAsyncData))]
2041+
public virtual Task SelectMany_correlated_with_DefaultIfEmpty_and_value_type_in_selector(bool async)
2042+
=> AssertQuery(
2043+
async,
2044+
ss =>
2045+
from c in ss.Set<Customer>()
2046+
from o in c.Orders
2047+
.Where(x => x.CustomerID == "NONEXISTENT")
2048+
.Take(1)
2049+
.OrderBy(x => true)
2050+
.Select(x => x.OrderID)
2051+
.DefaultIfEmpty()
2052+
select o);
2053+
20392054
[ConditionalTheory]
20402055
[MemberData(nameof(IsAsyncData))]
20412056
public virtual Task SelectMany_correlated_subquery_hard(bool async)

test/EFCore.SqlServer.FunctionalTests/Query/NorthwindMiscellaneousQuerySqlServerTest.cs

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6320,6 +6320,22 @@ FROM [Customers] AS [c]
63206320
""");
63216321
}
63226322

6323+
public override async Task SelectMany_correlated_with_DefaultIfEmpty_and_value_type_in_selector(bool async)
6324+
{
6325+
await base.SelectMany_correlated_with_DefaultIfEmpty_and_value_type_in_selector(async);
6326+
6327+
AssertSql(
6328+
"""
6329+
SELECT COALESCE([o0].[OrderID], 0)
6330+
FROM [Customers] AS [c]
6331+
OUTER APPLY (
6332+
SELECT TOP(1) [o].[OrderID]
6333+
FROM [Orders] AS [o]
6334+
WHERE [c].[CustomerID] = [o].[CustomerID] AND [o].[CustomerID] = N'NONEXISTENT'
6335+
) AS [o0]
6336+
""");
6337+
}
6338+
63236339
public override async Task Select_Property_when_shadow_unconstrained_generic_method(bool async)
63246340
{
63256341
await base.Select_Property_when_shadow_unconstrained_generic_method(async);

0 commit comments

Comments
 (0)