Skip to content

Commit 92cf9e7

Browse files
committed
CSHARP-6017: Support LINQ LeftJoin for joins and includes
1 parent 37a88b2 commit 92cf9e7

7 files changed

Lines changed: 527 additions & 437 deletions

File tree

src/MongoDB.Driver/Linq/LeftJoinResult.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* Copyright 2010-present MongoDB Inc.
1+
/* Copyright 2010-present MongoDB Inc.
22
*
33
* Licensed under the Apache License, Version 2.0 (the "License");
44
* you may not use this file except in compliance with the License.
@@ -20,7 +20,7 @@ namespace MongoDB.Driver.Linq
2020
/// </summary>
2121
/// <typeparam name="TOuter">The type of the outer documents.</typeparam>
2222
/// <typeparam name="TInner">The type of the inner documents.</typeparam>
23-
public struct LeftJoinResult<TOuter, TInner>
23+
public struct LeftJoinResult<TOuter, TInner> where TInner : class
2424
{
2525
/// <summary>
2626
/// The outer document.

src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToPipelineTranslators/JoinMethodToPipelineTranslator.cs

Lines changed: 26 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
/* Copyright 2010-present MongoDB Inc.
1+
/* Copyright 2010-present MongoDB Inc.
22
*
33
* Licensed under the Apache License, Version 2.0 (the "License");
44
* you may not use this file except in compliance with the License.
@@ -13,8 +13,6 @@
1313
* limitations under the License.
1414
*/
1515

16-
using System.Collections.Generic;
17-
using System.Linq;
1816
using System.Linq.Expressions;
1917
using MongoDB.Bson.Serialization;
2018
using MongoDB.Driver.Linq.Linq3Implementation.Ast;
@@ -61,29 +59,36 @@ public static TranslatedPipeline Translate(TranslationContext context, MethodCal
6159
outerSerializer = pipeline.OutputSerializer;
6260
}
6361

64-
if (isLeftJoin)
65-
{
66-
ThrowIfReservedFieldNames(expression, outerSerializer);
67-
}
68-
6962
var wrapOuterStage = AstStage.Project(
7063
AstProject.Set("_outer", outerAst),
7164
AstProject.Exclude("_id"));
7265
var wrappedOuterSerializer = WrappedValueSerializer.Create("_outer", outerSerializer);
7366

74-
var (innerCollectionName, innerSerializer) = innerExpression.GetCollectionInfoFromQueryable(containerExpression: expression);
67+
string innerCollectionName;
68+
IBsonSerializer innerSerializer;
69+
AstPipeline innerFilterPipeline = null;
70+
71+
if (innerExpression is ConstantExpression)
72+
{
73+
(innerCollectionName, innerSerializer) = innerExpression.GetCollectionInfoFromQueryable(containerExpression: expression);
74+
}
75+
else
76+
{
77+
var rootInnerExpression = GetRootQueryableExpression(innerExpression);
78+
(innerCollectionName, innerSerializer) = rootInnerExpression.GetCollectionInfoFromQueryable(containerExpression: expression);
79+
var innerTranslation = ExpressionToPipelineTranslator.Translate(context, innerExpression);
80+
innerSerializer = innerTranslation.OutputSerializer;
81+
innerFilterPipeline = innerTranslation.Ast.Stages.Count > 0 ? innerTranslation.Ast : null;
82+
}
83+
7584
var localField = outerKeySelectorLambda.TranslateToDottedFieldName(context, wrappedOuterSerializer);
7685
var foreignField = innerKeySelectorLambda.TranslateToDottedFieldName(context, innerSerializer);
7786

78-
var lookupStage = AstStage.Lookup(
79-
from: innerCollectionName,
80-
localField,
81-
foreignField,
82-
@as: "_inner");
87+
var lookupStage = innerFilterPipeline != null
88+
? AstStage.Lookup(innerCollectionName, localField, foreignField, [], innerFilterPipeline, "_inner")
89+
: AstStage.Lookup(from: innerCollectionName, localField, foreignField, @as: "_inner");
8390

84-
var unwindStage = isLeftJoin
85-
? AstStage.Unwind("_inner", preserveNullAndEmptyArrays: true)
86-
: AstStage.Unwind("_inner");
91+
var unwindStage = AstStage.Unwind("_inner", preserveNullAndEmptyArrays: isLeftJoin ? true : null);
8792

8893
var outerParameter = resultSelectorLambda.Parameters[0];
8994
var outerField = AstExpression.GetField(AstExpression.RootVar, "_outer");
@@ -108,28 +113,11 @@ public static TranslatedPipeline Translate(TranslationContext context, MethodCal
108113
throw new ExpressionNotSupportedException(expression);
109114
}
110115

111-
private static readonly HashSet<string> __reservedFieldNames = new HashSet<string> { "_outer", "_inner" };
112-
113-
private static void ThrowIfReservedFieldNames(MethodCallExpression expression, IBsonSerializer serializer)
116+
private static Expression GetRootQueryableExpression(Expression expression)
114117
{
115-
if (serializer is not IBsonDocumentSerializer documentSerializer)
116-
return;
117-
118-
var conflicting = new List<string>();
119-
foreach (var member in serializer.ValueType.GetMembers())
120-
{
121-
if (documentSerializer.TryGetMemberSerializationInfo(member.Name, out var info) &&
122-
__reservedFieldNames.Contains(info.ElementName))
123-
{
124-
conflicting.Add(info.ElementName);
125-
}
126-
}
127-
128-
if (conflicting.Count > 0)
129-
{
130-
throw new ExpressionNotSupportedException(expression,
131-
because: $"the outer document type uses reserved field name(s) {string.Join(", ", conflicting.Select(n => $"'{n}'"))} which are used internally by LeftJoin");
132-
}
118+
while (expression is MethodCallExpression methodCall)
119+
expression = methodCall.Arguments[0];
120+
return expression;
133121
}
134122
}
135123
}

src/MongoDB.Driver/Linq/MongoQueryable.cs

Lines changed: 2 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -821,33 +821,6 @@ public static IQueryable<TResult> Join<TOuter, TInner, TKey, TResult>(this IQuer
821821
return Queryable.Join(outer, inner.AsQueryable(), outerKeySelector, innerKeySelector, resultSelector);
822822
}
823823

824-
/// <summary>
825-
/// Correlates the elements of two sequences based on matching keys, preserving all outer elements
826-
/// even when no matching inner element exists (left outer join semantics).
827-
/// </summary>
828-
/// <typeparam name="TOuter">The type of the elements of the first sequence.</typeparam>
829-
/// <typeparam name="TInner">The type of the elements of the second sequence.</typeparam>
830-
/// <typeparam name="TKey">The type of the keys returned by the key selector functions.</typeparam>
831-
/// <typeparam name="TResult">The type of the result elements.</typeparam>
832-
/// <param name="outer">The first sequence to join.</param>
833-
/// <param name="inner">The collection to join to the first sequence.</param>
834-
/// <param name="outerKeySelector">A function to extract the join key from each element of the first sequence.</param>
835-
/// <param name="innerKeySelector">A function to extract the join key from each element of the second sequence.</param>
836-
/// <param name="resultSelector">A function to create a result element from an outer element and a matching inner element (or null).</param>
837-
/// <returns>
838-
/// An <see cref="IQueryable{TResult}" /> that contains elements of type <typeparamref name="TResult" /> obtained by performing a left outer join on two sequences.
839-
/// </returns>
840-
public static IQueryable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(this IQueryable<TOuter> outer, IMongoCollection<TInner> inner, Expression<Func<TOuter, TKey>> outerKeySelector, Expression<Func<TInner, TKey>> innerKeySelector, Expression<Func<TOuter, TInner, TResult>> resultSelector)
841-
{
842-
Ensure.IsNotNull(outer, nameof(outer));
843-
Ensure.IsNotNull(inner, nameof(inner));
844-
Ensure.IsNotNull(outerKeySelector, nameof(outerKeySelector));
845-
Ensure.IsNotNull(innerKeySelector, nameof(innerKeySelector));
846-
Ensure.IsNotNull(resultSelector, nameof(resultSelector));
847-
848-
return LeftJoin(outer, inner.AsQueryable(), outerKeySelector, innerKeySelector, resultSelector);
849-
}
850-
851824
/// <summary>
852825
/// Correlates the elements of two sequences based on matching keys, preserving all outer elements
853826
/// even when no matching inner element exists (left outer join semantics).
@@ -860,7 +833,7 @@ public static IQueryable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(this I
860833
/// <param name="inner">The sequence to join to the first sequence.</param>
861834
/// <param name="outerKeySelector">A function to extract the join key from each element of the first sequence.</param>
862835
/// <param name="innerKeySelector">A function to extract the join key from each element of the second sequence.</param>
863-
/// <param name="resultSelector">A function to create a result element from an outer element and a matching inner element (or null).</param>
836+
/// <param name="resultSelector">A function to create a result element from an outer element and a matching inner element (or default if no match).</param>
864837
/// <returns>
865838
/// An <see cref="IQueryable{TResult}" /> that contains elements of type <typeparamref name="TResult" /> obtained by performing a left outer join on two sequences.
866839
/// </returns>
@@ -877,7 +850,7 @@ public static IQueryable<TResult> LeftJoin<TOuter, TInner, TKey, TResult>(this I
877850
null,
878851
GetMethodInfo(LeftJoin, outer, inner, outerKeySelector, innerKeySelector, resultSelector),
879852
outer.Expression,
880-
Expression.Constant(inner),
853+
inner.Expression,
881854
Expression.Quote(outerKeySelector),
882855
Expression.Quote(innerKeySelector),
883856
Expression.Quote(resultSelector)));

0 commit comments

Comments
 (0)