Skip to content

Commit 88a63ea

Browse files
committed
CSHARP-4781: DefaultIfEmpty using wrong default value.
1 parent cf65611 commit 88a63ea

File tree

6 files changed

+138
-17
lines changed

6 files changed

+138
-17
lines changed

src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToAggregationExpressionTranslators/MethodTranslators/DefaultIfEmptyMethodToAggregationExpressionTranslator.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,12 +13,12 @@
1313
* limitations under the License.
1414
*/
1515

16-
using System;
1716
using System.Linq.Expressions;
1817
using MongoDB.Bson;
1918
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions;
2019
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
2120
using MongoDB.Driver.Linq.Linq3Implementation.Reflection;
21+
using MongoDB.Driver.Support;
2222

2323
namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators
2424
{
@@ -39,7 +39,7 @@ public static AggregationExpression Translate(TranslationContext context, Method
3939
if (method.Is(EnumerableMethod.DefaultIfEmpty))
4040
{
4141
var sourceItemSerializer = ArraySerializerHelper.GetItemSerializer(sourceTranslation.Serializer);
42-
var defaultValue = Activator.CreateInstance(sourceItemSerializer.ValueType);
42+
var defaultValue = sourceItemSerializer.ValueType.GetDefaultValue();
4343
var serializedDefaultValue = SerializationHelper.SerializeValue(sourceItemSerializer, defaultValue);
4444
defaultValueAst = AstExpression.Constant(new BsonArray { serializedDefaultValue });
4545
}

src/MongoDB.Driver/Support/ReflectionExtensions.cs

Lines changed: 9 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,12 +25,10 @@ internal static class ReflectionExtensions
2525
{
2626
public static object GetDefaultValue(this Type type)
2727
{
28-
if (type.GetTypeInfo().IsValueType)
29-
{
30-
return Activator.CreateInstance(type);
31-
}
32-
33-
return null;
28+
var genericMethod = typeof(ReflectionExtensions)
29+
.GetMethod(nameof(GetDefaultValueGeneric), BindingFlags.NonPublic | BindingFlags.Static)
30+
.MakeGenericMethod(type);
31+
return genericMethod.Invoke(null, null);
3432
}
3533

3634
public static bool ImplementsInterface(this Type type, Type iface)
@@ -153,5 +151,10 @@ public static Type FindIEnumerable(this Type seqType)
153151

154152
return null;
155153
}
154+
155+
private static TValue GetDefaultValueGeneric<TValue>()
156+
{
157+
return default(TValue);
158+
}
156159
}
157160
}

tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp3713Tests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,15 +40,15 @@ public void DefaultIfEmpty_should_work()
4040
var stages = Linq3TestHelpers.Translate(collection, queryable);
4141
var expectedStages = new[]
4242
{
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 } }",
43+
"{ $project : { _v : { $map : { input : { $let : { vars : { source : '$InnerArray' }, in : { $cond : { if : { $eq : [{ $size : '$$source' }, 0] }, then : [null], else : '$$source' } } } }, as : 'a', in : { o : '$$ROOT', a : '$$a' } } }, _id : 0 } }",
4444
"{ $unwind : '$_v' }"
4545
};
4646
Linq3TestHelpers.AssertStages(stages, expectedStages);
4747

4848
var result = queryable.ToList();
4949
result.Count.Should().Be(2);
5050
result[0].o.Id.Should().Be(1);
51-
result[0].a.S.Should().Be(null);
51+
result[0].a.Should().Be(null);
5252
result[1].o.Id.Should().Be(2);
5353
result[1].a.S.Should().Be("abc");
5454
}

tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4048Tests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -737,7 +737,7 @@ public void IGrouping_DefaultIfEmpty_of_root_should_work()
737737
var expectedStages = new[]
738738
{
739739
"{ $group : { _id : '$_id', _elements : { $push : '$$ROOT' } } }",
740-
"{ $project : { _id : '$_id', Result : { $let : { vars : { source : '$_elements' }, in : { $cond : { if : { $eq : [{ $size : '$$source' }, 0] }, then : [{ _id : 0, X : 0 }], else : '$$source' } } } } } }",
740+
"{ $project : { _id : '$_id', Result : { $let : { vars : { source : '$_elements' }, in : { $cond : { if : { $eq : [{ $size : '$$source' }, 0] }, then : [null], else : '$$source' } } } } } }",
741741
"{ $sort : { _id : 1 } }"
742742
};
743743
AssertStages(stages, expectedStages);

tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Jira/CSharp4589Tests.cs

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -113,7 +113,7 @@ public void Multiple_GroupJoins_using_method_syntax_should_work(
113113
vars : { source : '$teamAllianceMappingsListTemp' },
114114
in : { $cond : {
115115
if : { $eq : [{ $size : '$$source' }, 0] },
116-
then : [{ _id : 0, TeamId : 0, AllianceTeamId : 0 }],
116+
then : [null],
117117
else : '$$source' } } } },
118118
as : 'allianceMapping',
119119
in : { TransparentIdentifier0 : '$$ROOT', allianceMapping : '$$allianceMapping' } } },
@@ -128,7 +128,7 @@ public void Multiple_GroupJoins_using_method_syntax_should_work(
128128
vars : { source : '$organizationAdminsListTemp' },
129129
in : { $cond : {
130130
if : { $eq : [{ $size : '$$source' }, 0] },
131-
then : [{ _id : 0, OrganizationId : 0, UserId : 0 }],
131+
then : [null],
132132
else : '$$source' } } } },
133133
as : 'organizationAdmin',
134134
in : { TransparentIdentifier2 : '$$ROOT', organizationAdmin : '$$organizationAdmin' } } },
@@ -143,7 +143,7 @@ public void Multiple_GroupJoins_using_method_syntax_should_work(
143143
vars : { source : '$usersListTemp' },
144144
in : { $cond : {
145145
if : { $eq : [{ $size : '$$source' }, 0] },
146-
then : [{ _id : 0, UserId : 0, ProfileImage : null }],
146+
then : [null],
147147
else : '$$source' } } } },
148148
as : 'organizationUser',
149149
in : {
@@ -217,7 +217,7 @@ from organizationUser in usersListTemp.DefaultIfEmpty()
217217
vars : { source : '$teamAllianceMappingsListTemp' },
218218
in : { $cond : {
219219
if : { $eq : [{ $size : '$$source' }, 0] },
220-
then : [{ _id : 0, TeamId : 0, AllianceTeamId : 0 }],
220+
then : [null],
221221
else : '$$source' } } } },
222222
as : 'allianceMapping',
223223
in : { '<>h__TransparentIdentifier0' : '$$ROOT', allianceMapping : '$$allianceMapping' } } },
@@ -232,7 +232,7 @@ from organizationUser in usersListTemp.DefaultIfEmpty()
232232
vars : { source : '$organizationAdminsListTemp' },
233233
in : { $cond : {
234234
if : { $eq : [{ $size : '$$source' }, 0] },
235-
then : [{ _id : 0, OrganizationId : 0, UserId : 0 }],
235+
then : [null],
236236
else : '$$source' } } } },
237237
as : 'organizationAdmin',
238238
in : { '<>h__TransparentIdentifier2' : '$$ROOT', organizationAdmin : '$$organizationAdmin' } } },
@@ -247,7 +247,7 @@ from organizationUser in usersListTemp.DefaultIfEmpty()
247247
vars : { source : '$usersListTemp' },
248248
in : { $cond : {
249249
if : { $eq : [{ $size : '$$source' }, 0] },
250-
then : [{ _id : 0, UserId : 0, ProfileImage : null }],
250+
then : [null],
251251
else : '$$source' } } } },
252252
as : 'organizationUser',
253253
in : {
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
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;
18+
using FluentAssertions;
19+
using MongoDB.Driver.Linq;
20+
using MongoDB.TestHelpers.XunitExtensions;
21+
using Xunit;
22+
23+
namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Jira
24+
{
25+
public class CSharp4781Tests : Linq3IntegrationTest
26+
{
27+
[Fact]
28+
public void Enumerable_DefaultIfEmpty_should_return_default_int()
29+
{
30+
var c = new C { Id = 1, IntArray = new int[0] };
31+
32+
var result = c.IntArray.DefaultIfEmpty();
33+
34+
result.Should().Equal(new int[] { 0 });
35+
}
36+
37+
[Fact]
38+
public void Enumerable_DefaultIfEmpty_should_return_default_string()
39+
{
40+
var c = new C { Id = 1, StringArray = new string[0] };
41+
42+
var result = c.StringArray.DefaultIfEmpty();
43+
44+
result.Should().Equal(new string[] { null });
45+
}
46+
47+
[Theory]
48+
[ParameterAttributeData]
49+
public void Select_with_DefaultIfEmpty_should_return_default_int(
50+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
51+
{
52+
var collection = GetCollection(linqProvider);
53+
54+
var queryable = collection.AsQueryable()
55+
.Select(x => x.IntArray.DefaultIfEmpty());
56+
57+
if (linqProvider == LinqProvider.V2)
58+
{
59+
var exception = Record.Exception(() => Translate(collection, queryable));
60+
exception.Should().BeOfType<NotSupportedException>();
61+
}
62+
else
63+
{
64+
var stages = Translate(collection, queryable);
65+
AssertStages(stages, "{ $project : { _v : { $let : { vars : { source : '$IntArray' }, in : { $cond : { if : { $eq : [{ $size : '$$source' }, 0] }, then : [0], else : '$$source' } } } }, _id : 0 } }");
66+
67+
var results = queryable.ToArray();
68+
results.Should().HaveCount(2);
69+
results[0].Should().Equal(new int[] { 1 });
70+
results[1].Should().Equal(new int[] { 0 });
71+
}
72+
}
73+
74+
[Theory]
75+
[ParameterAttributeData]
76+
public void Select_with_DefaultIfEmpty_should_return_default_string(
77+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
78+
{
79+
var collection = GetCollection(linqProvider);
80+
81+
var queryable = collection.AsQueryable()
82+
.Select(x => x.StringArray.DefaultIfEmpty());
83+
84+
if (linqProvider == LinqProvider.V2)
85+
{
86+
var exception = Record.Exception(() => Translate(collection, queryable));
87+
exception.Should().BeOfType<NotSupportedException>();
88+
}
89+
else
90+
{
91+
var stages = Translate(collection, queryable);
92+
AssertStages(stages, "{ $project : { _v : { $let : { vars : { source : '$StringArray' }, in : { $cond : { if : { $eq : [{ $size : '$$source' }, 0] }, then : [null], else : '$$source' } } } }, _id : 0 } }");
93+
94+
var results = queryable.ToArray();
95+
results.Should().HaveCount(2);
96+
results[0].Should().Equal(new string[] { "abc" });
97+
results[1].Should().Equal(new string[] { null });
98+
}
99+
}
100+
101+
private IMongoCollection<C> GetCollection(LinqProvider linqProvider)
102+
{
103+
var collection = GetCollection<C>("test", linqProvider);
104+
CreateCollection(
105+
collection,
106+
new C { Id = 1, IntArray = new int[] { 1 }, StringArray = new string[] { "abc" } },
107+
new C { Id = 2, IntArray = new int[] { }, StringArray = new string[] { } });
108+
return collection;
109+
}
110+
111+
private class C
112+
{
113+
public int Id { get; set; }
114+
public int[] IntArray { get; set; }
115+
public string[] StringArray { get; set; } // note that string is suitable for this test since it does not have a default constructor
116+
}
117+
}
118+
}

0 commit comments

Comments
 (0)