Skip to content

Commit bc1760a

Browse files
authored
CSHARP-5730: Support static String.Compare method (#1789)
1 parent c37bad3 commit bc1760a

File tree

10 files changed

+1291
-125
lines changed

10 files changed

+1291
-125
lines changed

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

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,20 @@ internal enum AstComparisonFilterOperator
2929

3030
internal static class AstComparisonFilterOperatorExtensions
3131
{
32+
public static AstComparisonFilterOperator GetComparisonOperatorForSwappedLeftAndRight(this AstComparisonFilterOperator @operator)
33+
{
34+
return @operator switch
35+
{
36+
AstComparisonFilterOperator.Eq => AstComparisonFilterOperator.Eq,
37+
AstComparisonFilterOperator.Gt => AstComparisonFilterOperator.Lt,
38+
AstComparisonFilterOperator.Gte => AstComparisonFilterOperator.Lte,
39+
AstComparisonFilterOperator.Lt => AstComparisonFilterOperator.Gt,
40+
AstComparisonFilterOperator.Lte => AstComparisonFilterOperator.Gte,
41+
AstComparisonFilterOperator.Ne => AstComparisonFilterOperator.Ne,
42+
_ => throw new InvalidOperationException($"Unexpected comparison filter operator: {@operator}.")
43+
};
44+
}
45+
3246
public static string Render(this AstComparisonFilterOperator @operator)
3347
{
3448
return @operator switch

src/MongoDB.Driver/Linq/Linq3Implementation/Misc/MethodInfoExtensions.cs

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

16+
using System;
17+
using System.Linq;
1618
using System.Reflection;
1719

1820
namespace MongoDB.Driver.Linq.Linq3Implementation.Misc
@@ -38,6 +40,30 @@ public static bool Is(this MethodInfo method, MethodInfo comparand)
3840
return false;
3941
}
4042

43+
public static bool IsInstanceCompareToMethod(this MethodInfo method)
44+
{
45+
if (method.IsPublic &&
46+
!method.IsStatic &&
47+
method.ReturnType == typeof(int) &&
48+
method.Name == "CompareTo" &&
49+
method.GetParameters() is var parameters &&
50+
parameters.Length == 1)
51+
{
52+
var declaringType = method.DeclaringType;
53+
var comparandType = declaringType switch
54+
{
55+
_ when declaringType == typeof(IComparable) => typeof(object),
56+
_ when declaringType.IsConstructedGenericType && declaringType.GetGenericTypeDefinition() == typeof(IComparable<>) => declaringType.GetGenericArguments().Single(),
57+
_ => declaringType
58+
};
59+
60+
var parameterType = parameters[0].ParameterType;
61+
return parameterType == comparandType;
62+
}
63+
64+
return false;
65+
}
66+
4167
public static bool IsOneOf(this MethodInfo method, MethodInfo comparand1, MethodInfo comparand2)
4268
{
4369
return method.Is(comparand1) || method.Is(comparand2);
@@ -78,5 +104,18 @@ public static bool IsOneOf(this MethodInfo method, params MethodInfo[][] compara
78104

79105
return false;
80106
}
107+
108+
public static bool IsStaticCompareMethod(this MethodInfo method)
109+
{
110+
return
111+
method.IsPublic &&
112+
method.IsStatic &&
113+
method.ReturnType == typeof(int) &&
114+
method.Name == "Compare" &&
115+
method.GetParameters() is var parameters &&
116+
parameters.Length == 2 &&
117+
parameters[0].ParameterType == method.DeclaringType &&
118+
parameters[1].ParameterType == method.DeclaringType;
119+
}
81120
}
82121
}

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -72,6 +72,8 @@ internal static class StringMethod
7272
private static readonly MethodInfo __startsWithWithString;
7373
private static readonly MethodInfo __startsWithWithStringAndComparisonType;
7474
private static readonly MethodInfo __startsWithWithStringAndIgnoreCaseAndCulture;
75+
private static readonly MethodInfo __staticCompare;
76+
private static readonly MethodInfo __staticCompareWithIgnoreCase;
7577
private static readonly MethodInfo __stringInWithEnumerable;
7678
private static readonly MethodInfo __stringInWithParams;
7779
private static readonly MethodInfo __stringNinWithEnumerable;
@@ -151,6 +153,8 @@ static StringMethod()
151153
__startsWithWithString = ReflectionInfo.Method((string s, string value) => s.StartsWith(value));
152154
__startsWithWithStringAndComparisonType = ReflectionInfo.Method((string s, string value, StringComparison comparisonType) => s.StartsWith(value, comparisonType));
153155
__startsWithWithStringAndIgnoreCaseAndCulture = ReflectionInfo.Method((string s, string value, bool ignoreCase, CultureInfo culture) => s.StartsWith(value, ignoreCase, culture));
156+
__staticCompare = ReflectionInfo.Method((string strA, string strB) => String.Compare(strA, strB));
157+
__staticCompareWithIgnoreCase = ReflectionInfo.Method((string strA, string strB, bool ignoreCase) => String.Compare(strA, strB, ignoreCase));
154158
__stringInWithEnumerable = ReflectionInfo.Method((string s, IEnumerable<StringOrRegularExpression> values) => s.StringIn(values));
155159
__stringInWithParams = ReflectionInfo.Method((string s, StringOrRegularExpression[] values) => s.StringIn(values));
156160
__stringNinWithEnumerable = ReflectionInfo.Method((string s, IEnumerable<StringOrRegularExpression> values) => s.StringNin(values));
@@ -220,6 +224,8 @@ static StringMethod()
220224
public static MethodInfo StartsWithWithString => __startsWithWithString;
221225
public static MethodInfo StartsWithWithStringAndComparisonType => __startsWithWithStringAndComparisonType;
222226
public static MethodInfo StartsWithWithStringAndIgnoreCaseAndCulture => __startsWithWithStringAndIgnoreCaseAndCulture;
227+
public static MethodInfo StaticCompare => __staticCompare;
228+
public static MethodInfo StaticCompareWithIgnoreCase => __staticCompareWithIgnoreCase;
223229
public static MethodInfo StringInWithEnumerable => __stringInWithEnumerable;
224230
public static MethodInfo StringInWithParams => __stringInWithParams;
225231
public static MethodInfo StringNinWithEnumerable => __stringNinWithEnumerable;

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

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,6 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC
3333
case "AsQueryable": return AsQueryableMethodToAggregationExpressionTranslator.Translate(context, expression);
3434
case "Average": return AverageMethodToAggregationExpressionTranslator.Translate(context, expression);
3535
case "Ceiling": return CeilingMethodToAggregationExpressionTranslator.Translate(context, expression);
36-
case "CompareTo": return CompareToMethodToAggregationExpressionTranslator.Translate(context, expression);
3736
case "Concat": return ConcatMethodToAggregationExpressionTranslator.Translate(context, expression);
3837
case "Constant": return ConstantMethodToAggregationExpressionTranslator.Translate(context, expression);
3938
case "Contains": return ContainsMethodToAggregationExpressionTranslator.Translate(context, expression);
@@ -138,6 +137,10 @@ public static TranslatedExpression Translate(TranslationContext context, MethodC
138137
case "TopN":
139138
return PickMethodToAggregationExpressionTranslator.Translate(context, expression);
140139

140+
case "Compare":
141+
case "CompareTo":
142+
return CompareMethodToAggregationExpressionTranslator.Translate(context, expression);
143+
141144
case "Count":
142145
case "LongCount":
143146
return CountMethodToAggregationExpressionTranslator.Translate(context, expression);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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.Expressions;
17+
using System.Reflection;
18+
using MongoDB.Bson.Serialization.Serializers;
19+
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions;
20+
using MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods;
21+
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
22+
23+
namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators
24+
{
25+
internal static class CompareMethodToAggregationExpressionTranslator
26+
{
27+
private static readonly MethodInfo[] __stringCompareMethods =
28+
[
29+
StringMethod.StaticCompare,
30+
StringMethod.StaticCompareWithIgnoreCase
31+
];
32+
33+
public static TranslatedExpression Translate(TranslationContext context, MethodCallExpression expression)
34+
{
35+
var method = expression.Method;
36+
var arguments = expression.Arguments;
37+
38+
if (method.IsStaticCompareMethod() || method.IsInstanceCompareToMethod() || method.IsOneOf(__stringCompareMethods))
39+
{
40+
Expression value1Expression;
41+
Expression value2Expression;
42+
if (method.IsStatic)
43+
{
44+
value1Expression = arguments[0];
45+
value2Expression = arguments[1];
46+
}
47+
else
48+
{
49+
value1Expression = expression.Object;
50+
value2Expression = arguments[0];
51+
}
52+
53+
var value1Translation = ExpressionToAggregationExpressionTranslator.Translate(context, value1Expression);
54+
var value2Translation = ExpressionToAggregationExpressionTranslator.Translate(context, value2Expression);
55+
56+
AstExpression ast;
57+
if (method.Is(StringMethod.StaticCompareWithIgnoreCase))
58+
{
59+
var ignoreCaseExpression = arguments[2];
60+
var ignoreCase = ignoreCaseExpression.GetConstantValue<bool>(containingExpression: expression);
61+
ast = ignoreCase
62+
? AstExpression.StrCaseCmp(value1Translation.Ast, value2Translation.Ast)
63+
: AstExpression.Cmp(value1Translation.Ast, value2Translation.Ast);
64+
}
65+
else
66+
{
67+
ast = AstExpression.Cmp(value1Translation.Ast, value2Translation.Ast);
68+
}
69+
70+
return new TranslatedExpression(expression, ast, Int32Serializer.Instance);
71+
}
72+
73+
throw new ExpressionNotSupportedException(expression);
74+
}
75+
}
76+
}

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

Lines changed: 0 additions & 43 deletions
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
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.Expressions;
17+
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Filters;
18+
using MongoDB.Driver.Linq.Linq3Implementation.ExtensionMethods;
19+
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
20+
using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToFilterTranslators.ToFilterFieldTranslators;
21+
22+
namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToFilterTranslators.ExpressionTranslators
23+
{
24+
internal static class CompareComparisonExpressionToFilterTranslator
25+
{
26+
public static bool CanTranslate(Expression leftExpression)
27+
{
28+
return
29+
leftExpression is MethodCallExpression leftMethodCallExpression &&
30+
leftMethodCallExpression.Method is var method &&
31+
(method.IsStaticCompareMethod() || method.IsInstanceCompareToMethod() || method.Is(StringMethod.StaticCompareWithIgnoreCase));
32+
}
33+
34+
// caller is responsible for ensuring constant is on the right
35+
public static AstFilter Translate(
36+
TranslationContext context,
37+
Expression expression,
38+
Expression leftExpression,
39+
AstComparisonFilterOperator outerComparisonOperator,
40+
Expression rightExpression)
41+
{
42+
if (CanTranslate(leftExpression))
43+
{
44+
var compareMethodCallExpression = (MethodCallExpression)leftExpression;
45+
var compareMethod = compareMethodCallExpression.Method;
46+
var compareArguments = compareMethodCallExpression.Arguments;
47+
var outerValue = rightExpression.GetConstantValue<int>(containingExpression: expression);
48+
49+
Expression fieldExpression;
50+
Expression innerValueExpression;
51+
if (compareMethod.IsStatic)
52+
{
53+
fieldExpression = compareArguments[0];
54+
innerValueExpression = compareArguments[1];
55+
}
56+
else
57+
{
58+
fieldExpression = compareMethodCallExpression.Object;
59+
innerValueExpression = compareArguments[0];
60+
}
61+
62+
if (compareMethod.Is(StringMethod.StaticCompareWithIgnoreCase))
63+
{
64+
var ignoreCaseExpression = compareArguments[2];
65+
var ignoreCase = ignoreCaseExpression.GetConstantValue<bool>(containingExpression: compareMethodCallExpression);
66+
if (ignoreCase)
67+
{
68+
throw new ExpressionNotSupportedException(compareMethodCallExpression, because: "ignoreCase must be false");
69+
}
70+
}
71+
72+
var fieldComparisonOperator = (outerComparisonOperator, outerValue) switch
73+
{
74+
(AstComparisonFilterOperator.Eq, -1) => AstComparisonFilterOperator.Lt,
75+
(AstComparisonFilterOperator.Ne, -1) => AstComparisonFilterOperator.Gte,
76+
(AstComparisonFilterOperator.Gt, -1) => AstComparisonFilterOperator.Gte,
77+
(AstComparisonFilterOperator.Eq, 0) => AstComparisonFilterOperator.Eq,
78+
(AstComparisonFilterOperator.Ne, 0) => AstComparisonFilterOperator.Ne,
79+
(AstComparisonFilterOperator.Lt, 0) => AstComparisonFilterOperator.Lt,
80+
(AstComparisonFilterOperator.Lte, 0) => AstComparisonFilterOperator.Lte,
81+
(AstComparisonFilterOperator.Gt, 0) => AstComparisonFilterOperator.Gt,
82+
(AstComparisonFilterOperator.Gte, 0) => AstComparisonFilterOperator.Gte,
83+
(AstComparisonFilterOperator.Eq, 1) => AstComparisonFilterOperator.Gt,
84+
(AstComparisonFilterOperator.Ne, 1) => AstComparisonFilterOperator.Lte,
85+
(AstComparisonFilterOperator.Lt, 1) => AstComparisonFilterOperator.Lte,
86+
_ => throw new ExpressionNotSupportedException(expression)
87+
};
88+
89+
if (fieldExpression.NodeType == ExpressionType.Constant && innerValueExpression.NodeType != ExpressionType.Constant)
90+
{
91+
(fieldExpression, innerValueExpression) = (innerValueExpression, fieldExpression);
92+
fieldComparisonOperator = fieldComparisonOperator.GetComparisonOperatorForSwappedLeftAndRight();
93+
}
94+
95+
var fieldTranslation = ExpressionToFilterFieldTranslator.Translate(context, fieldExpression);
96+
var value = innerValueExpression.GetConstantValue<object>(containingExpression: expression);
97+
var serializedValue = SerializationHelper.SerializeValue(fieldTranslation.Serializer, value);
98+
99+
return AstFilter.Compare(fieldTranslation.Ast, fieldComparisonOperator, serializedValue);
100+
}
101+
102+
throw new ExpressionNotSupportedException(expression);
103+
}
104+
}
105+
}

0 commit comments

Comments
 (0)