Skip to content

Commit

Permalink
Implement support for the new .NET LeftJoin operator
Browse files Browse the repository at this point in the history
Closes #12793
  • Loading branch information
roji committed Jan 13, 2025
1 parent 271ec27 commit f6df909
Show file tree
Hide file tree
Showing 17 changed files with 229 additions and 108 deletions.
4 changes: 2 additions & 2 deletions global.json
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
{
"sdk": {
"version": "10.0.100-alpha.1.24573.1",
"version": "10.0.100-alpha.1.25059.31",
"allowPrerelease": true,
"rollForward": "latestMajor"
},
"tools": {
"dotnet": "10.0.100-alpha.1.24573.1",
"dotnet": "10.0.100-alpha.1.25059.31",
"runtimes": {
"dotnet": [
"$(MicrosoftNETCoreAppRuntimewinx64Version)"
Expand Down
32 changes: 0 additions & 32 deletions src/EFCore/Extensions/Internal/QueryableExtensions.cs

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -249,7 +249,7 @@ protected Expression ExpandSkipNavigation(
secondaryExpansion = Expression.Call(
(innerJoin
? QueryableMethods.Join
: QueryableExtensions.LeftJoinMethodInfo).MakeGenericMethod(
: QueryableMethods.LeftJoin).MakeGenericMethod(
sourceElementType, innerSourceElementType,
outerKeySelector.ReturnType,
resultSelector.ReturnType),
Expand Down Expand Up @@ -462,7 +462,7 @@ outerKey is NewArrayExpression newArrayExpression
Expression.Call(
(innerJoin
? QueryableMethods.Join
: QueryableExtensions.LeftJoinMethodInfo).MakeGenericMethod(
: QueryableMethods.LeftJoin).MakeGenericMethod(
source.SourceElementType,
innerSource.SourceElementType,
outerKeySelector.ReturnType,
Expand Down Expand Up @@ -794,7 +794,7 @@ private Expression ExpandIncludesHelper(Expression root, EntityReference entityR
&& joinMethodCallExpression.Method.GetGenericMethodDefinition()
== (skipNavigation.Inverse.ForeignKey.IsRequired
? QueryableMethods.Join
: QueryableExtensions.LeftJoinMethodInfo)
: QueryableMethods.LeftJoin)
&& joinMethodCallExpression.Arguments[4] is UnaryExpression
{
NodeType: ExpressionType.Quote,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,9 +319,7 @@ protected override Expression VisitMember(MemberExpression memberExpression)
protected override Expression VisitMethodCall(MethodCallExpression methodCallExpression)
{
var method = methodCallExpression.Method;
if (method.DeclaringType == typeof(Queryable)
|| method.DeclaringType == typeof(QueryableExtensions)
|| method.DeclaringType == typeof(EntityFrameworkQueryableExtensions))
if (method.DeclaringType == typeof(Queryable) || method.DeclaringType == typeof(EntityFrameworkQueryableExtensions))
{
var genericMethod = method.IsGenericMethod ? method.GetGenericMethodDefinition() : null;
// First argument is source
Expand Down Expand Up @@ -474,8 +472,8 @@ when QueryableMethods.IsSumWithSelector(method):
goto default;
}

case nameof(QueryableExtensions.LeftJoin)
when genericMethod == QueryableExtensions.LeftJoinMethodInfo:
case nameof(Queryable.LeftJoin)
when genericMethod == QueryableMethods.LeftJoin:
{
var secondArgument = Visit(methodCallExpression.Arguments[1]);
secondArgument = UnwrapCollectionMaterialization(secondArgument);
Expand Down Expand Up @@ -1312,7 +1310,7 @@ private NavigationExpansionExpression ProcessLeftJoin(
innerSource.CurrentParameter);

var source = Expression.Call(
QueryableExtensions.LeftJoinMethodInfo.MakeGenericMethod(
QueryableMethods.LeftJoin.MakeGenericMethod(
outerSource.SourceElementType, innerSource.SourceElementType, outerKeySelector.ReturnType,
newResultSelector.ReturnType),
outerSource.Source,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -626,7 +626,7 @@ when selectManyMethod.GetGenericMethodDefinition() == QueryableMethods.SelectMan
genericArguments[^1] = resultSelector.ReturnType;

return Expression.Call(
(defaultIfEmpty ? QueryableExtensions.LeftJoinMethodInfo : QueryableMethods.Join).MakeGenericMethod(
(defaultIfEmpty ? QueryableMethods.LeftJoin : QueryableMethods.Join).MakeGenericMethod(
genericArguments),
outer, inner, outerKeySelector, innerKeySelector, resultSelector);
}
Expand Down Expand Up @@ -726,7 +726,7 @@ when selectManyMethod.GetGenericMethodDefinition() == QueryableMethods.SelectMan
genericArguments[^1] = resultSelector.ReturnType;

return Expression.Call(
(defaultIfEmpty ? QueryableExtensions.LeftJoinMethodInfo : QueryableMethods.Join).MakeGenericMethod(
(defaultIfEmpty ? QueryableMethods.LeftJoin : QueryableMethods.Join).MakeGenericMethod(
genericArguments),
outer, inner, outerKeySelector, innerKeySelector, resultSelector);
}
Expand Down
1 change: 0 additions & 1 deletion src/EFCore/Query/QueryRootProcessor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
var method = methodCallExpression.Method;
if (method.DeclaringType != typeof(Queryable)
&& method.DeclaringType != typeof(Enumerable)
&& method.DeclaringType != typeof(QueryableExtensions)
&& method.DeclaringType != typeof(EntityFrameworkQueryableExtensions))
{
return base.VisitMethodCall(methodCallExpression);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -199,8 +199,7 @@ protected override Expression VisitMethodCall(MethodCallExpression methodCallExp
}
}

if (method.DeclaringType == typeof(Queryable)
|| method.DeclaringType == typeof(QueryableExtensions))
if (method.DeclaringType == typeof(Queryable))
{
var source = Visit(methodCallExpression.Arguments[0]);
if (source is ShapedQueryExpression shapedQueryExpression)
Expand Down Expand Up @@ -394,8 +393,8 @@ when QueryableMethods.IsAverageWithSelector(method):
break;
}

case nameof(QueryableExtensions.LeftJoin)
when genericMethod == QueryableExtensions.LeftJoinMethodInfo:
case nameof(Queryable.LeftJoin)
when genericMethod == QueryableMethods.LeftJoin:
{
if (Visit(methodCallExpression.Arguments[1]) is ShapedQueryExpression innerShapedQueryExpression)
{
Expand Down
18 changes: 18 additions & 0 deletions src/EFCore/Query/QueryableMethods.cs
Original file line number Diff line number Diff line change
Expand Up @@ -203,6 +203,13 @@ public static class QueryableMethods
/// </summary>
public static MethodInfo LastOrDefaultWithPredicate { get; }

/// <summary>
/// The <see cref="MethodInfo" /> for
/// <see
/// cref="Queryable.LeftJoin{TOuter,TInner,TKey,TResult}(IQueryable{TOuter},IEnumerable{TInner},Expression{Func{TOuter,TKey}},Expression{Func{TInner,TKey}},Expression{Func{TOuter,TInner,TResult}})" />
/// </summary>
public static MethodInfo LeftJoin { get; }

/// <summary>
/// The <see cref="MethodInfo" /> for <see cref="Queryable.LongCount{TSource}(IQueryable{TSource})" />
/// </summary>
Expand Down Expand Up @@ -632,6 +639,17 @@ static QueryableMethods()
typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(types[0], typeof(bool)))
]);

LeftJoin = GetMethod(
nameof(Queryable.LeftJoin), 4,
types =>
[
typeof(IQueryable<>).MakeGenericType(types[0]),
typeof(IEnumerable<>).MakeGenericType(types[1]),
typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(types[0], types[2])),
typeof(Expression<>).MakeGenericType(typeof(Func<,>).MakeGenericType(types[1], types[2])),
typeof(Expression<>).MakeGenericType(typeof(Func<,,>).MakeGenericType(types[0], types[1], types[3]))
]);

LongCountWithoutPredicate = GetMethod(
nameof(Queryable.LongCount), 1,
types => [typeof(IQueryable<>).MakeGenericType(types[0])]);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -328,7 +328,20 @@ on od.OrderID equals o.OrderID

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Delete_with_left_join(bool async)
public virtual Task Delete_with_LeftJoin(bool async)
=> AssertDelete(
async,
ss => ss.Set<OrderDetail>().Where(e => e.OrderID < 10276)
.LeftJoin(
ss.Set<Order>().Where(o => o.OrderID < 10300).OrderBy(e => e.OrderID).Skip(0).Take(100),
od => od.OrderID,
o => o.OrderID,
(od, o) => od),
rowsAffectedCount: 74);

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Delete_with_LeftJoin_via_flattened_GroupJoin(bool async)
=> AssertDelete(
async,
ss => from od in ss.Set<OrderDetail>().Where(e => e.OrderID < 10276)
Expand Down Expand Up @@ -820,7 +833,24 @@ on c.CustomerID equals o.CustomerID

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Update_with_left_join_set_constant(bool async)
public virtual Task Update_with_LeftJoin(bool async)
=> AssertUpdate(
async,
ss => ss
.Set<Customer>().Where(c => c.CustomerID.StartsWith("F"))
.LeftJoin(
ss.Set<Order>().Where(o => o.OrderID < 10300),
c => c.CustomerID,
o => o.CustomerID,
(c, o) => new { c, o }),
e => e.c,
s => s.SetProperty(c => c.c.ContactName, "Updated"),
rowsAffectedCount: 8,
(b, a) => Assert.All(a, c => Assert.Equal("Updated", c.ContactName)));

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task Update_with_LeftJoin_via_flattened_GroupJoin(bool async)
=> AssertUpdate(
async,
ss => from c in ss.Set<Customer>().Where(c => c.CustomerID.StartsWith("F"))
Expand Down
36 changes: 19 additions & 17 deletions test/EFCore.Specification.Tests/CustomConvertersTestBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -435,23 +435,25 @@ public virtual async Task Value_conversion_is_appropriately_used_for_left_join_c
{
using var context = CreateContext();
var blogId = 1;
var query = await (from b in context.Set<Blog>()
join p in context.Set<Post>()
on new
{
BlogId = (int?)b.BlogId,
b.IsVisible,
AnotherId = b.BlogId
}
equals new
{
p.BlogId,
IsVisible = true,
AnotherId = blogId
} into g
from p in g.DefaultIfEmpty()
where b.IsVisible
select b.Url).ToListAsync();
var query = await context.Set<Blog>()
.LeftJoin(
context.Set<Post>(),
b => new
{
BlogId = (int?)b.BlogId,
b.IsVisible,
AnotherId = b.BlogId
},
p => new
{
p.BlogId,
IsVisible = true,
AnotherId = blogId
},
(b, p) => b)
.Where(b => b.IsVisible)
.Select(b => b.Url)
.ToListAsync();

var result = Assert.Single(query);
Assert.Equal("http://blog.com", result);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,25 +64,25 @@ public class Product

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual async Task Left_join_with_missing_key_values_on_both_sides(bool async)
public virtual async Task LeftJoin_with_missing_key_values_on_both_sides(bool async)
{
var contextFactory = await InitializeAsync<Context6901>();
using var context = contextFactory.CreateContext();

var customers
= from customer in context.Customers
join postcode in context.Postcodes
on customer.PostcodeID equals postcode.PostcodeID into custPCTmp
from custPC in custPCTmp.DefaultIfEmpty()
select new
{
customer.CustomerID,
customer.CustomerName,
TownName = custPC == null ? string.Empty : custPC.TownName,
PostcodeValue = custPC == null ? string.Empty : custPC.PostcodeValue
};

var results = customers.ToList();
var results
= context.Customers
.LeftJoin(
context.Postcodes,
c => c.PostcodeID,
p => p.PostcodeID,
(c, p) => new
{
c.CustomerID,
c.CustomerName,
TownName = p == null ? string.Empty : p.TownName,
PostcodeValue = p == null ? string.Empty : p.PostcodeValue
})
.ToList();

Assert.Equal(5, results.Count);
Assert.True(results[3].CustomerName != results[4].CustomerName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -860,10 +860,9 @@ public virtual Task Entity_with_complex_type_with_group_by_and_first(bool async)
public virtual Task Projecting_property_of_complex_type_using_left_join_with_pushdown(bool async)
=> AssertQuery(
async,
ss => from cg in ss.Set<CustomerGroup>()
join c in ss.Set<Customer>().Where(x => x.Id > 5) on cg.Id equals c.Id into grouping
from c in grouping.DefaultIfEmpty()
select c == null ? null : (int?)c.BillingAddress.ZipCode);
ss => ss.Set<CustomerGroup>()
.LeftJoin(ss.Set<Customer>().Where(x => x.Id > 5), cg => cg.Id, c => c.Id, (cg, c) => c)
.Select(c => c == null ? null : (int?)c.BillingAddress.ZipCode));

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
Expand All @@ -880,10 +879,12 @@ public virtual Task Projecting_complex_from_optional_navigation_using_conditiona
public virtual Task Project_entity_with_complex_type_pushdown_and_then_left_join(bool async)
=> AssertQuery(
async,
ss => from c1 in ss.Set<Customer>().OrderBy(x => x.Id).Take(20).Distinct()
join c2 in ss.Set<Customer>().OrderByDescending(x => x.Id).Take(30).Distinct() on c1.Id equals c2.Id into grouping
from c2 in grouping.DefaultIfEmpty()
select new { Zip1 = c1.BillingAddress.ZipCode, Zip2 = c2.ShippingAddress.ZipCode });
ss => ss.Set<Customer>().OrderBy(x => x.Id).Take(20).Distinct()
.LeftJoin(
ss.Set<Customer>().OrderByDescending(x => x.Id).Take(30).Distinct(),
c1 => c1.Id,
c2 => c2.Id,
(c1, c2) => new { Zip1 = c1.BillingAddress.ZipCode, Zip2 = c2 == null ? (int?)null : c2.ShippingAddress.ZipCode }));

protected DbContext CreateContext()
=> Fixture.CreateContext();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,12 +59,7 @@ public virtual Task Join_select_many(bool async)
ss => from c in ss.Set<Customer>().Where(c => c.CustomerID.StartsWith("F"))
join o in ss.Set<Order>() on c.CustomerID equals o.CustomerID
from e in ss.Set<Employee>()
select new
{
c,
o,
e
},
select new { c, o, e },
e => (e.c.CustomerID, e.o.OrderID, e.e.EmployeeID));

[ConditionalTheory]
Expand Down Expand Up @@ -297,6 +292,19 @@ public virtual Task Join_same_collection_force_alias_uniquefication(bool async)
ss.Set<Order>(), o => o.CustomerID, i => i.CustomerID, (_, o) => new { _, o }),
e => (e._.OrderID, e.o.OrderID));

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task LeftJoin(bool async)
=> AssertQuery(
async,
ss => ss.Set<Customer>()
.LeftJoin(
ss.Set<Order>(),
c => c.CustomerID,
o => o.CustomerID,
(c, o) => new { c, o }),
e => (e.c.CustomerID, e.o?.OrderID));

[ConditionalTheory]
[MemberData(nameof(IsAsyncData))]
public virtual Task GroupJoin_customers_employees_shadow(bool async)
Expand Down
Loading

0 comments on commit f6df909

Please sign in to comment.