Skip to content

Commit 5974753

Browse files
committed
CSHARP-4665: Add LINQ3 support for IsNullOrWhiteSpace.
1 parent 5633ed6 commit 5974753

File tree

9 files changed

+395
-1
lines changed

9 files changed

+395
-1
lines changed

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

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -125,6 +125,7 @@ public class Feature
125125
private static readonly Feature __tailableCursor = new Feature("TailableCursor", WireVersion.Server32);
126126
private static readonly Feature __toConversionOperators = new Feature("ToConversionOperators", WireVersion.Server40);
127127
private static readonly Feature __trigOperators = new Feature("TrigOperators", WireVersion.Server42);
128+
private static readonly Feature __trimOperator = new Feature("TrimOperator", WireVersion.Server40);
128129
private static readonly Feature __transactions = new Feature("Transactions", WireVersion.Server40);
129130
private static readonly Feature __updateWithAggregationPipeline = new Feature("UpdateWithAggregationPipeline", WireVersion.Server42);
130131
private static readonly Feature __userManagementCommands = new Feature("UserManagementCommands", WireVersion.Server26);
@@ -705,6 +706,11 @@ public class Feature
705706
/// </summary>
706707
public static Feature TrigOperators => __trigOperators;
707708

709+
/// <summary>
710+
/// Gets the trim operator feature.
711+
/// </summary>
712+
public static Feature TrimOperator => __trimOperator;
713+
708714
/// <summary>
709715
/// Gets the user management commands feature.
710716
/// </summary>

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ internal static class StringMethod
5353
private static readonly MethodInfo __indexOfWithStringAndStartIndexAndComparisonType;
5454
private static readonly MethodInfo __indexOfWithStringAndStartIndexAndCountAndComparisonType;
5555
private static readonly MethodInfo __isNullOrEmpty;
56+
private static readonly MethodInfo __isNullOrWhiteSpace;
5657
private static readonly MethodInfo __splitWithChars;
5758
private static readonly MethodInfo __splitWithCharsAndCount;
5859
private static readonly MethodInfo __splitWithCharsAndCountAndOptions;
@@ -124,6 +125,7 @@ static StringMethod()
124125
__indexOfWithStringAndStartIndexAndComparisonType = ReflectionInfo.Method((string s, string value, int startIndex, StringComparison comparisonType) => s.IndexOf(value, startIndex, comparisonType));
125126
__indexOfWithStringAndStartIndexAndCountAndComparisonType = ReflectionInfo.Method((string s, string value, int startIndex, int count, StringComparison comparisonType) => s.IndexOf(value, startIndex, count, comparisonType));
126127
__isNullOrEmpty = ReflectionInfo.Method((string value) => string.IsNullOrEmpty(value));
128+
__isNullOrWhiteSpace = ReflectionInfo.Method((string value) => string.IsNullOrWhiteSpace(value));
127129
__splitWithChars = ReflectionInfo.Method((string s, char[] separator) => s.Split(separator));
128130
__splitWithCharsAndCount = ReflectionInfo.Method((string s, char[] separator, int count) => s.Split(separator, count));
129131
__splitWithCharsAndCountAndOptions = ReflectionInfo.Method((string s, char[] separator, int count, StringSplitOptions options) => s.Split(separator, count, options));
@@ -183,6 +185,7 @@ static StringMethod()
183185
public static MethodInfo IndexOfWithStringAndStartIndexAndComparisonType => __indexOfWithStringAndStartIndexAndComparisonType;
184186
public static MethodInfo IndexOfWithStringAndStartIndexAndCountAndComparisonType => __indexOfWithStringAndStartIndexAndCountAndComparisonType;
185187
public static MethodInfo IsNullOrEmpty => __isNullOrEmpty;
188+
public static MethodInfo IsNullOrWhiteSpace => __isNullOrWhiteSpace;
186189
public static MethodInfo SplitWithChars => __splitWithChars;
187190
public static MethodInfo SplitWithCharsAndCount => __splitWithCharsAndCount;
188191
public static MethodInfo SplitWithCharsAndCountAndOptions => __splitWithCharsAndCountAndOptions;

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,7 @@ public static AggregationExpression Translate(TranslationContext context, Method
5454
case "Integral": return IntegralMethodToAggregationExpressionTranslator.Translate(context, expression);
5555
case "Intersect": return IntersectMethodToAggregationExpressionTranslator.Translate(context, expression);
5656
case "IsNullOrEmpty": return IsNullOrEmptyMethodToAggregationExpressionTranslator.Translate(context, expression);
57+
case "IsNullOrWhiteSpace": return IsNullOrWhiteSpaceMethodToAggregationExpressionTranslator.Translate(context, expression);
5758
case "IsSubsetOf": return IsSubsetOfMethodToAggregationExpressionTranslator.Translate(context, expression);
5859
case "Locf": return LocfMethodToAggregationExpressionTranslator.Translate(context, expression);
5960
case "Parse": return ParseMethodToAggregationExpressionTranslator.Translate(context, expression);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
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.Serializers;
19+
using MongoDB.Driver.Linq.Linq3Implementation.Ast.Expressions;
20+
using MongoDB.Driver.Linq.Linq3Implementation.Misc;
21+
22+
namespace MongoDB.Driver.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators
23+
{
24+
internal static class IsNullOrWhiteSpaceMethodToAggregationExpressionTranslator
25+
{
26+
public static AggregationExpression Translate(TranslationContext context, MethodCallExpression expression)
27+
{
28+
var method = expression.Method;
29+
var arguments = expression.Arguments;
30+
31+
if (method.Is(StringMethod.IsNullOrWhiteSpace))
32+
{
33+
var stringExpression = arguments[0];
34+
var stringTranslation = ExpressionToAggregationExpressionTranslator.Translate(context, stringExpression);
35+
var ast = AstExpression.Or(
36+
AstExpression.Eq(stringTranslation.Ast, BsonNull.Value),
37+
AstExpression.Eq(AstExpression.Trim(stringTranslation.Ast), ""));
38+
39+
return new AggregationExpression(expression, ast, new BooleanSerializer());
40+
}
41+
42+
throw new ExpressionNotSupportedException(expression);
43+
}
44+
}
45+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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.Driver.Linq.Linq3Implementation.Ast.Filters;
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.MethodTranslators
23+
{
24+
internal static class IsNullOrWhiteSpaceMethodToFilterTranslator
25+
{
26+
// public static methods
27+
public static AstFilter Translate(TranslationContext context, MethodCallExpression expression)
28+
{
29+
var method = expression.Method;
30+
var arguments = expression.Arguments;
31+
32+
if (method.Is(StringMethod.IsNullOrWhiteSpace))
33+
{
34+
var fieldExpression = arguments[0];
35+
var field = ExpressionToFilterFieldTranslator.Translate(context, fieldExpression);
36+
return AstFilter.In(field, new BsonValue[] { BsonNull.Value, BsonRegularExpression.Create(@"^\s*$") });
37+
}
38+
39+
throw new ExpressionNotSupportedException(expression);
40+
}
41+
}
42+
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ public static AstFilter Translate(TranslationContext context, MethodCallExpressi
3232
case "Inject": return InjectMethodToFilterTranslator.Translate(context, expression);
3333
case "IsMatch": return IsMatchMethodToFilterTranslator.Translate(context, expression);
3434
case "IsNullOrEmpty": return IsNullOrEmptyMethodToFilterTranslator.Translate(context, expression);
35+
case "IsNullOrWhiteSpace": return IsNullOrWhiteSpaceMethodToFilterTranslator.Translate(context, expression);
3536
case "StartsWith": return StartsWithMethodToFilterTranslator.Translate(context, expression);
3637

3738
case "All":

tests/MongoDB.Driver.Tests/Linq/Linq3Implementation/Linq3IntegrationTest.cs

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -169,9 +169,15 @@ protected BsonDocument TranslateFilter<TDocument>(IMongoCollection<TDocument> co
169169
}
170170

171171
protected BsonDocument TranslateFindFilter<TDocument, TProjection>(IMongoCollection<TDocument> collection, IFindFluent<TDocument, TProjection> find)
172+
{
173+
var linqProvider = collection.Database.Client.Settings.LinqProvider;
174+
return TranslateFindFilter(collection, find, linqProvider);
175+
}
176+
177+
protected BsonDocument TranslateFindFilter<TDocument, TProjection>(IMongoCollection<TDocument> collection, IFindFluent<TDocument, TProjection> find, LinqProvider linqProvider)
172178
{
173179
var filterDefinition = ((FindFluent<TDocument, TProjection>)find).Filter;
174-
return filterDefinition.Render(collection.DocumentSerializer, BsonSerializer.SerializerRegistry, LinqProvider.V3);
180+
return filterDefinition.Render(collection.DocumentSerializer, BsonSerializer.SerializerRegistry, linqProvider);
175181
}
176182

177183
protected BsonDocument TranslateFindProjection<TDocument, TProjection>(
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
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 System.Linq;
18+
using FluentAssertions;
19+
using MongoDB.Driver.Core.Misc;
20+
using MongoDB.Driver.Core.TestHelpers.XunitExtensions;
21+
using MongoDB.Driver.Linq;
22+
using MongoDB.TestHelpers.XunitExtensions;
23+
using Xunit;
24+
25+
namespace MongoDB.Driver.Tests.Linq.Linq3Implementation.Translators.ExpressionToAggregationExpressionTranslators.MethodTranslators
26+
{
27+
public class IsNullOrWhiteSpaceMethodToAggregationExpressionTranslatorTests : Linq3IntegrationTest
28+
{
29+
[Theory]
30+
[ParameterAttributeData]
31+
public void Project_IsNullOrWhiteSpace_using_anonymous_class_should_return_expected_results(
32+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
33+
{
34+
var collection = CreateCollection(linqProvider);
35+
36+
var find = collection.Find("{}")
37+
.Project(x => new { R = string.IsNullOrWhiteSpace(x.S) })
38+
.SortBy(x => x.Id);
39+
40+
var projection = TranslateFindProjection(collection, find);
41+
if (linqProvider == LinqProvider.V2)
42+
{
43+
projection.Should().Be("{ S : 1, _id : 0 }"); // LINQ2 will execute part of the projection client side
44+
}
45+
else
46+
{
47+
RequireServer.Check().Supports(Feature.FindProjectionExpressions, Feature.TrimOperator);
48+
projection.Should().Be("{ R : { $or : [{ $eq : ['$S', null] }, { $eq : [{ $trim : { input : '$S' } }, ''] }] }, _id : 0 }");
49+
}
50+
51+
var results = find.ToList();
52+
results.Select(x => x.R).Should().Equal(true, true, true, true, false);
53+
}
54+
55+
[Theory]
56+
[ParameterAttributeData]
57+
public void Project_IsNullOrWhiteSpace_using_named_class_should_return_expected_results(
58+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
59+
{
60+
var collection = CreateCollection(linqProvider);
61+
62+
var find = collection.Find("{}")
63+
.Project(x => new Result { R = string.IsNullOrWhiteSpace(x.S) })
64+
.SortBy(x => x.Id);
65+
66+
var projection = TranslateFindProjection(collection, find);
67+
if (linqProvider == LinqProvider.V2)
68+
{
69+
projection.Should().Be("{ S : 1, _id : 0 }"); // LINQ2 will execute part of the projection client side
70+
}
71+
else
72+
{
73+
RequireServer.Check().Supports(Feature.FindProjectionExpressions, Feature.TrimOperator);
74+
projection.Should().Be("{ R : { $or : [{ $eq : ['$S', null] }, { $eq : [{ $trim : { input : '$S' } }, ''] }] }, _id : 0 }");
75+
}
76+
77+
var results = find.ToList();
78+
results.Select(x => x.R).Should().Equal(true, true, true, true, false);
79+
}
80+
81+
[Theory]
82+
[ParameterAttributeData]
83+
public void Select_IsNullOrWhiteSpace_using_scalar_result_should_return_expected_results(
84+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
85+
{
86+
var collection = CreateCollection(linqProvider);
87+
88+
var queryable = collection.AsQueryable()
89+
.OrderBy(x => x.Id)
90+
.Select(x => string.IsNullOrWhiteSpace(x.S));
91+
92+
if (linqProvider == LinqProvider.V2)
93+
{
94+
var exception = Record.Exception(() => Translate(collection, queryable));
95+
exception.Should().BeOfType<NotSupportedException>();
96+
}
97+
else
98+
{
99+
RequireServer.Check().Supports(Feature.TrimOperator);
100+
var stages = Translate(collection, queryable);
101+
AssertStages(
102+
stages,
103+
"{ $sort : { _id : 1 } }",
104+
"{ $project : { _v : { $or : [{ $eq : ['$S', null] }, { $eq : [{ $trim : { input : '$S' } }, ''] }] }, _id : 0 } }");
105+
106+
var results = queryable.ToList();
107+
results.Should().Equal(true, true, true, true, false);
108+
}
109+
}
110+
111+
[Theory]
112+
[ParameterAttributeData]
113+
public void Select_IsNullOrWhiteSpace_using_anonymous_class_should_return_expected_results(
114+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
115+
{
116+
var collection = CreateCollection(linqProvider);
117+
118+
var queryable = collection.AsQueryable()
119+
.OrderBy(x => x.Id)
120+
.Select(x => new { R = string.IsNullOrWhiteSpace(x.S) });
121+
122+
if (linqProvider == LinqProvider.V2)
123+
{
124+
var exception = Record.Exception(() => Translate(collection, queryable));
125+
exception.Should().BeOfType<NotSupportedException>();
126+
}
127+
else
128+
{
129+
RequireServer.Check().Supports(Feature.TrimOperator);
130+
var stages = Translate(collection, queryable);
131+
AssertStages(
132+
stages,
133+
"{ $sort : { _id : 1 } }",
134+
"{ $project : { R : { $or : [{ $eq : ['$S', null] }, { $eq : [{ $trim : { input : '$S' } }, ''] }] }, _id : 0 } }");
135+
136+
var results = queryable.ToList();
137+
results.Select(x => x.R).Should().Equal(true, true, true, true, false);
138+
}
139+
}
140+
141+
[Theory]
142+
[ParameterAttributeData]
143+
public void Select_IsNullOrWhiteSpace_using_named_class_should_return_expected_results(
144+
[Values(LinqProvider.V2, LinqProvider.V3)] LinqProvider linqProvider)
145+
{
146+
var collection = CreateCollection(linqProvider);
147+
148+
var queryable = collection.AsQueryable()
149+
.OrderBy(x => x.Id)
150+
.Select(x => new Result { R = string.IsNullOrWhiteSpace(x.S) });
151+
152+
if (linqProvider == LinqProvider.V2)
153+
{
154+
var exception = Record.Exception(() => Translate(collection, queryable));
155+
exception.Should().BeOfType<NotSupportedException>();
156+
}
157+
else
158+
{
159+
RequireServer.Check().Supports(Feature.TrimOperator);
160+
var stages = Translate(collection, queryable);
161+
AssertStages(
162+
stages,
163+
"{ $sort : { _id : 1 } }",
164+
"{ $project : { R : { $or : [{ $eq : ['$S', null] }, { $eq : [{ $trim : { input : '$S' } }, ''] }] }, _id : 0 } }");
165+
166+
var results = queryable.ToList();
167+
results.Select(x => x.R).Should().Equal(true, true, true, true, false);
168+
}
169+
}
170+
171+
private IMongoCollection<C> CreateCollection(LinqProvider linqProvider)
172+
{
173+
var collection = GetCollection<C>(linqProvider: linqProvider);
174+
CreateCollection(
175+
collection,
176+
new C { Id = 1, S = null },
177+
new C { Id = 2, S = "" },
178+
new C { Id = 3, S = " " },
179+
new C { Id = 4, S = " \t\r\n" },
180+
new C { Id = 5, S = "abc" });
181+
return collection;
182+
}
183+
184+
public class C
185+
{
186+
public int Id { get; set; }
187+
public string S { get; set; }
188+
}
189+
190+
public class Result
191+
{
192+
public bool R { get; set; }
193+
}
194+
}
195+
}

0 commit comments

Comments
 (0)