Skip to content

Commit 327b17c

Browse files
CSHARP-2557: Fix bug in '$elemMatch' generating if expression predicate contains DocumentsExpressions as an operand.
1 parent 4ac7934 commit 327b17c

File tree

4 files changed

+87
-10
lines changed

4 files changed

+87
-10
lines changed

src/MongoDB.Driver/Linq/Translators/PredicateTranslator.cs

Lines changed: 28 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,32 @@ private bool CanAnyBeRenderedWithoutElemMatch(Expression node)
217217
}
218218
}
219219

220+
private FilterDefinition<BsonDocument> ConvertElemMatchFilterToScalarElementMatchIfNeeded(FilterDefinition<BsonDocument> filter, IFieldExpression fieldExpression, Expression wherePredicate)
221+
{
222+
if ((!(fieldExpression.Serializer is IBsonDocumentSerializer)) || DoesExpressionUseDocumentItself(wherePredicate))
223+
{
224+
return new ScalarElementMatchFilterDefinition<BsonDocument>(filter);
225+
}
226+
else
227+
{
228+
return filter;
229+
}
230+
}
231+
232+
private bool DoesExpressionUseDocumentItself(Expression node)
233+
{
234+
// if a left operand is DocumentExpression, we need to generate a "$elemMatch" in a short form,
235+
// otherwise we will have $elemMatch queries with gaps similar to : "{ $elemMatch : { ' ' : {"
236+
if (node is BinaryExpression binaryExpression)
237+
{
238+
if (binaryExpression.Left is DocumentExpression)
239+
{
240+
return true;
241+
}
242+
}
243+
return false;
244+
}
245+
220246
private FilterDefinition<BsonDocument> TranslateArrayLength(Expression variableNode, ExpressionType operatorType, ConstantExpression constantNode)
221247
{
222248
var allowedOperators = new[]
@@ -935,10 +961,8 @@ private FilterDefinition<BsonDocument> TranslatePipelineAny(PipelineExpression n
935961
{
936962
var predicate = DocumentToFieldConverter.Convert(whereExpression.Predicate);
937963
filter = __builder.ElemMatch(fieldExpression.FieldName, Translate(predicate));
938-
if (!(fieldExpression.Serializer is IBsonDocumentSerializer))
939-
{
940-
filter = new ScalarElementMatchFilterDefinition<BsonDocument>(filter);
941-
}
964+
965+
filter = ConvertElemMatchFilterToScalarElementMatchIfNeeded(filter, fieldExpression, whereExpression.Predicate);
942966
}
943967

944968
return filter;

tests/MongoDB.Driver.Tests/Linq/IntegrationTestBase.cs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
using System;
1717
using System.Collections.Concurrent;
1818
using System.Collections.Generic;
19-
using System.Linq;
19+
using MongoDB.Bson;
2020
using MongoDB.Bson.Serialization.Attributes;
2121

2222
namespace MongoDB.Driver.Tests.Linq
@@ -101,7 +101,8 @@ private void InsertFirst()
101101
{
102102
D = "Delilah"
103103
}
104-
}
104+
},
105+
Ids = new [] { new ObjectId("111111111111111111111111") }
105106
},
106107
new C
107108
{
@@ -251,6 +252,8 @@ public class RootDescended : Root
251252

252253
public class C
253254
{
255+
public IEnumerable<ObjectId> Ids { get; set; }
256+
254257
public string D { get; set; }
255258

256259
public E E { get; set; }

tests/MongoDB.Driver.Tests/Linq/Translators/PredicateTranslatorTests.cs

Lines changed: 53 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,56 @@ public void Any_with_a_predicate_on_documents()
120120
"{ G : { $elemMatch : { D : \"Don't\", 'E.F' : 33 } } }");
121121
}
122122

123+
[Fact]
124+
public void Any_with_a_predicate_on_document_itself()
125+
{
126+
Assert(
127+
x => x.G.Any(g => g != null),
128+
2,
129+
"{ 'G' : { '$elemMatch' : { '$ne' : null } } }");
130+
131+
Assert(
132+
x => x.G.Any(g => null != g),
133+
2,
134+
"{ 'G' : { '$elemMatch' : { '$ne' : null } } }");
135+
136+
Assert(
137+
x => x.G.Any(g => g.E.I.Any(i => i == "insecure")),
138+
1,
139+
"{ \"G.E.I\" : { '$elemMatch' : { '$eq': 'insecure' } } }");
140+
}
141+
142+
[Fact]
143+
public void Any_with_a_predicate_on_document_itself_and_objectId()
144+
{
145+
Assert(
146+
x => x.G.Any(g => g.Ids.Any(i => i == ObjectId.Parse("111111111111111111111111"))),
147+
1,
148+
"{ 'G.Ids' : { '$elemMatch' : { '$eq' : ObjectId('111111111111111111111111') } } }");
149+
}
150+
151+
[Fact]
152+
public void Any_with_a_predicate_on_documents_itself_and_ClassEquals()
153+
{
154+
var c1 = new C()
155+
{
156+
D = "Dolphin",
157+
E = new E()
158+
{
159+
F = 55,
160+
H = 66,
161+
I = new List<string>()
162+
{
163+
"insecure"
164+
}
165+
}
166+
};
167+
Assert(
168+
x => x.G.Any(g => g == c1),
169+
1,
170+
"{ \"G\" : { \"$elemMatch\" : { \"Ids\" : null, \"D\" : \"Dolphin\", \"E\" : { \"F\" : 55, \"H\" : 66, \"I\" : [\"insecure\"] }, \"S\" : null, \"X\" : null } } }");
171+
}
172+
123173
[Fact]
124174
public void Any_with_a_gte_predicate_on_documents()
125175
{
@@ -585,7 +635,7 @@ public void ClassEquals()
585635
Assert(
586636
x => x.C == new C { D = "Dexter" },
587637
0,
588-
"{C: {D: 'Dexter', E: null, S: null, X: null}}");
638+
"{ C : { Ids : null, D : 'Dexter', E : null, S : null, X : null } }");
589639
}
590640

591641
[Fact]
@@ -594,7 +644,7 @@ public void ClassEqualsMethod()
594644
Assert(
595645
x => x.C.Equals(new C { D = "Dexter" }),
596646
0,
597-
"{C: {D: 'Dexter', E: null, S: null, X: null}}");
647+
"{ C : { Ids : null, D : 'Dexter', E : null, S : null, X : null } }");
598648
}
599649

600650
[Fact]
@@ -603,7 +653,7 @@ public void ClassNotEquals()
603653
Assert(
604654
x => x.C != new C { D = "Dexter" },
605655
2,
606-
"{C: {$ne: {D: 'Dexter', E: null, S: null, X: null}}}");
656+
"{ C : { $ne : { Ids : null, D : 'Dexter', E : null, S : null, X : null } } }");
607657
}
608658

609659
[Fact]

tests/MongoDB.Driver.Tests/Linq/Translators/PredicateValueConversionTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public void Value_conversion_with_custom_type_converter()
4646
Assert(
4747
x => x.C == objectToConvert,
4848
0,
49-
"{ C : { D : 'Dexter', E : null, S : null, X : null } }");
49+
"{ C : { Ids : null, D : 'Dexter', E : null, S : null, X : null } }");
5050
}
5151

5252
public void Assert(Expression<Func<Root, bool>> filter, int expectedCount, string expectedFilter)

0 commit comments

Comments
 (0)