Skip to content

Commit a34499f

Browse files
committed
CSHARP-2348: Switch to $expr when $elemMatch filter is not supported by server.
1 parent 0870e13 commit a34499f

File tree

3 files changed

+167
-0
lines changed

3 files changed

+167
-0
lines changed

src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Filters/AstFilter.cs

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

16+
using System;
1617
using System.Collections.Generic;
1718
using System.Linq;
1819
using MongoDB.Bson;
@@ -92,7 +93,30 @@ public static AstFieldOperationFilter Compare(AstFilterField field, AstCompariso
9293

9394
public static AstFieldOperationFilter ElemMatch(AstFilterField field, AstFilter filter)
9495
{
96+
if (!ServerSupportsElemMatchFilter(filter))
97+
{
98+
throw new ExpressionNotSupportedException($"$elemMatch does not support filter: {filter}");
99+
}
100+
95101
return new AstFieldOperationFilter(field, new AstElemMatchFilterOperation(filter));
102+
103+
static bool ServerSupportsElemMatchFilter(AstFilter filter)
104+
{
105+
if (filter is AstOrFilter orFilter &&
106+
orFilter.Filters.Any(IsImpliedElementFilter))
107+
{
108+
return false;
109+
}
110+
111+
// TODO: add detection of other unsupported filters as we discover them
112+
113+
return true;
114+
115+
static bool IsImpliedElementFilter(AstFilter filter)
116+
=>
117+
filter is AstFieldOperationFilter fieldOperationFilter &&
118+
fieldOperationFilter.Field.Path == "@<elem>";
119+
}
96120
}
97121

98122
public static AstFieldOperationFilter Eq(AstFilterField field, BsonValue value)

src/MongoDB.Driver/Linq/Linq3Implementation/Translators/ExpressionToFilterTranslators/MethodTranslators/AllOrAnyMethodToFilterTranslator.cs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,12 +72,30 @@ public static AstFilter Translate(TranslationContext context, MethodCallExpressi
7272
}
7373
else
7474
{
75+
// { $elemMatch : { $or : [{ $eq : x }, { $eq : y }, ... ] } } => { $in : [x, y, ...] }
76+
if (filter is AstOrFilter orFilter &&
77+
orFilter.Filters.All(IsImpliedElementEqualityComparison))
78+
{
79+
var values = orFilter.Filters
80+
.Select(filter => ((AstFieldOperationFilter)filter).Operation)
81+
.Select(operation => ((AstComparisonFilterOperation)operation).Value);
82+
83+
return AstFilter.In(field, values);
84+
}
85+
7586
return AstFilter.ElemMatch(field, filter);
7687
}
7788
}
7889
}
7990

8091
throw new ExpressionNotSupportedException(expression);
92+
93+
static bool IsImpliedElementEqualityComparison(AstFilter filter)
94+
=>
95+
filter is AstFieldOperationFilter fieldOperationFilter &&
96+
fieldOperationFilter.Field.Path == "@<elem>" &&
97+
fieldOperationFilter.Operation is AstComparisonFilterOperation comparisonFilterOperation &&
98+
comparisonFilterOperation.Operator == AstComparisonFilterOperator.Eq;
8199
}
82100
}
83101

Lines changed: 125 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,125 @@
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 MongoDB.Driver.Linq;
19+
using MongoDB.Driver.Tests.Linq.Linq3Implementation;
20+
using MongoDB.TestHelpers.XunitExtensions;
21+
using Xunit;
22+
23+
namespace MongoDB.Driver.Tests.Linq.Linq3ImplementationTests.Jira
24+
{
25+
public class CSharp2348Tests : Linq3IntegrationTest
26+
{
27+
[Theory]
28+
[ParameterAttributeData]
29+
public void Any_with_equals_should_work(
30+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
31+
{
32+
var collection = CreateCollection(linqProvider);
33+
34+
var find = collection.Find(x => x.A.Any(v => v == 2));
35+
36+
var renderedFilter = TranslateFindFilter(collection, find);
37+
if (linqProvider == LinqProvider.V2)
38+
{
39+
renderedFilter.Should().Be("{ A : { $elemMatch : { $eq : 2 } } }"); // LINQ2 translation is not as simple as it could be but is correct
40+
}
41+
else
42+
{
43+
renderedFilter.Should().Be("{ A : 2 }");
44+
}
45+
46+
var results = find.ToList().OrderBy(x => x.Id).ToList();
47+
results.Select(x => x.Id).Should().Equal(2);
48+
}
49+
50+
[Theory]
51+
[ParameterAttributeData]
52+
public void Any_with_or_of_equals_should_work(
53+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
54+
{
55+
var collection = CreateCollection(linqProvider);
56+
57+
var find = collection.Find(x => x.A.Any(v => v == 2 || v == 3));
58+
59+
var renderedFilter = TranslateFindFilter(collection, find);
60+
var results = find.ToList().OrderBy(x => x.Id).ToList();
61+
62+
if (linqProvider == LinqProvider.V2)
63+
{
64+
renderedFilter.Should().Be("{ A : { $elemMatch : { $or : [{ '' : 2 }, { '' : 3 }] } } }"); // LINQ2 translation is wrong
65+
results.Should().BeEmpty(); // LINQ2 result is wrong
66+
}
67+
else
68+
{
69+
renderedFilter.Should().Be("{ A : { $in : [2, 3] } }");
70+
results.Select(x => x.Id).Should().Equal(2, 3);
71+
}
72+
}
73+
74+
[Theory]
75+
[ParameterAttributeData]
76+
public void Any_with_or_of_equals_and_greater_than_should_work(
77+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
78+
{
79+
var collection = CreateCollection(linqProvider);
80+
81+
var find = collection.Find(x => x.A.Any(v => v == 2 || v > 3));
82+
83+
var renderedFilter = TranslateFindFilter(collection, find);
84+
var results = find.ToList().OrderBy(x => x.Id).ToList();
85+
86+
if (linqProvider == LinqProvider.V2)
87+
{
88+
renderedFilter.Should().Be("{ A : { $elemMatch : { $or : [{ '' : 2 }, { '' : { $gt : 3 } }] } } }"); // LINQ2 translation is wrong
89+
results.Should().BeEmpty(); // LINQ2 result is wrong
90+
}
91+
else
92+
{
93+
// the ideal translation would be { Roles : { $elemMatch : { $or : [{ $eq : 2 }, { $gt : 3 }] } } }
94+
// but the server does not support implied element names in combination with $or
95+
// see: https://jira.mongodb.org/browse/SERVER-93020
96+
renderedFilter.Should().Be("{ $expr : { $anyElementTrue : { $map : { input : '$A', as : 'v', in : { $or : [{ $eq : ['$$v', 2] }, { $gt : ['$$v', 3] }] } } } } }");
97+
results.Select(x => x.Id).Should().Equal(2, 4);
98+
}
99+
}
100+
101+
private IMongoCollection<User> CreateCollection(LinqProvider linqProvider)
102+
{
103+
var collection = GetCollection<User>("test", linqProvider);
104+
CreateCollection(
105+
collection,
106+
new User { Id = 1, A = new[] { 1 } },
107+
new User { Id = 2, A = new[] { 1, 2 } },
108+
new User { Id = 3, A = new[] { 1, 3 } },
109+
new User { Id = 4, A = new[] { 1, 4 } });
110+
return collection;
111+
}
112+
113+
public class User
114+
{
115+
public int Id { get; set; }
116+
public int[] A { get; set; }
117+
}
118+
119+
public enum Role
120+
{
121+
Admin = 1,
122+
Editor = 2
123+
}
124+
}
125+
}

0 commit comments

Comments
 (0)