Skip to content

Commit c8b95a3

Browse files
authored
CSHARP-4090 / CSHARP-4092 (mongodb#750)
CSHARP-4090: Implement support for string.Contains(match, StringComparison). CSHARP-4092: Fix case-insensitive StartsWith/EndsWith to generate correct MQL.
1 parent b335c34 commit c8b95a3

File tree

4 files changed

+177
-5
lines changed

4 files changed

+177
-5
lines changed

src/MongoDB.Driver/Linq/Linq3Implementation/Reflection/StringMethod.cs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,9 @@ internal static class StringMethod
2424
{
2525
// private static fields
2626
private static readonly MethodInfo __contains;
27+
#if NETSTANDARD2_1_OR_GREATER
28+
private static readonly MethodInfo __containsWithComparisonType;
29+
#endif
2730
private static readonly MethodInfo __endsWith;
2831
private static readonly MethodInfo __endsWithWithComparisonType;
2932
private static readonly MethodInfo __endsWithWithIgnoreCaseAndCulture;
@@ -72,6 +75,9 @@ internal static class StringMethod
7275
static StringMethod()
7376
{
7477
__contains = ReflectionInfo.Method((string s, string value) => s.Contains(value));
78+
#if NETSTANDARD2_1_OR_GREATER
79+
__containsWithComparisonType = ReflectionInfo.Method((string s, string value, StringComparison comparisonType) => s.Contains(value, comparisonType));
80+
#endif
7581
__endsWith = ReflectionInfo.Method((string s, string value) => s.EndsWith(value));
7682
__endsWithWithComparisonType = ReflectionInfo.Method((string s, string value, StringComparison comparisonType) => s.EndsWith(value, comparisonType));
7783
__endsWithWithIgnoreCaseAndCulture = ReflectionInfo.Method((string s, string value, bool ignoreCase, CultureInfo culture) => s.EndsWith(value, ignoreCase, culture));
@@ -119,6 +125,9 @@ static StringMethod()
119125

120126
// public properties
121127
public static MethodInfo Contains => __contains;
128+
#if NETSTANDARD2_1_OR_GREATER
129+
public static MethodInfo ContainsWithComparisonType => __containsWithComparisonType;
130+
#endif
122131
public static MethodInfo EndsWith => __endsWith;
123132
public static MethodInfo EndsWithWithComparisonType => __endsWithWithComparisonType;
124133
public static MethodInfo EndsWithWithIgnoreCaseAndCulture => __endsWithWithIgnoreCaseAndCulture;

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

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
*/
1515

1616
using System.Linq.Expressions;
17+
using System.Reflection;
1718
using MongoDB.Bson.Serialization.Serializers;
1819
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions;
1920
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
@@ -23,10 +24,23 @@ namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggreg
2324
{
2425
internal static class ContainsMethodToAggregationExpressionTranslator
2526
{
27+
private static readonly MethodInfo[] __containsMethods;
28+
29+
static ContainsMethodToAggregationExpressionTranslator()
30+
{
31+
__containsMethods = new[]
32+
{
33+
StringMethod.Contains,
34+
#if NETSTANDARD2_1_OR_GREATER
35+
StringMethod.ContainsWithComparisonType
36+
#endif
37+
};
38+
}
39+
2640
// public methods
2741
public static AggregationExpression Translate(TranslationContext context, MethodCallExpression expression)
2842
{
29-
if (expression.Method.Is(StringMethod.Contains))
43+
if (expression.Method.IsOneOf(__containsMethods))
3044
{
3145
return StartsWithContainsOrEndsWithMethodToAggregationExpressionTranslator.Translate(context, expression);
3246
}

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

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,12 +14,10 @@
1414
*/
1515

1616
using System;
17-
using System.Collections.Generic;
1817
using System.Globalization;
1918
using System.Linq.Expressions;
2019
using System.Reflection;
2120
using MongoDB.Bson.Serialization.Serializers;
22-
using MongoDB.Driver.Linq.Linq3Implementation.Ast;
2321
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions;
2422
using MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods;
2523
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
@@ -40,6 +38,9 @@ static StartsWithContainsOrEndsWithMethodToAggregationExpressionTranslator()
4038
StringMethod.StartsWithWithComparisonType,
4139
StringMethod.StartsWithWithIgnoreCaseAndCulture,
4240
StringMethod.Contains,
41+
#if NETSTANDARD2_1_OR_GREATER
42+
StringMethod.ContainsWithComparisonType,
43+
#endif
4344
StringMethod.EndsWith,
4445
StringMethod.EndsWithWithComparisonType,
4546
StringMethod.EndsWithWithIgnoreCaseAndCulture
@@ -48,6 +49,9 @@ static StartsWithContainsOrEndsWithMethodToAggregationExpressionTranslator()
4849
__withComparisonTypeMethods = new[]
4950
{
5051
StringMethod.StartsWithWithComparisonType,
52+
#if NETSTANDARD2_1_OR_GREATER
53+
StringMethod.ContainsWithComparisonType,
54+
#endif
5155
StringMethod.EndsWithWithComparisonType
5256
};
5357

@@ -86,7 +90,7 @@ public static AggregationExpression Translate(TranslationContext context, Method
8690
if (ignoreCase)
8791
{
8892
stringAst = AstExpression.ToLower(stringAst);
89-
substringAst = AstExpression.ToLower(stringAst);
93+
substringAst = AstExpression.ToLower(substringAst);
9094
}
9195
var ast = CreateAst(method.Name, stringAst, substringAst);
9296
return new AggregationExpression(expression, ast, new BooleanSerializer());
@@ -118,7 +122,7 @@ static AstExpression CreateEndsWithAst(AstExpression stringAst, AstExpression su
118122
{
119123
var (stringVar, stringSimpleAst) = AstExpression.UseVarIfNotSimple("string", stringAst);
120124
var (substringVar, substringSimpleAst) = AstExpression.UseVarIfNotSimple("substring", substringAst);
121-
var startAst = AstExpression.Subtract(AstExpression.StrLenCP(stringSimpleAst), AstExpression.StrLenCP(substringSimpleAst));
125+
var startAst = AstExpression.Subtract(AstExpression.StrLenCP(stringSimpleAst), AstExpression.StrLenCP(substringSimpleAst));
122126
var ast = AstExpression.Gte(AstExpression.IndexOfCP(stringSimpleAst, substringSimpleAst, startAst), 0);
123127
return AstExpression.Let(stringVar, substringVar, ast);
124128
}
Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
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 FluentAssertions;
18+
using MongoDB.Driver.Linq;
19+
using Xunit;
20+
21+
namespace MongoDB.Driver.Tests.Linq.Linq3ImplementationTests.Jira
22+
{
23+
public class CSharp4090Tests : Linq3IntegrationTest
24+
{
25+
[Fact]
26+
public void String_starts_with_with_current_culture_ignore_case_should_work()
27+
{
28+
var collection = GetCollection<C>();
29+
collection.Database.DropCollection(collection.CollectionNamespace.CollectionName);
30+
31+
collection.InsertMany(
32+
new[]
33+
{
34+
new C { Id = 100, Text = "Apple-Orange-Banana", Match = "apple" },
35+
new C { Id = 101, Text = "Apple-Kiwi-Pear", Match = "mango" }
36+
});
37+
38+
var find = collection.Find(x => x.Text.StartsWith(x.Match, StringComparison.CurrentCultureIgnoreCase));
39+
40+
var rendered = find.ToString();
41+
rendered.Should().Be("find({ \"$expr\" : { \"$eq\" : [{ \"$indexOfCP\" : [{ \"$toLower\" : \"$Text\" }, { \"$toLower\" : \"$Match\" }] }, 0] } })");
42+
43+
var results = find.ToList();
44+
results.Count.Should().Be(1);
45+
results[0].Id.Should().Be(100);
46+
}
47+
48+
[Theory]
49+
[InlineData(StringComparison.InvariantCulture)]
50+
[InlineData(StringComparison.InvariantCultureIgnoreCase)]
51+
[InlineData(StringComparison.Ordinal)]
52+
[InlineData(StringComparison.OrdinalIgnoreCase)]
53+
public void Should_throw_not_supported_exception_for_starts_with_with_unsupported_string_comparison_type(StringComparison comparison)
54+
{
55+
var collection = GetCollection<C>();
56+
57+
var exception = Record.Exception(() => collection.Find(x => x.Text.StartsWith("orange", comparison)).ToList());
58+
59+
exception.Should().BeOfType<ExpressionNotSupportedException>();
60+
}
61+
62+
#if NETCOREAPP3_1_OR_GREATER
63+
[Fact]
64+
public void String_contains_with_current_culture_ignore_case_should_work()
65+
{
66+
var collection = GetCollection<C>();
67+
collection.Database.DropCollection(collection.CollectionNamespace.CollectionName);
68+
69+
collection.InsertMany(
70+
new[]
71+
{
72+
new C { Id = 100, Text = "Apple-Orange-Banana", Match = "orange" },
73+
new C { Id = 101, Text = "Apple-Kiwi-Pear", Match = "mango" }
74+
});
75+
76+
var find = collection.Find(x => x.Text.Contains(x.Match, StringComparison.CurrentCultureIgnoreCase));
77+
78+
var rendered = find.ToString();
79+
rendered.Should().Be("find({ \"$expr\" : { \"$gte\" : [{ \"$indexOfCP\" : [{ \"$toLower\" : \"$Text\" }, { \"$toLower\" : \"$Match\" }] }, 0] } })");
80+
81+
var results = find.ToList();
82+
results.Count.Should().Be(1);
83+
results[0].Id.Should().Be(100);
84+
}
85+
86+
[Theory]
87+
[InlineData(StringComparison.InvariantCulture)]
88+
[InlineData(StringComparison.InvariantCultureIgnoreCase)]
89+
[InlineData(StringComparison.Ordinal)]
90+
[InlineData(StringComparison.OrdinalIgnoreCase)]
91+
public void Should_throw_not_supported_exception_for_contains_with_unsupported_string_comparison_type(StringComparison comparison)
92+
{
93+
var collection = GetCollection<C>();
94+
95+
var exception = Record.Exception(() => collection.Find(x => x.Text.Contains("orange", comparison)).ToList());
96+
97+
exception.Should().BeOfType<ExpressionNotSupportedException>();
98+
}
99+
#endif
100+
101+
[Fact]
102+
public void String_ends_with_with_current_culture_ignore_case_should_work()
103+
{
104+
var collection = GetCollection<C>();
105+
collection.Database.DropCollection(collection.CollectionNamespace.CollectionName);
106+
107+
collection.InsertMany(
108+
new[]
109+
{
110+
new C { Id = 100, Text = "Apple-Orange-Banana", Match = "banana" },
111+
new C { Id = 101, Text = "Apple-Kiwi-Pear", Match = "mango" }
112+
});
113+
114+
var find = collection.Find(x => x.Text.EndsWith(x.Match, StringComparison.CurrentCultureIgnoreCase));
115+
116+
var rendered = find.ToString();
117+
rendered.Should().Be("find({ \"$expr\" : { \"$let\" : { \"vars\" : { \"string\" : { \"$toLower\" : \"$Text\" }, \"substring\" : { \"$toLower\" : \"$Match\" } }, \"in\" : { \"$gte\" : [{ \"$indexOfCP\" : [\"$$string\", \"$$substring\", { \"$subtract\" : [{ \"$strLenCP\" : \"$$string\" }, { \"$strLenCP\" : \"$$substring\" }] }] }, 0] } } } })");
118+
119+
var results = find.ToList();
120+
results.Count.Should().Be(1);
121+
results[0].Id.Should().Be(100);
122+
}
123+
124+
[Theory]
125+
[InlineData(StringComparison.InvariantCulture)]
126+
[InlineData(StringComparison.InvariantCultureIgnoreCase)]
127+
[InlineData(StringComparison.Ordinal)]
128+
[InlineData(StringComparison.OrdinalIgnoreCase)]
129+
public void Should_throw_not_supported_exception_for_ends_with_with_unsupported_string_comparison_type(StringComparison comparison)
130+
{
131+
var collection = GetCollection<C>();
132+
133+
var exception = Record.Exception(() => collection.Find(x => x.Text.EndsWith("orange", comparison)).ToList());
134+
135+
exception.Should().BeOfType<ExpressionNotSupportedException>();
136+
}
137+
138+
public class C
139+
{
140+
public int Id { get; set; }
141+
public string Text { get; set; }
142+
public string Match { get; set; }
143+
}
144+
}
145+
}

0 commit comments

Comments
 (0)