From 4d02ca68d8a9379cd57b38cafe5cfbe68b993662 Mon Sep 17 00:00:00 2001 From: Alex Skalozub Date: Wed, 27 Jul 2016 10:15:29 +0300 Subject: [PATCH 01/10] Changes in string functions: 1) Overload for Substring() with single arg now works 2) Overload for Equals() with single arg now works 3) Also added static version of String.Equals() 4) Added some of the overloads for String.Concat() 5) More checks for correct number of arguments --- .../AggregateLanguageTranslator.cs | 173 ++++++++++++------ 1 file changed, 120 insertions(+), 53 deletions(-) diff --git a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs index f01fb4a99ff..cd16913f054 100644 --- a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs +++ b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs @@ -86,6 +86,8 @@ private BsonValue TranslateValue(Expression node) return TranslateOperation((BinaryExpression)node, "$multiply", true); case ExpressionType.New: return TranslateNew((NewExpression)node); + case ExpressionType.NewArrayInit: + return TranslateNewArrayInit((NewArrayExpression)node); case ExpressionType.Not: return TranslateNot((UnaryExpression)node); case ExpressionType.NotEqual: @@ -389,6 +391,18 @@ private BsonValue TranslateNew(NewExpression node) return TranslateMapping(mapping); } + private BsonValue TranslateNewArrayInit(NewArrayExpression node) + { + var nodes = new BsonArray(); + + foreach (var expr in node.Expressions) + { + nodes.Add(TranslateValue(expr)); + } + + return nodes; + } + private BsonValue TranslateMapping(ProjectionMapping mapping) { BsonDocument doc = new BsonDocument(); @@ -774,6 +788,8 @@ private bool TryTranslateMinResultOperator(PipelineExpression node, out BsonValu private bool TryTranslateStaticMathMethodCall(MethodCallExpression node, out BsonValue result) { result = null; + if (node.Arguments.Count == 0) return false; + switch (node.Method.Name) { case "Abs": @@ -835,81 +851,132 @@ private bool TryTranslateStaticMathMethodCall(MethodCallExpression node, out Bso return false; } - private bool TryTranslateStaticStringMethodCall(MethodCallExpression node, out BsonValue result) + private static BsonValue StringEqualsHelper(BsonValue a, BsonValue b, Expression stringComparisonExpr) { - result = null; - switch (node.Method.Name) + StringComparison comparisonType = StringComparison.Ordinal; + + if (stringComparisonExpr != null && stringComparisonExpr.NodeType == ExpressionType.Constant) + comparisonType = (StringComparison)((ConstantExpression)stringComparisonExpr).Value; + + switch (comparisonType) { - case "IsNullOrEmpty": - var field = TranslateValue(node.Arguments[0]); - result = new BsonDocument("$or", - new BsonArray - { - new BsonDocument("$eq", new BsonArray { field, BsonNull.Value }), - new BsonDocument("$eq", new BsonArray { field, BsonString.Empty }) - }); - return true; - } + case StringComparison.OrdinalIgnoreCase: + // $strcasecmp returns -1/0/1, so additional operation is needed: + return new BsonDocument("$eq", new BsonArray(new[] + { + new BsonDocument("$strcasecmp", new BsonArray(new[] { a, b })), + (BsonValue)0 + })); - return false; + case StringComparison.Ordinal: + return new BsonDocument("$eq", new BsonArray(new[] { a, b })); + + default: + throw new NotSupportedException("Only Ordinal and OrdinalIgnoreCase are supported for string comparisons."); + } } - private bool TryTranslateStringMethodCall(MethodCallExpression node, out BsonValue result) + private bool TryTranslateStaticStringMethodCall(MethodCallExpression node, out BsonValue result) { result = null; - var field = TranslateValue(node.Object); + if (node.Arguments.Count == 0) return false; + + var field = TranslateValue(node.Arguments[0]); + switch (node.Method.Name) { - case "Equals": - if (node.Arguments.Count == 2 && node.Arguments[1].NodeType == ExpressionType.Constant) + case "IsNullOrEmpty": + if (node.Arguments.Count == 1) { - var comparisonType = (StringComparison)((ConstantExpression)node.Arguments[1]).Value; - switch (comparisonType) - { - case StringComparison.OrdinalIgnoreCase: - result = new BsonDocument("$eq", - new BsonArray(new BsonValue[] - { - new BsonDocument("$strcasecmp", new BsonArray(new[] { field, TranslateValue(node.Arguments[0]) })), - 0 - })); - return true; - case StringComparison.Ordinal: - result = new BsonDocument("$eq", new BsonArray(new[] { field, TranslateValue(node.Arguments[0]) })); - return true; - default: - throw new NotSupportedException("Only Ordinal and OrdinalIgnoreCase are supported for string comparisons."); - } + result = new BsonDocument("$or", + new BsonArray + { + new BsonDocument("$eq", new BsonArray { field, BsonNull.Value }), + new BsonDocument("$eq", new BsonArray { field, BsonString.Empty }) + }); + return true; } break; - case "Substring": - if (node.Arguments.Count == 2) + + case "Equals": + if (node.Arguments.Count >= 2 && node.Arguments.Count <= 3) { - result = new BsonDocument("$substr", new BsonArray(new[] - { - field, - TranslateValue(node.Arguments[0]), - TranslateValue(node.Arguments[1]) - })); + result = StringEqualsHelper(field, TranslateValue(node.Arguments[1]), + (node.Arguments.Count == 3) ? node.Arguments[2] : null); return true; } break; - case "ToLower": - case "ToLowerInvariant": - if (node.Arguments.Count == 0) + + case "Concat": + if (node.Arguments[0].Type == typeof(string[])) { - result = new BsonDocument("$toLower", field); + // this is .Concat(params string[] args) overload + result = new BsonDocument("$concat", field); return true; } - break; - case "ToUpper": - case "ToUpperInvariant": - if (node.Arguments.Count == 0) + else if (node.Arguments[0].Type == typeof(string)) { - result = new BsonDocument("$toUpper", field); + // this is one of .Concat(string [, string, [string, [string]]]) overloads + var array = new BsonArray(); + for (int i = 0; i < node.Arguments.Count; i++) + { + array.Add(TranslateValue(node.Arguments[i])); + } + + result = new BsonDocument("$concat", array); return true; } - break; + else + throw new NotSupportedException("Some overloads of String.Concat() are not supported"); + + } + + return false; + } + + private bool TryTranslateStringMethodCall(MethodCallExpression node, out BsonValue result) + { + result = null; + var field = TranslateValue(node.Object); + + if (node.Arguments.Count == 0) + { + // parameterless methods + + switch (node.Method.Name) + { + case "ToLower": + case "ToLowerInvariant": + result = new BsonDocument("$toLower", field); + return true; + + case "ToUpper": + case "ToUpperInvariant": + result = new BsonDocument("$toUpper", field); + return true; + } + } + else if (node.Arguments.Count <= 2) + { + // methods with 1 or 2 parameters + var arg0 = TranslateValue(node.Arguments[0]); + + switch (node.Method.Name) + { + case "Equals": + result = StringEqualsHelper(field, arg0, + (node.Arguments.Count == 2) ? node.Arguments[1] : null); + return true; + + case "Substring": + result = new BsonDocument("$substr", new BsonArray(new[] + { + field, + arg0, + (node.Arguments.Count == 2) ? TranslateValue(node.Arguments[1]) : -1 + })); + return true; + } } return false; From 8acf7ed888c754732a28318312396c64051adc5c Mon Sep 17 00:00:00 2001 From: Alex Skalozub Date: Wed, 27 Jul 2016 10:16:25 +0300 Subject: [PATCH 02/10] Added negation unary operator --- .../Linq/Translators/AggregateLanguageTranslator.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs index cd16913f054..ea68d09eb86 100644 --- a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs +++ b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs @@ -84,6 +84,9 @@ private BsonValue TranslateValue(Expression node) case ExpressionType.Multiply: case ExpressionType.MultiplyChecked: return TranslateOperation((BinaryExpression)node, "$multiply", true); + case ExpressionType.Negate: + case ExpressionType.NegateChecked: + return TranslateNegate((UnaryExpression)node); case ExpressionType.New: return TranslateNew((NewExpression)node); case ExpressionType.NewArrayInit: @@ -391,6 +394,12 @@ private BsonValue TranslateNew(NewExpression node) return TranslateMapping(mapping); } + private BsonValue TranslateNegate(UnaryExpression node) + { + var operand = TranslateValue(node.Operand); + return new BsonDocument("$subtract", new BsonArray(new BsonValue[] { 0, operand })); + } + private BsonValue TranslateNewArrayInit(NewArrayExpression node) { var nodes = new BsonArray(); From 958fe91a9600ed35e97fe02dd35a5c4f0efd6a06 Mon Sep 17 00:00:00 2001 From: Alex Skalozub Date: Wed, 27 Jul 2016 10:19:17 +0300 Subject: [PATCH 03/10] Fixed exception when aggregating fields of child document --- .../AggregateLanguageTranslator.cs | 28 +++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs index ea68d09eb86..581eafc0cc3 100644 --- a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs +++ b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs @@ -321,6 +321,34 @@ private BsonValue TranslateMemberAccess(MemberExpression node) return new BsonDocument("$size", TranslateValue(node.Expression)); } + // + // Consider a collection of documents like this: + // { + // _id: ..., + // CustomerId: ..., + // Order: { + // Qty: 5, + // Price: 15 + // } + // } + // + // And then we're aggregating customers and calculating total price: + // $group: { + // _id: "$CustomerId", + // TotalQty: { $sum: "$Order.Qty" }, + // TotalPrice: { $sum: "$Order.Price" } + // } + // + // This is exactly the place when we get here with member access. + // + + result = TranslateValue(node.Expression); + if (result.BsonType == BsonType.String && result.AsString.StartsWith("$")) + { + // this looks like variable access expression + return BsonValue.Create(result.AsString + "." + node.Member.Name); + } + var message = string.Format("Member {0} of type {1} in the expression tree {2} cannot be translated.", node.Member.Name, node.Member.DeclaringType, From 91eab71d3e73eb784b6b03e6be0a5a53a54715a2 Mon Sep 17 00:00:00 2001 From: Alex Skalozub Date: Wed, 27 Jul 2016 10:31:46 +0300 Subject: [PATCH 04/10] More support for dates: 1) Added DateTime.ToString() (via $dateToString) 2) Added support of AddDays(), AddHours(), AddMinutes(), AddSeconds() and AddMilliseconds() operations AddYears() and AddMonths() are currently not implemented to not overcomplicate requests, while Add() and Subtract() are not implemented because TimeSpan is not convertible to BsonValue --- .../AggregateLanguageTranslator.cs | 134 ++++++++++++++++-- 1 file changed, 124 insertions(+), 10 deletions(-) diff --git a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs index 581eafc0cc3..b16920a13ca 100644 --- a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs +++ b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs @@ -382,6 +382,12 @@ private BsonValue TranslateMethodCall(MethodCallExpression node) return result; } + if (node.Object.Type == typeof(DateTime) + && TryTranslateDateTimeMethodCall(node, out result)) + { + return result; + } + if (node.Object.Type.GetTypeInfo().IsGenericType && node.Object.Type.GetGenericTypeDefinition() == typeof(HashSet<>) && TryTranslateHashSetMethodCall(node, out result)) @@ -697,46 +703,154 @@ private bool TryTranslateCountResultOperator(PipelineExpression node, out BsonVa private bool TryTranslateDateTimeMemberAccess(MemberExpression node, out BsonValue result) { result = null; - var field = TranslateValue(node.Expression); + var date = TranslateValue(node.Expression); + switch (node.Member.Name) { case "Day": - result = new BsonDocument("$dayOfMonth", field); + result = new BsonDocument("$dayOfMonth", date); return true; case "DayOfWeek": // The server's day of week values are 1 greater than // .NET's DayOfWeek enum values result = new BsonDocument("$subtract", new BsonArray { - new BsonDocument("$dayOfWeek", field), + new BsonDocument("$dayOfWeek", date), (BsonInt32)1 }); return true; case "DayOfYear": - result = new BsonDocument("$dayOfYear", field); + result = new BsonDocument("$dayOfYear", date); return true; case "Hour": - result = new BsonDocument("$hour", field); + result = new BsonDocument("$hour", date); return true; case "Millisecond": - result = new BsonDocument("$millisecond", field); + result = new BsonDocument("$millisecond", date); return true; case "Minute": - result = new BsonDocument("$minute", field); + result = new BsonDocument("$minute", date); return true; case "Month": - result = new BsonDocument("$month", field); + result = new BsonDocument("$month", date); return true; case "Second": - result = new BsonDocument("$second", field); + result = new BsonDocument("$second", date); return true; case "Year": - result = new BsonDocument("$year", field); + result = new BsonDocument("$year", date); return true; } return false; } + + private bool TryTranslateDateTimeAddXXX(MethodCallExpression node, long multiplier, out BsonValue result) + { + result = null; + if (node.Arguments.Count != 1) return false; + + var date = TranslateValue(node.Object); + BsonValue arg; + + var expr = node.Arguments[0]; + if (expr.NodeType == ExpressionType.Constant) + { + // argument value is constant + // no need to multiply on server, do it right away + + double value = (double)((ConstantExpression)expr).Value; + + // trim to long + long value_ms = (long)Math.Round(value * multiplier); + + arg = BsonValue.Create(value_ms); + } + else + { + // value derived from another expression + // will be calculated on the server + + arg = TranslateValue(expr); + + if (multiplier != 1) + { + arg = new BsonDocument("$multiply", new BsonArray(new[] { multiplier, arg })); + } + } + + result = new BsonDocument("$add", new BsonArray(new[] { date, arg })); + return true; + } + + private bool TryTranslateDateTimeToString(MethodCallExpression node, out BsonValue result) + { + result = null; + if (node.Arguments.Count > 2) return false; + + var date = TranslateValue(node.Object); + + // assume default format YYYY-MM-DD HH:mm + BsonValue format = "%Y-%m-%d %H:%M"; + + if (node.Arguments.Count == 1) + { + // can be either .ToString(IFormatProvider) or .ToString(string) overload + // need to check argument type to be sure + + var arg0 = node.Arguments[0]; + if (arg0.Type == typeof(string)) + { + format = TranslateValue(arg0); + } + } + else if (node.Arguments.Count == 2) + { + // this is .ToString(string, IFormatProvider), format always goes first + format = TranslateValue(node.Arguments[0]); + } + + result = new BsonDocument("$dateToString", new BsonDocument().Add("format", format).Add("date", date)); + return true; + } + + private bool TryTranslateDateTimeMethodCall(MethodCallExpression node, out BsonValue result) + { + switch (node.Method.Name) + { + case "ToString": + // $dateToString + return TryTranslateDateTimeToString(node, out result); + + case "AddYears": + case "AddMonths": + // adding years/months has potential issues with leap years, + // so don't bother with them for now + throw new NotSupportedException("Adding months/years is not supported"); + + case "AddDays": + return TryTranslateDateTimeAddXXX(node, 24 * 60 * 60 * 1000, out result); + case "AddHours": + return TryTranslateDateTimeAddXXX(node, 60 * 60 * 1000, out result); + case "AddMinutes": + return TryTranslateDateTimeAddXXX(node, 60 * 1000, out result); + case "AddSeconds": + return TryTranslateDateTimeAddXXX(node, 1000, out result); + case "AddMilliseconds": + return TryTranslateDateTimeAddXXX(node, 1, out result); + + case "Add": + case "Subtract": + // TimeSpan is not mapped into BsonValue, + // so don't bother with it for now + break; + } + + // other methods are not supported + result = null; + return false; + } + private bool TryTranslateFirstResultOperator(PipelineExpression node, out BsonValue result) { From 2f82da5d9aff1e09c4051437a3a8759ec6524cc9 Mon Sep 17 00:00:00 2001 From: Alex Skalozub Date: Wed, 27 Jul 2016 20:57:51 +0300 Subject: [PATCH 05/10] Code style fixes --- .../AggregateLanguageTranslator.cs | 60 ++++++++++++------- 1 file changed, 39 insertions(+), 21 deletions(-) diff --git a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs index b16920a13ca..98dc36bf2a6 100644 --- a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs +++ b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs @@ -748,7 +748,11 @@ private bool TryTranslateDateTimeMemberAccess(MemberExpression node, out BsonVal private bool TryTranslateDateTimeAddXXX(MethodCallExpression node, long multiplier, out BsonValue result) { result = null; - if (node.Arguments.Count != 1) return false; + if (node.Arguments.Count != 1) + { + // All DateTime.AddXXX() functions take single argument + return false; + } var date = TranslateValue(node.Object); BsonValue arg; @@ -758,7 +762,6 @@ private bool TryTranslateDateTimeAddXXX(MethodCallExpression node, long multipli { // argument value is constant // no need to multiply on server, do it right away - double value = (double)((ConstantExpression)expr).Value; // trim to long @@ -770,7 +773,6 @@ private bool TryTranslateDateTimeAddXXX(MethodCallExpression node, long multipli { // value derived from another expression // will be calculated on the server - arg = TranslateValue(expr); if (multiplier != 1) @@ -786,7 +788,11 @@ private bool TryTranslateDateTimeAddXXX(MethodCallExpression node, long multipli private bool TryTranslateDateTimeToString(MethodCallExpression node, out BsonValue result) { result = null; - if (node.Arguments.Count > 2) return false; + if (node.Arguments.Count > 2) + { + // No overload for DateTime.ToString() takes more than 2 args + return false; + } var date = TranslateValue(node.Object); @@ -797,11 +803,9 @@ private bool TryTranslateDateTimeToString(MethodCallExpression node, out BsonVal { // can be either .ToString(IFormatProvider) or .ToString(string) overload // need to check argument type to be sure - - var arg0 = node.Arguments[0]; - if (arg0.Type == typeof(string)) + if (node.Arguments[0].Type == typeof(string)) { - format = TranslateValue(arg0); + format = TranslateValue(node.Arguments[0]); } } else if (node.Arguments.Count == 2) @@ -939,31 +943,37 @@ private bool TryTranslateMinResultOperator(PipelineExpression node, out BsonValu private bool TryTranslateStaticMathMethodCall(MethodCallExpression node, out BsonValue result) { result = null; - if (node.Arguments.Count == 0) return false; + if (node.Arguments.Count == 0) + { + // None of these methods take no args + return false; + } + + var field = TranslateValue(node.Arguments[0]); switch (node.Method.Name) { case "Abs": - result = new BsonDocument("$abs", TranslateValue(node.Arguments[0])); + result = new BsonDocument("$abs", field); return true; case "Ceiling": - result = new BsonDocument("$ceil", TranslateValue(node.Arguments[0])); + result = new BsonDocument("$ceil", field); return true; case "Exp": result = new BsonDocument("$exp", new BsonArray { - TranslateValue(node.Arguments[0]) + field }); return true; case "Floor": - result = new BsonDocument("$floor", TranslateValue(node.Arguments[0])); + result = new BsonDocument("$floor", field); return true; case "Log": if (node.Arguments.Count == 2) { result = new BsonDocument("$log", new BsonArray { - TranslateValue(node.Arguments[0]), + field, TranslateValue(node.Arguments[1]) }); } @@ -971,43 +981,46 @@ private bool TryTranslateStaticMathMethodCall(MethodCallExpression node, out Bso { result = new BsonDocument("$ln", new BsonArray { - TranslateValue(node.Arguments[0]) + field }); } return true; case "Log10": result = new BsonDocument("$log10", new BsonArray { - TranslateValue(node.Arguments[0]) + field }); return true; case "Pow": result = new BsonDocument("$pow", new BsonArray { - TranslateValue(node.Arguments[0]), + field, TranslateValue(node.Arguments[1]) }); return true; case "Sqrt": result = new BsonDocument("$sqrt", new BsonArray { - TranslateValue(node.Arguments[0]) + field }); return true; case "Truncate": - result = new BsonDocument("$trunc", TranslateValue(node.Arguments[0])); + result = new BsonDocument("$trunc", field); return true; } return false; } - private static BsonValue StringEqualsHelper(BsonValue a, BsonValue b, Expression stringComparisonExpr) + private BsonValue StringEqualsHelper(BsonValue a, BsonValue b, Expression stringComparisonExpr) { StringComparison comparisonType = StringComparison.Ordinal; if (stringComparisonExpr != null && stringComparisonExpr.NodeType == ExpressionType.Constant) + { + // has comparison type specified comparisonType = (StringComparison)((ConstantExpression)stringComparisonExpr).Value; + } switch (comparisonType) { @@ -1030,7 +1043,11 @@ private static BsonValue StringEqualsHelper(BsonValue a, BsonValue b, Expression private bool TryTranslateStaticStringMethodCall(MethodCallExpression node, out BsonValue result) { result = null; - if (node.Arguments.Count == 0) return false; + if (node.Arguments.Count == 0) + { + // None of these methods take no args + return false; + } var field = TranslateValue(node.Arguments[0]); @@ -1110,6 +1127,7 @@ private bool TryTranslateStringMethodCall(MethodCallExpression node, out BsonVal else if (node.Arguments.Count <= 2) { // methods with 1 or 2 parameters + var arg0 = TranslateValue(node.Arguments[0]); switch (node.Method.Name) From a97ee2794c9f2feb3b0565d3fcc2440a73b49fb2 Mon Sep 17 00:00:00 2001 From: Alex Skalozub Date: Wed, 27 Jul 2016 21:41:16 +0300 Subject: [PATCH 06/10] Check for correct number of arguments for Pow --- .../Translators/AggregateLanguageTranslator.cs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs index 98dc36bf2a6..f3842f4a041 100644 --- a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs +++ b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs @@ -992,12 +992,16 @@ private bool TryTranslateStaticMathMethodCall(MethodCallExpression node, out Bso }); return true; case "Pow": - result = new BsonDocument("$pow", new BsonArray + if (node.Arguments.Count == 2) { - field, - TranslateValue(node.Arguments[1]) - }); - return true; + result = new BsonDocument("$pow", new BsonArray + { + field, + TranslateValue(node.Arguments[1]) + }); + return true; + } + break; case "Sqrt": result = new BsonDocument("$sqrt", new BsonArray { From 9aea2e65f7b1646add1e832beb452cfdbd7d6a30 Mon Sep 17 00:00:00 2001 From: Alexey Skalozub Date: Thu, 28 Jul 2016 00:50:03 +0300 Subject: [PATCH 07/10] Added unit tests, more accurate parameters checking in String.Concat() translator --- .../AggregateLanguageTranslator.cs | 8 +- .../AggregateProjectTranslatorTests.cs | 241 +++++++++++++++++- 2 files changed, 244 insertions(+), 5 deletions(-) diff --git a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs index f3842f4a041..bc7bbd7116b 100644 --- a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs +++ b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs @@ -25,6 +25,7 @@ using MongoDB.Driver.Linq.Expressions.ResultOperators; using MongoDB.Driver.Linq.Processors; using MongoDB.Driver.Support; +using System.Linq; namespace MongoDB.Driver.Linq.Translators { @@ -1080,15 +1081,16 @@ private bool TryTranslateStaticStringMethodCall(MethodCallExpression node, out B break; case "Concat": - if (node.Arguments[0].Type == typeof(string[])) + if (node.Arguments.Count == 1 && node.Arguments[0].Type == typeof(string[])) { // this is .Concat(params string[] args) overload result = new BsonDocument("$concat", field); return true; } - else if (node.Arguments[0].Type == typeof(string)) + else if (node.Arguments.Count >= 2 && node.Arguments.Count <= 4 && + node.Arguments.All(p => p.Type == typeof(string))) { - // this is one of .Concat(string [, string, [string, [string]]]) overloads + // this is one of .Concat(string, string[, string[, string]]) overloads var array = new BsonArray(); for (int i = 0; i < node.Arguments.Count; i++) { diff --git a/tests/MongoDB.Driver.Tests/Linq/Translators/AggregateProjectTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Translators/AggregateProjectTranslatorTests.cs index 9b66a5e81fb..3e18a142860 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Translators/AggregateProjectTranslatorTests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Translators/AggregateProjectTranslatorTests.cs @@ -27,6 +27,7 @@ using MongoDB.Driver.Linq; using MongoDB.Driver.Linq.Translators; using Xunit; +using System.Globalization; namespace MongoDB.Driver.Tests.Linq.Translators { @@ -442,6 +443,82 @@ public void Should_translate_day_of_year() result.Value.Result.Should().Be(336); } + [Fact] + public void Should_translate_datetime_add_xxx() + { + var result = Project(x => new { J = x.J, Result = x.J.AddDays(2.5) }); + + result.Projection.Should().Be("{ J: \"$J\", Result: { \"$add\": [\"$J\", NumberLong(216000000)] }, _id: 0 }"); + + result.Value.Result.Should().Be(result.Value.J.AddDays(2.5)); + } + + [Fact] + public void Should_translate_datetime_add_xxx_negative() + { + var result = Project(x => new { J = x.J, Result = x.J.AddMinutes(-3) }); + + result.Projection.Should().Be("{ J: \"$J\", Result: { \"$add\": [\"$J\", NumberLong(-180000)] }, _id: 0 }"); + + result.Value.Result.Should().Be(result.Value.J.AddMinutes(-3)); + } + + [Fact] + public void Should_translate_datetime_add_xxx_from_variable() + { + var result = Project(x => new { J = x.J, N = x.C.E.F, Result = x.J.AddHours(x.C.E.F) }); + + result.Projection.Should().Be("{ J: \"$J\", N: \"$C.E.F\", Result: { \"$add\": [\"$J\", { \"$multiply\": [NumberLong(3600000), \"$C.E.F\"] } ] }, _id: 0 }"); + + result.Value.Result.Should().Be(result.Value.J.AddHours(result.Value.N)); + } + + [Fact] + public void Should_translate_datetime_add_xxx_negative_from_variable() + { + var result = Project(x => new { J = x.J, N = x.C.E.H, Result = x.J.AddHours(-x.C.E.H) }); + + result.Projection.Should().Be("{ J: \"$J\", N: \"$C.E.H\", Result: { \"$add\": [\"$J\", { \"$multiply\": [NumberLong(3600000), { \"$subtract\": [ 0, \"$C.E.H\" ] } ] } ] }, _id: 0 }"); + + result.Value.Result.Should().Be(result.Value.J.AddHours(-result.Value.N)); + } + + [SkippableFact] + public void Should_translate_datetime_to_string() + { + RequireServer.Where(minimumVersion: "3.0.0"); // ?? + + var result = Project(x => new { J = x.J, Result = x.J.ToString() }); + + result.Projection.Should().Be("{ J: \"$J\", Result: { \"$dateToString\": { \"format\": \"%Y-%m-%d %H:%M\", \"date\": \"$J\" } }, _id: 0 }"); + + result.Value.Result.Should().Be(result.Value.J.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture)); + } + + [SkippableFact] + public void Should_translate_datetime_to_string_with_format() + { + RequireServer.Where(minimumVersion: "3.0.0"); // ?? + + var result = Project(x => new { J = x.J, Result = x.J.ToString("%d.%m.%Y") }); + + result.Projection.Should().Be("{ J: \"$J\", Result: { \"$dateToString\": { \"format\": \"%d.%m.%Y\", \"date\": \"$J\" } }, _id: 0 }"); + + result.Value.Result.Should().Be(result.Value.J.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture)); + } + + [SkippableFact] + public void Should_translate_datetime_to_string_with_format_and_culture() + { + RequireServer.Where(minimumVersion: "3.0.0"); // ?? + + var result = Project(x => new { J = x.J, Result = x.J.ToString("%Y-%m-%dT%H:%M:%S.%LZ", CultureInfo.InvariantCulture) }); + + result.Projection.Should().Be("{ J: \"$J\", Result: { \"$dateToString\": { \"format\": \"%Y-%m-%dT%H:%M:%S.%LZ\", \"date\": \"$J\" } }, _id: 0 }"); + + result.Value.Result.Should().Be(result.Value.J.ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'", CultureInfo.InvariantCulture)); + } + [Fact] public void Should_translate_divide() { @@ -737,6 +814,16 @@ public void Should_translate_multiply_flattened() result.Value.Result.Should().Be(2420); } + + [Fact] + public void Should_translate_negation() + { + var result = Project(x => new { Result = -x.C.E.F }); + + result.Projection.Should().Be("{ Result: { \"$subtract\": [0, \"$C.E.F\"] }, _id: 0 }"); + + result.Value.Result.Should().Be(-11); + } [Fact] public void Should_translate_not() @@ -1126,7 +1213,7 @@ public void Should_translate_string_equals() result.Value.Result.Should().BeTrue(); } - + [Fact] public void Should_translate_string_equals_using_comparison() { @@ -1136,7 +1223,7 @@ public void Should_translate_string_equals_using_comparison() result.Value.Result.Should().BeTrue(); } - + [Fact] public void Should_translate_string_case_insensitive_equals() { @@ -1161,6 +1248,50 @@ public void Should_throw_for_a_not_supported_string_comparison_type(StringCompar act.ShouldThrow(); } + [Fact] + public void Should_translate_static_string_equals() + { + var result = Project(x => new { Result = string.Equals(x.B, "Balloon") }); + + result.Projection.Should().Be("{ Result: { \"$eq\": [\"$B\", \"Balloon\"] }, _id: 0 }"); + + result.Value.Result.Should().BeTrue(); + } + + [Fact] + public void Should_translate_static_string_equals_using_comparison() + { + var result = Project(x => new { Result = string.Equals(x.B, "Balloon", StringComparison.Ordinal) }); + + result.Projection.Should().Be("{ Result: { \"$eq\": [\"$B\", \"Balloon\"] }, _id: 0 }"); + + result.Value.Result.Should().BeTrue(); + } + + [Fact] + public void Should_translate_static_string_case_insensitive_equals() + { + var result = Project(x => new { Result = string.Equals(x.B, "balloon", StringComparison.OrdinalIgnoreCase) }); + + result.Projection.Should().Be("{ Result: { \"$eq\": [{ \"$strcasecmp\": [\"$B\", \"balloon\"] }, 0] }, _id: 0 }"); + + result.Value.Result.Should().BeTrue(); + } + + [Theory] + [InlineData(StringComparison.CurrentCulture)] + [InlineData(StringComparison.CurrentCultureIgnoreCase)] +#if NET45 + [InlineData(StringComparison.InvariantCulture)] + [InlineData(StringComparison.InvariantCultureIgnoreCase)] +#endif + public void Should_throw_for_a_not_supported_static_string_comparison_type(StringComparison comparison) + { + Action act = () => Project(x => new { Result = string.Equals(x.B, "balloon", comparison) }); + + act.ShouldThrow(); + } + [Fact] public void Should_translate_string_is_null_or_empty() { @@ -1170,7 +1301,103 @@ public void Should_translate_string_is_null_or_empty() result.Value.Result.Should().BeFalse(); } + + [Fact] + public void Should_translate_string_concat_with_2_strings() + { + var result = Project(x => new { Result = string.Concat(x.A, x.B) }); + + result.Projection.Should().Be("{ Result: { \"$concat\": [\"$A\", \"$B\"] }, _id: 0 }"); + + result.Value.Result.Should().Be("AwesomeBalloon"); + } + + [Fact] + public void Should_translate_string_concat_with_3_strings() + { + var result = Project(x => new { Result = string.Concat(x.B, "&", x.B) }); + + result.Projection.Should().Be("{ Result: { \"$concat\": [\"$B\", \"&\", \"$B\"] }, _id: 0 }"); + + result.Value.Result.Should().Be("Balloon&Balloon"); + } + + [Fact] + public void Should_translate_string_concat_with_4_strings() + { + var result = Project(x => new { Result = string.Concat(x.A, " ", x.B, "!") }); + + result.Projection.Should().Be("{ Result: { \"$concat\": [\"$A\", \" \", \"$B\", \"!\"] }, _id: 0 }"); + + result.Value.Result.Should().Be("Awesome Balloon!"); + } + + [Fact] + public void Should_translate_string_concat_with_params_string_array() + { + var result = Project(x => new { Result = string.Concat(x.A, "+", x.B, "+", x.C.D) }); + + result.Projection.Should().Be("{ Result: { \"$concat\": [\"$A\", \"+\", \"$B\", \"+\", \"$C.D\"] }, _id: 0 }"); + + result.Value.Result.Should().Be("Awesome+Balloon+Dexter"); + } + + [Fact] + public void Should_throw_for_a_not_supported_string_concat_with_enumerable_of_string() + { + Action act = () => Project(x => new { Result = string.Concat(x.C.E.I) }); + act.ShouldThrow(); + } + + [Fact] + public void Should_throw_for_a_not_supported_string_concat_with_1_object() + { + Action act = () => Project(x => new { Result = string.Concat((object)x.A) }); + + act.ShouldThrow(); + } + + [Fact] + public void Should_throw_for_a_not_supported_string_concat_with_2_objects() + { + Action act = () => Project(x => new { Result = string.Concat(x.A, x.C) }); + + act.ShouldThrow(); + } + + [Fact] + public void Should_throw_for_a_not_supported_string_concat_with_3_objects() + { + Action act = () => Project(x => new { Result = string.Concat(x.C, null) }); + + act.ShouldThrow(); + } + + [Fact] + public void Should_throw_for_a_not_supported_string_concat_with_4_objects() + { + Action act = () => Project(x => new { Result = string.Concat(x.C, x.B) }); + + act.ShouldThrow(); + } + + [Fact] + public void Should_throw_for_a_not_supported_string_concat_with_params_object_array() + { + Action act = () => Project(x => new { Result = string.Concat(x.A, 1, x.C, false, x.G) }); + + act.ShouldThrow(); + } + + [Fact] + public void Should_throw_for_a_not_supported_string_concat_with_generic_enumerable() + { + Action act = () => Project(x => new { Result = string.Concat(x.G) }); + + act.ShouldThrow(); + } + [Fact] public void Should_translate_substring() { @@ -1181,6 +1408,16 @@ public void Should_translate_substring() result.Value.Result.Should().Be("loon"); } + [Fact] + public void Should_translate_substring_with_1_arg() + { + var result = Project(x => new { Result = x.B.Substring(3) }); + + result.Projection.Should().Be("{ Result: { \"$substr\": [\"$B\", 3, -1] }, _id: 0 }"); + + result.Value.Result.Should().Be("loon"); + } + [Fact] public void Should_translate_subtract() { From 562475865aec5c4e6baee0726a9e21e4c3fce16d Mon Sep 17 00:00:00 2001 From: Alexey Skalozub Date: Tue, 2 Aug 2016 10:41:10 +0300 Subject: [PATCH 08/10] Fix copy-paste error in tests --- .../Linq/Translators/AggregateProjectTranslatorTests.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/MongoDB.Driver.Tests/Linq/Translators/AggregateProjectTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Translators/AggregateProjectTranslatorTests.cs index 3e18a142860..841513faaa3 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Translators/AggregateProjectTranslatorTests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Translators/AggregateProjectTranslatorTests.cs @@ -1369,7 +1369,7 @@ public void Should_throw_for_a_not_supported_string_concat_with_2_objects() [Fact] public void Should_throw_for_a_not_supported_string_concat_with_3_objects() { - Action act = () => Project(x => new { Result = string.Concat(x.C, null) }); + Action act = () => Project(x => new { Result = string.Concat(x.C, x.J, x.K) }); act.ShouldThrow(); } @@ -1377,7 +1377,7 @@ public void Should_throw_for_a_not_supported_string_concat_with_3_objects() [Fact] public void Should_throw_for_a_not_supported_string_concat_with_4_objects() { - Action act = () => Project(x => new { Result = string.Concat(x.C, x.B) }); + Action act = () => Project(x => new { Result = string.Concat(x.C, x.B, x.J, x.K) }); act.ShouldThrow(); } From eec2b44a3565bcec6a73ab1c6fc5662711e528c8 Mon Sep 17 00:00:00 2001 From: Elliott Hobbs Date: Mon, 5 Nov 2018 14:24:37 +1000 Subject: [PATCH 09/10] Fix failing tests Remove IFormat support from DateTime.ToString() Minor code cleanup --- .../AggregateLanguageTranslator.cs | 216 +++++++----------- .../AggregateProjectTranslatorTests.cs | 50 ++-- 2 files changed, 93 insertions(+), 173 deletions(-) diff --git a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs index 182881fe396..f6495889f96 100644 --- a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs +++ b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs @@ -346,26 +346,6 @@ private BsonValue TranslateMemberAccess(MemberExpression node) } // - // Consider a collection of documents like this: - // { - // _id: ..., - // CustomerId: ..., - // Order: { - // Qty: 5, - // Price: 15 - // } - // } - // - // And then we're aggregating customers and calculating total price: - // $group: { - // _id: "$CustomerId", - // TotalQty: { $sum: "$Order.Qty" }, - // TotalPrice: { $sum: "$Order.Price" } - // } - // - // This is exactly the place when we get here with member access. - // - result = TranslateValue(node.Expression); if (result.BsonType == BsonType.String && result.AsString.StartsWith("$")) { @@ -840,31 +820,7 @@ private bool TryTranslateCountResultOperator(PipelineExpression node, out BsonVa result = null; return false; } - - private bool TryTranslateDateTimeCall(MethodCallExpression node, out BsonValue result) - { - result = null; - var field = TranslateValue(node.Object); - - switch (node.Method.Name) - { - case "ToString": - if (node.Arguments.Count == 1) - { - var format = TranslateValue(node.Arguments[0]); - result = new BsonDocument("$dateToString", new BsonDocument - { - { "format", format }, - { "date", field } - }); - return true; - } - break; - } - - return false; - } - + private bool TryTranslateDateTimeMemberAccess(MemberExpression node, out BsonValue result) { result = null; @@ -966,48 +922,26 @@ private bool TryTranslateDateTimeAddXXX(MethodCallExpression node, long multipli result = new BsonDocument("$add", new BsonArray(new[] { date, arg })); return true; } - - private bool TryTranslateDateTimeToString(MethodCallExpression node, out BsonValue result) - { - result = null; - if (node.Arguments.Count > 2) - { - // No overload for DateTime.ToString() takes more than 2 args - return false; - } - - var date = TranslateValue(node.Object); - - // assume default format YYYY-MM-DD HH:mm - BsonValue format = "%Y-%m-%d %H:%M"; - - if (node.Arguments.Count == 1) - { - // can be either .ToString(IFormatProvider) or .ToString(string) overload - // need to check argument type to be sure - if (node.Arguments[0].Type == typeof(string)) - { - format = TranslateValue(node.Arguments[0]); - } - } - else if (node.Arguments.Count == 2) - { - // this is .ToString(string, IFormatProvider), format always goes first - format = TranslateValue(node.Arguments[0]); - } - - result = new BsonDocument("$dateToString", new BsonDocument().Add("format", format).Add("date", date)); - return true; - } - + private bool TryTranslateDateTimeMethodCall(MethodCallExpression node, out BsonValue result) { switch (node.Method.Name) { case "ToString": - // $dateToString - return TryTranslateDateTimeToString(node, out result); - + if (node.Arguments.Count <= 1) + { + // $dateToString + var field = TranslateValue(node.Object); + // default format YYYY-MM-DD HH:mm + var format = node.Arguments.Count == 1 ? TranslateValue(node.Arguments[0]) : "%Y-%m-%d %H:%M"; + result = new BsonDocument("$dateToString", new BsonDocument + { + { "format", format }, + { "date", field } + }); + return true; + } + break; case "AddYears": case "AddMonths": // adding years/months has potential issues with leap years, @@ -1259,10 +1193,8 @@ private BsonValue StringEqualsHelper(BsonValue a, BsonValue b, Expression string new BsonDocument("$strcasecmp", new BsonArray(new[] { a, b })), (BsonValue)0 })); - case StringComparison.Ordinal: return new BsonDocument("$eq", new BsonArray(new[] { a, b })); - default: throw new NotSupportedException("Only Ordinal and OrdinalIgnoreCase are supported for string comparisons."); } @@ -1278,31 +1210,37 @@ private bool TryTranslateStaticStringMethodCall(MethodCallExpression node, out B } var field = TranslateValue(node.Arguments[0]); - switch (node.Method.Name) { case "IsNullOrEmpty": if (node.Arguments.Count == 1) { - result = new BsonDocument("$or", + result = new BsonDocument("$or", new BsonArray { - new BsonDocument("$eq", new BsonArray { field, BsonNull.Value }), - new BsonDocument("$eq", new BsonArray { field, BsonString.Empty }) + new BsonDocument("$eq", new BsonArray + { + field, + BsonNull.Value + }), + new BsonDocument("$eq", new BsonArray + { + field, + BsonString.Empty + }) }); - return true; - } + return true; + } break; - case "Equals": if (node.Arguments.Count >= 2 && node.Arguments.Count <= 3) { - result = StringEqualsHelper(field, TranslateValue(node.Arguments[1]), + result = StringEqualsHelper(field, + TranslateValue(node.Arguments[1]), (node.Arguments.Count == 3) ? node.Arguments[2] : null); return true; } break; - case "Concat": if (node.Arguments.Count == 1 && node.Arguments[0].Type == typeof(string[])) { @@ -1315,17 +1253,18 @@ private bool TryTranslateStaticStringMethodCall(MethodCallExpression node, out B { // this is one of .Concat(string, string[, string[, string]]) overloads var array = new BsonArray(); - for (int i = 0; i < node.Arguments.Count; i++) + foreach (var arg in node.Arguments) { - array.Add(TranslateValue(node.Arguments[i])); + array.Add(TranslateValue(arg)); } result = new BsonDocument("$concat", array); return true; } else + { throw new NotSupportedException("Some overloads of String.Concat() are not supported"); - + } } return false; @@ -1335,45 +1274,37 @@ private bool TryTranslateStringMethodCall(MethodCallExpression node, out BsonVal { result = null; var field = TranslateValue(node.Object); - - if (node.Arguments.Count == 0) - { - // parameterless methods switch (node.Method.Name) { - case "ToLower": - case "ToLowerInvariant": - result = new BsonDocument("$toLower", field); + case "Equals": + if (node.Arguments.Count == 2 && node.Arguments[1].NodeType == ExpressionType.Constant) + { + var comparisonType = (StringComparison)((ConstantExpression)node.Arguments[1]).Value; + switch (comparisonType) + { + case StringComparison.OrdinalIgnoreCase: + result = new BsonDocument("$eq", + new BsonArray(new BsonValue[] + { + new BsonDocument("$strcasecmp", new BsonArray(new[] { field, TranslateValue(node.Arguments[0]) })), + 0 + })); return true; - - case "ToUpper": - case "ToUpperInvariant": - result = new BsonDocument("$toUpper", field); + case StringComparison.Ordinal: + result = new BsonDocument("$eq", new BsonArray(new[] { field, TranslateValue(node.Arguments[0]) })); return true; + default: + throw new NotSupportedException("Only Ordinal and OrdinalIgnoreCase are supported for string comparisons."); } } - else if (node.Arguments.Count <= 2) - { - // methods with 1 or 2 parameters - - var arg0 = TranslateValue(node.Arguments[0]); - - switch (node.Method.Name) - { - case "Equals": - result = StringEqualsHelper(field, arg0, - (node.Arguments.Count == 2) ? node.Arguments[1] : null); - return true; - + break; case "IndexOf": var indexOfArgs = new BsonArray { field }; - if (node.Arguments.Count < 1 || node.Arguments.Count > 3) { return false; } - if (node.Arguments[0].Type != typeof(char) && node.Arguments[0].Type != typeof(string)) { return false; @@ -1384,34 +1315,28 @@ private bool TryTranslateStringMethodCall(MethodCallExpression node, out BsonVal value = new BsonString(new string((char)value.AsInt32, 1)); } indexOfArgs.Add(value); - if (node.Arguments.Count > 1) { if (node.Arguments[1].Type != typeof(int)) { return false; } - var startIndex = TranslateValue(node.Arguments[1]); indexOfArgs.Add(startIndex); } - if (node.Arguments.Count > 2) { if (node.Arguments[2].Type != typeof(int)) { return false; } - var count = TranslateValue(node.Arguments[2]); var endIndex = new BsonDocument("$add", new BsonArray { indexOfArgs[2], count }); indexOfArgs.Add(endIndex); } - var indexOpName = _stringTranslationMode == AggregateStringTranslationMode.CodePoints ? "$indexOfCP" : "$indexOfBytes"; - result = new BsonDocument(indexOpName, indexOfArgs); return true; case "Split": @@ -1453,18 +1378,37 @@ private bool TryTranslateStringMethodCall(MethodCallExpression node, out BsonVal }); return true; case "Substring": - var substrOpName = _stringTranslationMode == AggregateStringTranslationMode.CodePoints ? - "$substrCP" : - "$substr"; + var substrOpName = _stringTranslationMode == AggregateStringTranslationMode.CodePoints ? + "$substrCP" : + "$substr"; + if (node.Arguments.Count == 2 || node.Arguments.Count == 1) + { result = new BsonDocument(substrOpName, new BsonArray(new[] - { - field, - arg0, - (node.Arguments.Count == 2) ? TranslateValue(node.Arguments[1]) : -1 - })); + { + field, + TranslateValue(node.Arguments[0]), + node.Arguments.Count == 2 ? TranslateValue(node.Arguments[1]) : -1 + })); return true; } + break; + case "ToLower": + case "ToLowerInvariant": + if (node.Arguments.Count == 0) + { + result = new BsonDocument("$toLower", field); + return true; } + break; + case "ToUpper": + case "ToUpperInvariant": + if (node.Arguments.Count == 0) + { + result = new BsonDocument("$toUpper", field); + return true; + } + break; + } return false; } diff --git a/tests/MongoDB.Driver.Tests/Linq/Translators/AggregateProjectTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Translators/AggregateProjectTranslatorTests.cs index d684a5867e5..9bbba515213 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Translators/AggregateProjectTranslatorTests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Translators/AggregateProjectTranslatorTests.cs @@ -430,6 +430,18 @@ public void Should_translate_dateToString() result.Value.Result.Should().Be("2012-12-01"); } + [SkippableFact] + public void Should_translate_dateToString_without_format() + { + RequireServer.Check().VersionGreaterThanOrEqualTo("3.2.0"); + + var result = Project(x => new { Result = x.J.ToString() }); + + result.Projection.Should().Be("{ Result: { \"$dateToString\": {format: \"%Y-%m-%d %H:%M\", date: \"$J\" } }, _id: 0 }"); + + result.Value.Result.Should().Be("2012-12-01 13:14"); + } + [Fact] public void Should_translate_day_of_month() { @@ -499,43 +511,7 @@ public void Should_translate_datetime_add_xxx_negative_from_variable() result.Value.Result.Should().Be(result.Value.J.AddHours(-result.Value.N)); } - - [SkippableFact] - public void Should_translate_datetime_to_string() - { - RequireServer.Check().VersionGreaterThanOrEqualTo("3.1.6"); - - var result = Project(x => new { J = x.J, Result = x.J.ToString() }); - - result.Projection.Should().Be("{ J: \"$J\", Result: { \"$dateToString\": { \"format\": \"%Y-%m-%d %H:%M\", \"date\": \"$J\" } }, _id: 0 }"); - - result.Value.Result.Should().Be(result.Value.J.ToString("yyyy-MM-dd HH:mm", CultureInfo.InvariantCulture)); - } - - [SkippableFact] - public void Should_translate_datetime_to_string_with_format() - { - RequireServer.Check().VersionGreaterThanOrEqualTo("3.1.6"); - - var result = Project(x => new { J = x.J, Result = x.J.ToString("%d.%m.%Y") }); - - result.Projection.Should().Be("{ J: \"$J\", Result: { \"$dateToString\": { \"format\": \"%d.%m.%Y\", \"date\": \"$J\" } }, _id: 0 }"); - - result.Value.Result.Should().Be(result.Value.J.ToString("dd.MM.yyyy", CultureInfo.InvariantCulture)); - } - - [SkippableFact] - public void Should_translate_datetime_to_string_with_format_and_culture() - { - RequireServer.Check().VersionGreaterThanOrEqualTo("3.1.6"); - - var result = Project(x => new { J = x.J, Result = x.J.ToString("%Y-%m-%dT%H:%M:%S.%LZ", CultureInfo.InvariantCulture) }); - - result.Projection.Should().Be("{ J: \"$J\", Result: { \"$dateToString\": { \"format\": \"%Y-%m-%dT%H:%M:%S.%LZ\", \"date\": \"$J\" } }, _id: 0 }"); - - result.Value.Result.Should().Be(result.Value.J.ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'", CultureInfo.InvariantCulture)); - } - + [Fact] public void Should_translate_divide() { From 229c0a73dc47ce1c99e0c4b41acafea8902a7150 Mon Sep 17 00:00:00 2001 From: Elliott Hobbs Date: Mon, 3 Dec 2018 16:17:10 +1000 Subject: [PATCH 10/10] CSHARP-2445 Improve supported methods of DateTime in LINQ Remove unused imports Update README --- README.md | 2 ++ .../Linq/Translators/AggregateLanguageTranslator.cs | 1 - .../Linq/Translators/AggregateProjectTranslatorTests.cs | 3 --- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 1e72507068c..1231b7291b2 100644 --- a/README.md +++ b/README.md @@ -133,5 +133,7 @@ Please see our [guidelines](CONTRIBUTING.md) for contributing to the driver. * Vyacheslav Stroy https://github.com/kreig * Testo test1@doramail.com * Zhmayev Yaroslav https://github.com/salaros +* Alexey Skalozub https://github.com/pieceofsummer@gmail +* Elliott Millar https://github.com/elliott-pd If you have contributed and we have neglected to add you to this list please contact one of the maintainers to be added to the list (with apologies). \ No newline at end of file diff --git a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs index f6495889f96..9ac80d63bda 100644 --- a/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs +++ b/src/MongoDB.Driver/Linq/Translators/AggregateLanguageTranslator.cs @@ -20,7 +20,6 @@ using System.Linq; using System.Linq.Expressions; using System.Reflection; -using System.Text; using MongoDB.Bson; using MongoDB.Driver.Linq.Expressions; using MongoDB.Driver.Linq.Expressions.ResultOperators; diff --git a/tests/MongoDB.Driver.Tests/Linq/Translators/AggregateProjectTranslatorTests.cs b/tests/MongoDB.Driver.Tests/Linq/Translators/AggregateProjectTranslatorTests.cs index 9bbba515213..6dc223b3130 100644 --- a/tests/MongoDB.Driver.Tests/Linq/Translators/AggregateProjectTranslatorTests.cs +++ b/tests/MongoDB.Driver.Tests/Linq/Translators/AggregateProjectTranslatorTests.cs @@ -17,17 +17,14 @@ using System.Collections.Generic; using System.Linq; using System.Linq.Expressions; -using System.Threading.Tasks; using FluentAssertions; using MongoDB.Bson; using MongoDB.Bson.Serialization; using MongoDB.Bson.TestHelpers.XunitExtensions; -using MongoDB.Driver.Core; using MongoDB.Driver.Core.TestHelpers.XunitExtensions; using MongoDB.Driver.Linq; using MongoDB.Driver.Linq.Translators; using Xunit; -using System.Globalization; namespace MongoDB.Driver.Tests.Linq.Translators {