Skip to content

Commit f85fc87

Browse files
author
rstam
committed
Some changes to how LINQ queries containing ToLower/ToUpper string comparisons are implemented.
1 parent 995ec4f commit f85fc87

File tree

3 files changed

+101
-84
lines changed

3 files changed

+101
-84
lines changed

Driver/Linq/Translators/PredicateTranslator.cs

Lines changed: 59 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
using System;
1717
using System.Collections;
1818
using System.Collections.Generic;
19+
using System.Globalization;
1920
using System.Linq;
2021
using System.Linq.Expressions;
2122
using System.Text.RegularExpressions;
@@ -258,7 +259,7 @@ private IMongoQuery BuildComparisonQuery(BinaryExpression binaryExpression)
258259
return query;
259260
}
260261

261-
query = BuildStringCaseComparisonQuery(variableExpression, operatorType, constantExpression);
262+
query = BuildStringCaseInsensitiveComparisonQuery(variableExpression, operatorType, constantExpression);
262263
if (query != null)
263264
{
264265
return query;
@@ -1047,58 +1048,82 @@ private IMongoQuery BuildStringLengthQuery(Expression variableExpression, Expres
10471048
return null;
10481049
}
10491050

1050-
private IMongoQuery BuildStringCaseComparisonQuery(Expression variableExpression, ExpressionType operatorType, ConstantExpression constantExpression)
1051+
private IMongoQuery BuildStringCaseInsensitiveComparisonQuery(Expression variableExpression, ExpressionType operatorType, ConstantExpression constantExpression)
10511052
{
10521053
var methodExpression = variableExpression as MethodCallExpression;
10531054
if (methodExpression == null)
10541055
{
10551056
return null;
10561057
}
1057-
var sourceExpression = methodExpression.Object as MemberExpression;
10581058

1059-
if (methodExpression.Type != typeof(string) || sourceExpression == null)
1059+
var methodName = methodExpression.Method.Name;
1060+
if ((methodName != "ToLower" && methodName != "ToUpper") ||
1061+
methodExpression.Object == null ||
1062+
methodExpression.Type != typeof(string) ||
1063+
methodExpression.Arguments.Count != 0)
10601064
{
10611065
return null;
10621066
}
10631067

1064-
var serializationInfo = _serializationInfoHelper.GetSerializationInfo(methodExpression.Object);
1065-
var serializedValue = _serializationInfoHelper.SerializeValue(serializationInfo, constantExpression.Value);
1066-
var coalescedStringValue = constantExpression.Value == null ? string.Empty : serializedValue.AsString;
1068+
if (operatorType != ExpressionType.Equal && operatorType != ExpressionType.NotEqual)
1069+
{
1070+
return null;
1071+
}
10671072

1068-
string regexPattern = "/^" + Regex.Escape(coalescedStringValue) + "$/i";
1069-
var regex = new BsonRegularExpression(regexPattern);
1073+
var serializationInfo = _serializationInfoHelper.GetSerializationInfo(methodExpression.Object);
1074+
var serializedValue = _serializationInfoHelper.SerializeValue(serializationInfo, constantExpression.Value);
10701075

1071-
bool caseMismatch = false;
1076+
if (serializedValue.IsString)
1077+
{
1078+
var stringValue = serializedValue.AsString;
1079+
var stringValueCaseMatches =
1080+
methodName == "ToLower" && stringValue == stringValue.ToLower(CultureInfo.InvariantCulture) ||
1081+
methodName == "ToUpper" && stringValue == stringValue.ToUpper(CultureInfo.InvariantCulture);
10721082

1073-
if (methodExpression.Method.Name == "ToLower" && (coalescedStringValue != coalescedStringValue.ToLower()))
1074-
caseMismatch = true;
1075-
else if (methodExpression.Method.Name == "ToUpper" && (coalescedStringValue != coalescedStringValue.ToUpper()))
1076-
caseMismatch = true;
1077-
else if (constantExpression.Value == null)
1078-
caseMismatch = true;
1083+
if (stringValueCaseMatches)
1084+
{
1085+
string pattern = "/^" + Regex.Escape(stringValue) + "$/i";
1086+
var regex = new BsonRegularExpression(pattern);
10791087

1080-
if (operatorType == ExpressionType.Equal)
1088+
if (operatorType == ExpressionType.Equal)
1089+
{
1090+
return _queryBuilder.Matches(serializationInfo.ElementName, regex);
1091+
}
1092+
else
1093+
{
1094+
return _queryBuilder.Not(_queryBuilder.Matches(serializationInfo.ElementName, regex));
1095+
}
1096+
}
1097+
else
1098+
{
1099+
if (operatorType == ExpressionType.Equal)
1100+
{
1101+
// == "mismatched case" matches no documents
1102+
return _queryBuilder.NotExists("_id");
1103+
}
1104+
else
1105+
{
1106+
// != "mismatched case" matches all documents
1107+
return new QueryDocument();
1108+
}
1109+
}
1110+
}
1111+
else if (serializedValue.IsBsonNull)
10811112
{
1082-
// if comparing Foo.ToLower() == "Some Non Lower Case String"
1083-
// then that is always false for all documents
1084-
if (caseMismatch)
1085-
return Query.Exists("_id", false);
1086-
1087-
return Query.And(Query.Exists(serializationInfo.ElementName, true),
1088-
Query.Matches(serializationInfo.ElementName, regex));
1113+
if (operatorType == ExpressionType.Equal)
1114+
{
1115+
return _queryBuilder.EQ(serializationInfo.ElementName, BsonNull.Value);
1116+
}
1117+
else
1118+
{
1119+
return _queryBuilder.NE(serializationInfo.ElementName, BsonNull.Value);
1120+
}
10891121
}
1090-
else if (operatorType == ExpressionType.NotEqual)
1122+
else
10911123
{
1092-
// if comparing Foo.ToLower() != "Some Non Lower Case String"
1093-
// then that is always true as long as Foo is set/exists
1094-
if (caseMismatch)
1095-
return Query.Exists(serializationInfo.ElementName, true);
1096-
1097-
return Query.And(Query.Exists(serializationInfo.ElementName, true),
1098-
Query.Not(serializationInfo.ElementName).Matches(regex));
1124+
var message = string.Format("When using {0} in a LINQ string comparison the value being compared to must serialize as a string.", methodName);
1125+
throw new ArgumentException(message);
10991126
}
1100-
1101-
return null;
11021127
}
11031128

11041129
private IMongoQuery BuildStringQuery(MethodCallExpression methodCallExpression)

DriverUnitTests/Linq/SelectQueryTests.cs

Lines changed: 23 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -5579,13 +5579,12 @@ where c.S.ToLower() == "abc"
55795579

55805580
var selectQuery = (SelectQuery)translatedQuery;
55815581
Assert.AreEqual("(C c) => (c.S.ToLower() == \"abc\")", ExpressionFormatter.ToString(selectQuery.Where));
5582-
55835582
Assert.IsNull(selectQuery.OrderBy);
55845583
Assert.IsNull(selectQuery.Projection);
55855584
Assert.IsNull(selectQuery.Skip);
55865585
Assert.IsNull(selectQuery.Take);
55875586

5588-
Assert.AreEqual("{ \"$and\" : [{ \"s\" : { \"$exists\" : true } }, { \"s\" : /^abc$/i }] }", selectQuery.BuildQuery().ToJson());
5587+
Assert.AreEqual("{ \"s\" : /^abc$/i }", selectQuery.BuildQuery().ToJson());
55895588
Assert.AreEqual(1, Consume(query));
55905589
}
55915590

@@ -5603,14 +5602,13 @@ where c.S.ToLower() != "abc"
56035602

56045603
var selectQuery = (SelectQuery)translatedQuery;
56055604
Assert.AreEqual("(C c) => (c.S.ToLower() != \"abc\")", ExpressionFormatter.ToString(selectQuery.Where));
5606-
56075605
Assert.IsNull(selectQuery.OrderBy);
56085606
Assert.IsNull(selectQuery.Projection);
56095607
Assert.IsNull(selectQuery.Skip);
56105608
Assert.IsNull(selectQuery.Take);
56115609

5612-
Assert.AreEqual("{ \"s\" : { \"$exists\" : true, \"$not\" : /^abc$/i } }", selectQuery.BuildQuery().ToJson());
5613-
Assert.AreEqual(1, Consume(query));
5610+
Assert.AreEqual("{ \"s\" : { \"$not\" : /^abc$/i } }", selectQuery.BuildQuery().ToJson());
5611+
Assert.AreEqual(4, Consume(query));
56145612
}
56155613

56165614
[Test]
@@ -5627,7 +5625,6 @@ where c.S.ToLower() == "Abc"
56275625

56285626
var selectQuery = (SelectQuery)translatedQuery;
56295627
Assert.AreEqual("(C c) => (c.S.ToLower() == \"Abc\")", ExpressionFormatter.ToString(selectQuery.Where));
5630-
56315628
Assert.IsNull(selectQuery.OrderBy);
56325629
Assert.IsNull(selectQuery.Projection);
56335630
Assert.IsNull(selectQuery.Skip);
@@ -5651,21 +5648,20 @@ where c.S.ToLower() != "Abc"
56515648

56525649
var selectQuery = (SelectQuery)translatedQuery;
56535650
Assert.AreEqual("(C c) => (c.S.ToLower() != \"Abc\")", ExpressionFormatter.ToString(selectQuery.Where));
5654-
56555651
Assert.IsNull(selectQuery.OrderBy);
56565652
Assert.IsNull(selectQuery.Projection);
56575653
Assert.IsNull(selectQuery.Skip);
56585654
Assert.IsNull(selectQuery.Take);
56595655

5660-
Assert.AreEqual("{ \"s\" : { \"$exists\" : true } }", selectQuery.BuildQuery().ToJson());
5661-
Assert.AreEqual(2, Consume(query));
5656+
Assert.AreEqual("{ }", selectQuery.BuildQuery().ToJson());
5657+
Assert.AreEqual(5, Consume(query));
56625658
}
56635659

56645660
[Test]
56655661
public void TestWhereSToLowerEqualsNullValue()
56665662
{
56675663
var query = from c in _collection.AsQueryable<C>()
5668-
where c.S.ToLower() == (string)null
5664+
where c.S.ToLower() == null
56695665
select c;
56705666

56715667
var translatedQuery = MongoQueryTranslator.Translate(query);
@@ -5674,21 +5670,21 @@ where c.S.ToLower() == (string)null
56745670
Assert.AreSame(typeof(C), translatedQuery.DocumentType);
56755671

56765672
var selectQuery = (SelectQuery)translatedQuery;
5677-
5673+
Assert.AreEqual("(C c) => (c.S.ToLower() == null)", ExpressionFormatter.ToString(selectQuery.Where));
56785674
Assert.IsNull(selectQuery.OrderBy);
56795675
Assert.IsNull(selectQuery.Projection);
56805676
Assert.IsNull(selectQuery.Skip);
56815677
Assert.IsNull(selectQuery.Take);
56825678

5683-
Assert.AreEqual("{ \"_id\" : { \"$exists\" : false } }", selectQuery.BuildQuery().ToJson());
5684-
Assert.AreEqual(0, Consume(query));
5679+
Assert.AreEqual("{ \"s\" : null }", selectQuery.BuildQuery().ToJson());
5680+
Assert.AreEqual(3, Consume(query));
56855681
}
56865682

56875683
[Test]
56885684
public void TestWhereSToLowerDoesNotEqualNullValue()
56895685
{
56905686
var query = from c in _collection.AsQueryable<C>()
5691-
where c.S.ToLower() != (string)null
5687+
where c.S.ToLower() != null
56925688
select c;
56935689

56945690
var translatedQuery = MongoQueryTranslator.Translate(query);
@@ -5697,13 +5693,13 @@ where c.S.ToLower() != (string)null
56975693
Assert.AreSame(typeof(C), translatedQuery.DocumentType);
56985694

56995695
var selectQuery = (SelectQuery)translatedQuery;
5700-
5696+
Assert.AreEqual("(C c) => (c.S.ToLower() != null)", ExpressionFormatter.ToString(selectQuery.Where));
57015697
Assert.IsNull(selectQuery.OrderBy);
57025698
Assert.IsNull(selectQuery.Projection);
57035699
Assert.IsNull(selectQuery.Skip);
57045700
Assert.IsNull(selectQuery.Take);
57055701

5706-
Assert.AreEqual("{ \"s\" : { \"$exists\" : true } }", selectQuery.BuildQuery().ToJson());
5702+
Assert.AreEqual("{ \"s\" : { \"$ne\" : null } }", selectQuery.BuildQuery().ToJson());
57075703
Assert.AreEqual(2, Consume(query));
57085704
}
57095705

@@ -5721,7 +5717,6 @@ where c.S.ToUpper() == "abc"
57215717

57225718
var selectQuery = (SelectQuery)translatedQuery;
57235719
Assert.AreEqual("(C c) => (c.S.ToUpper() == \"abc\")", ExpressionFormatter.ToString(selectQuery.Where));
5724-
57255720
Assert.IsNull(selectQuery.OrderBy);
57265721
Assert.IsNull(selectQuery.Projection);
57275722
Assert.IsNull(selectQuery.Skip);
@@ -5745,14 +5740,13 @@ where c.S.ToUpper() != "abc"
57455740

57465741
var selectQuery = (SelectQuery)translatedQuery;
57475742
Assert.AreEqual("(C c) => (c.S.ToUpper() != \"abc\")", ExpressionFormatter.ToString(selectQuery.Where));
5748-
57495743
Assert.IsNull(selectQuery.OrderBy);
57505744
Assert.IsNull(selectQuery.Projection);
57515745
Assert.IsNull(selectQuery.Skip);
57525746
Assert.IsNull(selectQuery.Take);
57535747

5754-
Assert.AreEqual("{ \"s\" : { \"$exists\" : true } }", selectQuery.BuildQuery().ToJson());
5755-
Assert.AreEqual(2, Consume(query));
5748+
Assert.AreEqual("{ }", selectQuery.BuildQuery().ToJson());
5749+
Assert.AreEqual(5, Consume(query));
57565750
}
57575751

57585752
[Test]
@@ -5769,7 +5763,6 @@ where c.S.ToUpper() == "Abc"
57695763

57705764
var selectQuery = (SelectQuery)translatedQuery;
57715765
Assert.AreEqual("(C c) => (c.S.ToUpper() == \"Abc\")", ExpressionFormatter.ToString(selectQuery.Where));
5772-
57735766
Assert.IsNull(selectQuery.OrderBy);
57745767
Assert.IsNull(selectQuery.Projection);
57755768
Assert.IsNull(selectQuery.Skip);
@@ -5793,21 +5786,20 @@ where c.S.ToUpper() != "Abc"
57935786

57945787
var selectQuery = (SelectQuery)translatedQuery;
57955788
Assert.AreEqual("(C c) => (c.S.ToUpper() != \"Abc\")", ExpressionFormatter.ToString(selectQuery.Where));
5796-
57975789
Assert.IsNull(selectQuery.OrderBy);
57985790
Assert.IsNull(selectQuery.Projection);
57995791
Assert.IsNull(selectQuery.Skip);
58005792
Assert.IsNull(selectQuery.Take);
58015793

5802-
Assert.AreEqual("{ \"s\" : { \"$exists\" : true } }", selectQuery.BuildQuery().ToJson());
5803-
Assert.AreEqual(2, Consume(query));
5794+
Assert.AreEqual("{ }", selectQuery.BuildQuery().ToJson());
5795+
Assert.AreEqual(5, Consume(query));
58045796
}
58055797

58065798
[Test]
58075799
public void TestWhereSToUpperEqualsNullValue()
58085800
{
58095801
var query = from c in _collection.AsQueryable<C>()
5810-
where c.S.ToUpper() == (string)null
5802+
where c.S.ToUpper() == null
58115803
select c;
58125804

58135805
var translatedQuery = MongoQueryTranslator.Translate(query);
@@ -5816,21 +5808,21 @@ where c.S.ToUpper() == (string)null
58165808
Assert.AreSame(typeof(C), translatedQuery.DocumentType);
58175809

58185810
var selectQuery = (SelectQuery)translatedQuery;
5819-
5811+
Assert.AreEqual("(C c) => (c.S.ToUpper() == null)", ExpressionFormatter.ToString(selectQuery.Where));
58205812
Assert.IsNull(selectQuery.OrderBy);
58215813
Assert.IsNull(selectQuery.Projection);
58225814
Assert.IsNull(selectQuery.Skip);
58235815
Assert.IsNull(selectQuery.Take);
58245816

5825-
Assert.AreEqual("{ \"_id\" : { \"$exists\" : false } }", selectQuery.BuildQuery().ToJson());
5826-
Assert.AreEqual(0, Consume(query));
5817+
Assert.AreEqual("{ \"s\" : null }", selectQuery.BuildQuery().ToJson());
5818+
Assert.AreEqual(3, Consume(query));
58275819
}
58285820

58295821
[Test]
58305822
public void TestWhereSToUpperDoesNotEqualNullValue()
58315823
{
58325824
var query = from c in _collection.AsQueryable<C>()
5833-
where c.S.ToUpper() != (string)null
5825+
where c.S.ToUpper() != null
58345826
select c;
58355827

58365828
var translatedQuery = MongoQueryTranslator.Translate(query);
@@ -5839,13 +5831,13 @@ where c.S.ToUpper() != (string)null
58395831
Assert.AreSame(typeof(C), translatedQuery.DocumentType);
58405832

58415833
var selectQuery = (SelectQuery)translatedQuery;
5842-
5834+
Assert.AreEqual("(C c) => (c.S.ToUpper() != null)", ExpressionFormatter.ToString(selectQuery.Where));
58435835
Assert.IsNull(selectQuery.OrderBy);
58445836
Assert.IsNull(selectQuery.Projection);
58455837
Assert.IsNull(selectQuery.Skip);
58465838
Assert.IsNull(selectQuery.Take);
58475839

5848-
Assert.AreEqual("{ \"s\" : { \"$exists\" : true } }", selectQuery.BuildQuery().ToJson());
5840+
Assert.AreEqual("{ \"s\" : { \"$ne\" : null } }", selectQuery.BuildQuery().ToJson());
58495841
Assert.AreEqual(2, Consume(query));
58505842
}
58515843

0 commit comments

Comments
 (0)