Skip to content

Commit a3921d0

Browse files
authored
CSHARP-4698: Expression not supported: Regex.IsMatch (#1120)
1 parent a24f1cd commit a3921d0

File tree

7 files changed

+219
-71
lines changed

7 files changed

+219
-71
lines changed

src/MongoDB.Driver.Core/Core/Misc/Feature.cs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ public class Feature
107107
private static readonly Feature __partialIndexes = new Feature("PartialIndexes", WireVersion.Server32);
108108
private static readonly Feature __pickAccumulatorsNewIn52 = new Feature("PickAccumulatorsNewIn52", WireVersion.Server52);
109109
private static readonly Feature __readConcern = new Feature("ReadConcern", WireVersion.Server32);
110+
private static readonly Feature __regexMatch = new Feature("RegexMatch", WireVersion.Server42);
110111
private static readonly Feature __retryableReads = new Feature("RetryableReads", WireVersion.Server36);
111112
private static readonly Feature __retryableWrites = new Feature("RetryableWrites", WireVersion.Server36);
112113
private static readonly Feature __scramSha1Authentication = new Feature("ScramSha1Authentication", WireVersion.Server30);
@@ -600,6 +601,12 @@ public class Feature
600601
[Obsolete("This property will be removed in a later release.")]
601602
public static Feature ReadConcern => __readConcern;
602603

604+
605+
/// <summary>
606+
/// Gets the regex match feature.
607+
/// </summary>
608+
public static Feature RegexMatch => __regexMatch;
609+
603610
/// <summary>
604611
/// Gets the retryable reads feature.
605612
/// </summary>

src/MongoDB.Driver/Linq/Linq3Implementation/Ast/Expressions/AstExpression.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -660,6 +660,9 @@ public static AstExpression Reduce(AstExpression input, AstExpression initialVal
660660
return new AstReduceExpression(input, initialValue, @in);
661661
}
662662

663+
public static AstExpression RegexMatch(AstExpression input, string pattern, string options)
664+
=> new AstRegexExpression(AstRegexOperator.Match, input, pattern, options);
665+
663666
public static AstExpression ReverseArray(AstExpression array)
664667
{
665668
return new AstUnaryExpression(AstUnaryOperator.ReverseArray, array);

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

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

16+
using System.Linq.Expressions;
1617
using System.Reflection;
1718
using System.Text.RegularExpressions;
19+
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
1820

1921
namespace MongoDB.Driver.Linq.Linq3Implementation.Reflection
2022
{
@@ -37,5 +39,61 @@ static RegexMethod()
3739
public static MethodInfo IsMatch => __isMatch;
3840
public static MethodInfo StaticIsMatch => __staticIsMatch;
3941
public static MethodInfo StaticIsMatchWithOptions => __staticIsMatchWithOptions;
42+
43+
// public methods
44+
public static bool IsMatchMethod(MethodCallExpression expression, out Expression inputExpression, out Regex regex)
45+
{
46+
var method = expression.Method;
47+
var arguments = expression.Arguments;
48+
49+
if (method.Is(__isMatch))
50+
{
51+
var objectExpression = expression.Object;
52+
if (objectExpression is ConstantExpression objectConstantExpression)
53+
{
54+
regex = (Regex)objectConstantExpression.Value;
55+
inputExpression = arguments[0];
56+
return true;
57+
}
58+
}
59+
60+
if (method.IsOneOf(__staticIsMatch, __staticIsMatchWithOptions))
61+
{
62+
inputExpression = arguments[0];
63+
var patternExpression = arguments[1];
64+
var optionsExpression = arguments.Count < 3 ? null : arguments[2];
65+
66+
string pattern;
67+
if (patternExpression is ConstantExpression patternConstantExpression)
68+
{
69+
pattern = (string)patternConstantExpression.Value;
70+
}
71+
else
72+
{
73+
goto returnFalse;
74+
}
75+
76+
var options = RegexOptions.None;
77+
if (optionsExpression != null)
78+
{
79+
if (optionsExpression is ConstantExpression optionsConstantExpression)
80+
{
81+
options = (RegexOptions)optionsConstantExpression.Value;
82+
}
83+
else
84+
{
85+
goto returnFalse;
86+
}
87+
}
88+
89+
regex = new Regex(pattern, options);
90+
return true;
91+
}
92+
93+
returnFalse:
94+
inputExpression = null;
95+
regex = null;
96+
return false;
97+
}
4098
}
4199
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ public static AggregationExpression Translate(TranslationContext context, Method
5353
case "get_Item": return GetItemMethodToAggregationExpressionTranslator.Translate(context, expression);
5454
case "Integral": return IntegralMethodToAggregationExpressionTranslator.Translate(context, expression);
5555
case "Intersect": return IntersectMethodToAggregationExpressionTranslator.Translate(context, expression);
56+
case "IsMatch": return IsMatchMethodToAggregationExpressionTranslator.Translate(context, expression);
5657
case "IsNullOrEmpty": return IsNullOrEmptyMethodToAggregationExpressionTranslator.Translate(context, expression);
5758
case "IsNullOrWhiteSpace": return IsNullOrWhiteSpaceMethodToAggregationExpressionTranslator.Translate(context, expression);
5859
case "IsSubsetOf": return IsSubsetOfMethodToAggregationExpressionTranslator.Translate(context, expression);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
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.Bson;
18+
using MongoDB.Bson.Serialization;
19+
using MongoDB.Bson.Serialization.Serializers;
20+
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions;
21+
using MongoDB.Driver.Linq.Linq3Implementation.Reflection;
22+
23+
namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators
24+
{
25+
internal static class IsMatchMethodToAggregationExpressionTranslator
26+
{
27+
// public static methods
28+
public static AggregationExpression Translate(TranslationContext context, MethodCallExpression expression)
29+
{
30+
if (RegexMethod.IsMatchMethod(expression, out var inputExpression, out var regex))
31+
{
32+
var inputTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, inputExpression);
33+
var regularExpression = new BsonRegularExpression(regex);
34+
35+
if (inputTranslation.Serializer is IRepresentationConfigurable representationConfigurable &&
36+
representationConfigurable.Representation != BsonType.String)
37+
{
38+
throw new ExpressionNotSupportedException(inputExpression, expression, because: "input expression is not represented as a string");
39+
}
40+
41+
var ast = AstExpression.RegexMatch(inputTranslation.Ast, regularExpression.Pattern, regularExpression.Options);
42+
return new AggregationExpression(expression, ast, BooleanSerializer.Instance);
43+
}
44+
45+
throw new ExpressionNotSupportedException(expression);
46+
}
47+
}
48+
}

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

Lines changed: 8 additions & 71 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,9 @@
1414
*/
1515

1616
using System.Linq.Expressions;
17-
using System.Text.RegularExpressions;
1817
using MongoDB.Bson;
1918
using MongoDB.Bson.Serialization;
2019
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Filters;
21-
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
2220
using MongoDB.Driver.Linq.Linq3Implementation.Reflection;
2321
using MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToFilterTranslators.ToFilterFieldTranslators;
2422

@@ -29,82 +27,21 @@ internal static class IsMatchMethodToFilterTranslator
2927
// public static methods
3028
public static AstFilter Translate(TranslationContext context, MethodCallExpression expression)
3129
{
32-
if (IsMatchMethod(expression, out var inputExpression, out var regularExpression))
30+
if (RegexMethod.IsMatchMethod(expression, out var inputExpression, out var regex))
3331
{
34-
return Translate(context, expression, inputExpression, regularExpression);
35-
}
36-
37-
throw new ExpressionNotSupportedException(expression);
38-
}
39-
40-
// private static methods
41-
private static bool IsMatchMethod(MethodCallExpression expression, out Expression inputExpression, out Regex regex)
42-
{
43-
var method = expression.Method;
44-
var arguments = expression.Arguments;
45-
46-
if (method.Is(RegexMethod.IsMatch))
47-
{
48-
var objectExpression = expression.Object;
49-
if (objectExpression is ConstantExpression objectConstantExpression)
50-
{
51-
regex = (Regex)objectConstantExpression.Value;
52-
inputExpression = arguments[0];
53-
return true;
54-
}
55-
}
56-
57-
if (method.IsOneOf(RegexMethod.StaticIsMatch, RegexMethod.StaticIsMatchWithOptions))
58-
{
59-
inputExpression = arguments[0];
60-
var patternExpression = arguments[1];
61-
var optionsExpression = arguments.Count < 3 ? null : arguments[2];
32+
var inputFieldAst = ExpressionToFilterFieldTranslator.Translate(context, inputExpression);
33+
var regularExpression = new BsonRegularExpression(regex);
6234

63-
string pattern;
64-
if (patternExpression is ConstantExpression patternConstantExpression)
35+
if (inputFieldAst.Serializer is IRepresentationConfigurable representationConfigurable &&
36+
representationConfigurable.Representation != BsonType.String)
6537
{
66-
pattern = (string)patternConstantExpression.Value;
67-
}
68-
else
69-
{
70-
goto returnFalse;
38+
throw new ExpressionNotSupportedException(inputExpression, expression, because: $"field \"{inputFieldAst.Path}\" is not represented as a string");
7139
}
7240

73-
var options = RegexOptions.None;
74-
if (optionsExpression != null)
75-
{
76-
if (optionsExpression is ConstantExpression optionsConstantExpression)
77-
{
78-
options = (RegexOptions)optionsConstantExpression.Value;
79-
}
80-
else
81-
{
82-
goto returnFalse;
83-
}
84-
}
85-
86-
regex = new Regex(pattern, options);
87-
return true;
41+
return AstFilter.Regex(inputFieldAst, regularExpression.Pattern, regularExpression.Options);
8842
}
8943

90-
returnFalse:
91-
inputExpression = null;
92-
regex = null;
93-
return false;
94-
}
95-
96-
private static AstFilter Translate(TranslationContext context, Expression expression, Expression inputExpression, Regex regex)
97-
{
98-
var inputFieldAst = ExpressionToFilterFieldTranslator.Translate(context, inputExpression);
99-
var regularExpression = new BsonRegularExpression(regex);
100-
101-
if (inputFieldAst.Serializer is IRepresentationConfigurable representationConfigurable &&
102-
representationConfigurable.Representation != BsonType.String)
103-
{
104-
throw new ExpressionNotSupportedException(inputExpression, expression, because: $"field \"{inputFieldAst.Path}\" is not represented as a string");
105-
}
106-
107-
return AstFilter.Regex(inputFieldAst, regularExpression.Pattern, regularExpression.Options);
44+
throw new ExpressionNotSupportedException(expression);
10845
}
10946
}
11047
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
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 System.Text.RegularExpressions;
18+
using FluentAssertions;
19+
using MongoDB.Driver.Core.Misc;
20+
using MongoDB.Driver.Core.TestHelpers.XunitExtensions;
21+
using MongoDB.Driver.Linq;
22+
using Xunit;
23+
24+
namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators
25+
{
26+
public class IsMatchMethodToAggregationExpressionTranslatorTests : Linq3IntegrationTest
27+
{
28+
[Fact]
29+
public void Should_translate_instance_regex_isMatch()
30+
{
31+
RequireServer.Check().Supports(Feature.RegexMatch);
32+
33+
var collection = CreateCollection();
34+
var regex = new Regex(@"\dB.*0");
35+
var queryable = collection.AsQueryable()
36+
.Where(i => regex.IsMatch(i.A + i.B));
37+
38+
var stages = Translate(collection, queryable);
39+
AssertStages(stages, @"{ '$match' : { '$expr' : { '$regexMatch' : { 'input' : { '$concat' : ['$A', '$B'] }, 'regex' : '\\dB.*0', 'options' : '' } } } }");
40+
41+
var result = queryable.Single();
42+
result.Id.Should().Be(2);
43+
}
44+
45+
[Fact]
46+
public void Should_translate_static_regex_isMatch()
47+
{
48+
RequireServer.Check().Supports(Feature.RegexMatch);
49+
50+
var collection = CreateCollection();
51+
var queryable = collection.AsQueryable()
52+
.Where(i => Regex.IsMatch(i.A + i.B, @"\dB.*0"));
53+
54+
var stages = Translate(collection, queryable);
55+
AssertStages(stages, @"{ '$match' : { '$expr' : { '$regexMatch' : { 'input' : { '$concat' : ['$A', '$B'] }, 'regex' : '\\dB.*0', 'options' : '' } } } }");
56+
57+
var result = queryable.Single();
58+
result.Id.Should().Be(2);
59+
}
60+
61+
[Fact]
62+
public void Should_translate_static_regex_isMatch_with_options()
63+
{
64+
RequireServer.Check().Supports(Feature.RegexMatch);
65+
66+
var collection = CreateCollection();
67+
var queryable = collection.AsQueryable()
68+
.Where(i => Regex.IsMatch(i.A + i.B, @"\dB.*0", RegexOptions.IgnoreCase));
69+
70+
var stages = Translate(collection, queryable);
71+
AssertStages(stages, @"{ '$match' : { '$expr' : { '$regexMatch' : { 'input' : { '$concat' : ['$A', '$B'] }, 'regex' : '\\dB.*0', 'options' : 'i' } } } }");
72+
73+
var result = queryable.Single();
74+
result.Id.Should().Be(2);
75+
}
76+
77+
private IMongoCollection<Data> CreateCollection()
78+
{
79+
var collection = GetCollection<Data>("test");
80+
CreateCollection(
81+
collection,
82+
new Data { Id = 1, A = "ABC", B = "1" },
83+
new Data { Id = 2, A = "1Br", B = "0" });
84+
return collection;
85+
}
86+
87+
private class Data
88+
{
89+
public int Id { get; set; }
90+
public string A { get; set; }
91+
public string B { get; set; }
92+
}
93+
}
94+
}

0 commit comments

Comments
 (0)