Skip to content

Commit 7fe09a9

Browse files
committed
CSHARP-3713: SelectMany doesn't support DefaultIfEmpty().
1 parent ffab9c6 commit 7fe09a9

File tree

4 files changed

+167
-1
lines changed

4 files changed

+167
-1
lines changed

src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodCallExpressionToAggregationExpressionTranslator.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ public static AggregationExpression Translate(TranslationContext context, Method
3333
case "CompareTo": return CompareToMethodToAggregationExpressionTranslator.Translate(context, expression);
3434
case "Concat": return ConcatMethodToAggregationExpressionTranslator.Translate(context, expression);
3535
case "Contains": return ContainsMethodToAggregationExpressionTranslator.Translate(context, expression);
36+
case "DefaultIfEmpty": return DefaultIfEmptyMethodToAggregationExpressionTranslator.Translate(context, expression);
3637
case "Distinct": return DistinctMethodToAggregationExpressionTranslator.Translate(context, expression);
3738
case "ElementAt": return ElementAtMethodToAggregationExpressionTranslator.Translate(context, expression);
3839
case "Equals": return EqualsMethodToAggregationExpressionTranslator.Translate(context, expression);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System;
17+
using System.Linq.Expressions;
18+
using MongoDB.Bson;
19+
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions;
20+
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
21+
using MongoDB.Driver.Linq.Linq3Implementation.Reflection;
22+
23+
namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators
24+
{
25+
internal static class DefaultIfEmptyMethodToAggregationExpressionTranslator
26+
{
27+
public static AggregationExpression Translate(TranslationContext context, MethodCallExpression expression)
28+
{
29+
var method = expression.Method;
30+
var arguments = expression.Arguments;
31+
32+
if (method.IsOneOf(EnumerableMethod.DefaultIfEmpty, EnumerableMethod.DefaultIfEmptyWithDefaultValue))
33+
{
34+
var sourceExpression = arguments[0];
35+
var sourceTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, sourceExpression);
36+
var sourceVar = AstExpression.Var("source");
37+
var sourceVarBinding = AstExpression.VarBinding(sourceVar, sourceTranslation.Ast);
38+
AstExpression defaultValueAst;
39+
if (method.Is(EnumerableMethod.DefaultIfEmpty))
40+
{
41+
var sourceItemSerializer = ArraySerializerHelper.GetItemSerializer(sourceTranslation.Serializer);
42+
var defaultValue = Activator.CreateInstance(sourceItemSerializer.ValueType);
43+
var serializedDefaultValue = SerializationHelper.SerializeValue(sourceItemSerializer, defaultValue);
44+
defaultValueAst = AstExpression.Constant(new BsonArray { serializedDefaultValue });
45+
}
46+
else
47+
{
48+
var defaultValueExpression = arguments[1];
49+
var defaultValueTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, defaultValueExpression);
50+
defaultValueAst = AstExpression.ComputedArray(new[] { defaultValueTranslation.Ast });
51+
}
52+
var ast = AstExpression.Let(
53+
sourceVarBinding,
54+
AstExpression.Cond(
55+
AstExpression.Eq(AstExpression.Size(sourceVar), 0),
56+
defaultValueAst,
57+
sourceVar));
58+
return new AggregationExpression(expression, ast, sourceTranslation.Serializer);
59+
}
60+
61+
throw new ExpressionNotSupportedException(expression);
62+
}
63+
}
64+
}

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -135,8 +135,9 @@ private static AstPipeline TranslateSelectManyWithCollectionSelectorAndNonIdenti
135135
var collectionItemSerializer = ArraySerializerHelper.GetItemSerializer(collectionSelectorTranslation.Serializer);
136136

137137
var resultSelectorSourceParameterExpression = resultSelectorLambda.Parameters[0];
138+
var resultSelectorSourceAst = AstExpression.Var("ROOT", isCurrent: true);
139+
var resultSelectorSourceParameterSymbol = context.CreateSymbol(resultSelectorSourceParameterExpression, resultSelectorSourceAst, sourceSerializer, isCurrent: true);
138140
var resultSelectorCollectionItemParameterExpression = resultSelectorLambda.Parameters[1];
139-
var resultSelectorSourceParameterSymbol = context.CreateSymbol(resultSelectorSourceParameterExpression, sourceSerializer, isCurrent: true);
140141
var resultSelectorCollectionItemParameterSymbol = context.CreateSymbol(resultSelectorCollectionItemParameterExpression, collectionItemSerializer);
141142
var resultSelectorContext = context.WithSymbols(resultSelectorSourceParameterSymbol, resultSelectorCollectionItemParameterSymbol);
142143
var resultSelectorTranslation = ExpressionToAggregationExpressionTranslator.Translate(resultSelectorContext, resultSelectorLambda.Body);
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/* Copyright 2010-present MongoDB Inc.
2+
*
3+
* Licensed under the Apache License, Version 2.0 (the "License");
4+
* you may not use this file except in compliance with the License.
5+
* You may obtain a copy of the License at
6+
*
7+
* http://www.apache.org/licenses/LICENSE-2.0
8+
*
9+
* Unless required by applicable law or agreed to in writing, software
10+
* distributed under the License is distributed on an "AS IS" BASIS,
11+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
* See the License for the specific language governing permissions and
13+
* limitations under the License.
14+
*/
15+
16+
using System.Linq;
17+
using FluentAssertions;
18+
using Xunit;
19+
20+
namespace MongoDB.Driver.Tests.Linq.Linq3ImplementationTests.Jira
21+
{
22+
public class CSharp3713Tests
23+
{
24+
[Fact]
25+
public void DefaultIfEmpty_should_work()
26+
{
27+
var client = DriverTestConfiguration.Linq3Client;
28+
var database = client.GetDatabase(DriverTestConfiguration.DatabaseNamespace.DatabaseName);
29+
var collection = database.GetCollection<C>(DriverTestConfiguration.CollectionNamespace.CollectionName);
30+
var subject = collection.AsQueryable();
31+
32+
database.DropCollection(collection.CollectionNamespace.CollectionName);
33+
collection.InsertMany(new[] {
34+
new C { Id = 1, InnerArray = new A[0] },
35+
new C { Id = 2, InnerArray = new[] { new A { S = "abc" } } }
36+
});
37+
38+
var queryable = subject.SelectMany(outerObject => outerObject.InnerArray.DefaultIfEmpty(), (o, a) => new { o, a });
39+
40+
var stages = Linq3TestHelpers.Translate(collection, queryable);
41+
var expectedStages = new[]
42+
{
43+
"{ $project : { _v : { $map : { input : { $let : { vars : { source : '$InnerArray' }, in : { $cond : { if : { $eq : [{ $size : '$$source' }, 0] }, then : [{ S : null }], else : '$$source' } } } }, as : 'a', in : { o : '$$ROOT', a : '$$a' } } }, _id : 0 } }",
44+
"{ $unwind : '$_v' }"
45+
};
46+
Linq3TestHelpers.AssertStages(stages, expectedStages);
47+
48+
var result = queryable.ToList();
49+
result.Count.Should().Be(2);
50+
result[0].o.Id.Should().Be(1);
51+
result[0].a.S.Should().Be(null);
52+
result[1].o.Id.Should().Be(2);
53+
result[1].a.S.Should().Be("abc");
54+
}
55+
56+
[Fact]
57+
public void DefaultIfEmpty_with_explicit_default_should_work()
58+
{
59+
var client = DriverTestConfiguration.Linq3Client;
60+
var database = client.GetDatabase(DriverTestConfiguration.DatabaseNamespace.DatabaseName);
61+
var collection = database.GetCollection<C>(DriverTestConfiguration.CollectionNamespace.CollectionName);
62+
var subject = collection.AsQueryable();
63+
64+
database.DropCollection(collection.CollectionNamespace.CollectionName);
65+
collection.InsertMany(new[] {
66+
new C { Id = 1, InnerArray = new A[0] },
67+
new C { Id = 2, InnerArray = new[] { new A { S = "abc" } } }
68+
});
69+
70+
var defaultValue = new A { S = "default" };
71+
var queryable = subject.SelectMany(outerObject => outerObject.InnerArray.DefaultIfEmpty(defaultValue), (o, a) => new { o, a });
72+
73+
var stages = Linq3TestHelpers.Translate(collection, queryable);
74+
var expectedStages = new[]
75+
{
76+
"{ $project : { _v : { $map : { input : { $let : { vars : { source : '$InnerArray' }, in : { $cond : { if : { $eq : [{ $size : '$$source' }, 0] }, then : [{ S : 'default' }], else : '$$source' } } } }, as : 'a', in : { o : '$$ROOT', a : '$$a' } } }, _id : 0 } }",
77+
"{ $unwind : '$_v' }"
78+
};
79+
Linq3TestHelpers.AssertStages(stages, expectedStages);
80+
81+
var result = queryable.ToList();
82+
result.Count.Should().Be(2);
83+
result[0].o.Id.Should().Be(1);
84+
result[0].a.S.Should().Be("default");
85+
result[1].o.Id.Should().Be(2);
86+
result[1].a.S.Should().Be("abc");
87+
}
88+
89+
private class C
90+
{
91+
public int Id { get; set; }
92+
public A[] InnerArray { get; set; }
93+
}
94+
95+
private class A
96+
{
97+
public string S { get; set; }
98+
}
99+
}
100+
}

0 commit comments

Comments
 (0)