From e08df5e247e0a49737d3a02a6bb52c73d0065149 Mon Sep 17 00:00:00 2001 From: Turnerj Date: Tue, 5 Nov 2019 13:32:11 +1030 Subject: [PATCH 01/18] Bits and pieces to create a custom LINQ engine Kickstarts #87 though a ton more work is needed --- .../Querying/CallConverters/WhereConverter.cs | 21 ++++++ .../Querying/ExpressionHelper.cs | 59 +++++++++++++++ .../Infrastructure/Querying/ICallConverter.cs | 13 ++++ .../Infrastructure/Querying/QueryMapping.cs | 59 +++++++++++++++ .../Querying/QueryMappingTests.cs | 72 +++++++++++++++++++ 5 files changed, 224 insertions(+) create mode 100644 src/MongoFramework/Infrastructure/Querying/CallConverters/WhereConverter.cs create mode 100644 src/MongoFramework/Infrastructure/Querying/ExpressionHelper.cs create mode 100644 src/MongoFramework/Infrastructure/Querying/ICallConverter.cs create mode 100644 src/MongoFramework/Infrastructure/Querying/QueryMapping.cs create mode 100644 tests/MongoFramework.Tests/Infrastructure/Querying/QueryMappingTests.cs diff --git a/src/MongoFramework/Infrastructure/Querying/CallConverters/WhereConverter.cs b/src/MongoFramework/Infrastructure/Querying/CallConverters/WhereConverter.cs new file mode 100644 index 00000000..a2c11e0c --- /dev/null +++ b/src/MongoFramework/Infrastructure/Querying/CallConverters/WhereConverter.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; +using MongoDB.Bson; + +namespace MongoFramework.Infrastructure.Querying.CallConverters +{ + public class WhereConverter : ICallConverter + { + public static ICallConverter Instance { get; } = new WhereConverter(); + + public BsonDocument Convert(MethodCallExpression expression) + { + return new BsonDocument + { + { "$match", ExpressionHelper.Where(expression.Arguments[1]) } + }; + } + } +} diff --git a/src/MongoFramework/Infrastructure/Querying/ExpressionHelper.cs b/src/MongoFramework/Infrastructure/Querying/ExpressionHelper.cs new file mode 100644 index 00000000..3f5d3bef --- /dev/null +++ b/src/MongoFramework/Infrastructure/Querying/ExpressionHelper.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using MongoDB.Bson; +using MongoFramework.Infrastructure.Mapping; + +namespace MongoFramework.Infrastructure.Querying +{ + public static class ExpressionHelper + { + public static Expression> Stub() + { + return null; + } + public static Expression> Stub() + { + return null; + } + public static Expression> Stub() + { + return null; + } + public static Expression> Stub() + { + return null; + } + + public static MethodInfo GetGenericMethodInfo(Expression expression) + { + if (expression.Body.NodeType == ExpressionType.Call) + { + var methodInfo = ((MethodCallExpression)expression.Body).Method; + return methodInfo.GetGenericMethodDefinition(); + } + + throw new InvalidOperationException("The provided expression does not call a method"); + } + + public static BsonDocument Where(LambdaExpression expression) + { + if (expression.ReturnType != typeof(bool)) + { + throw new ArgumentException("Expression must return a boolean"); + } + + var incomingType = expression.Parameters[0].Type; + + if (EntityMapping.IsRegistered(incomingType)) + { + var definition = EntityMapping.GetOrCreateDefinition(incomingType); + + + } + return null; + } + } +} diff --git a/src/MongoFramework/Infrastructure/Querying/ICallConverter.cs b/src/MongoFramework/Infrastructure/Querying/ICallConverter.cs new file mode 100644 index 00000000..5b4918ef --- /dev/null +++ b/src/MongoFramework/Infrastructure/Querying/ICallConverter.cs @@ -0,0 +1,13 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; +using MongoDB.Bson; + +namespace MongoFramework.Infrastructure.Querying +{ + public interface ICallConverter + { + BsonDocument Convert(MethodCallExpression expression); + } +} diff --git a/src/MongoFramework/Infrastructure/Querying/QueryMapping.cs b/src/MongoFramework/Infrastructure/Querying/QueryMapping.cs new file mode 100644 index 00000000..1cd84bc2 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Querying/QueryMapping.cs @@ -0,0 +1,59 @@ +using System; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using MongoDB.Bson; +using MongoFramework.Infrastructure.Querying.CallConverters; + +namespace MongoFramework.Infrastructure.Querying +{ + public static class QueryMapping + { + public static ConcurrentDictionary CallConverters { get; } = new ConcurrentDictionary(); + + static QueryMapping() + { + AddConverter(WhereConverter.Instance, () => Queryable.Where(default, ExpressionHelper.Stub())); + } + + public static void AddConverter(ICallConverter callConverter, params Expression[] methodExpressions) + { + foreach (var expression in methodExpressions) + { + var methodInfo = ExpressionHelper.GetGenericMethodInfo(expression); + CallConverters.TryAdd(methodInfo, callConverter); + } + } + + public static IEnumerable FromExpression(Expression expression) + { + var currentExpression = expression; + var stages = new Stack(); + + while (currentExpression is MethodCallExpression callExpression) + { + var methodDefinition = callExpression.Method; + if (methodDefinition.IsGenericMethod) + { + methodDefinition = methodDefinition.GetGenericMethodDefinition(); + } + + if (CallConverters.TryGetValue(methodDefinition, out var converter)) + { + var queryStage = converter.Convert(callExpression); + stages.Push(queryStage); + currentExpression = callExpression.Arguments[0]; + } + else + { + throw new InvalidOperationException($"No converter has been configured for {callExpression.Method}"); + } + } + + return stages; + } + } +} diff --git a/tests/MongoFramework.Tests/Infrastructure/Querying/QueryMappingTests.cs b/tests/MongoFramework.Tests/Infrastructure/Querying/QueryMappingTests.cs new file mode 100644 index 00000000..ce1ee733 --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Querying/QueryMappingTests.cs @@ -0,0 +1,72 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoFramework.Infrastructure.Querying; + +namespace MongoFramework.Tests.Infrastructure.Querying +{ + [TestClass] + public class QueryMappingTests : TestBase + { + private class QueryTestModel + { + public string Id { get; set; } + public int SomeNumberField { get; set; } + public string AnotherStringField { get; set; } + public NestedQueryTestModel NestedModel { get; set; } + } + + private class NestedQueryTestModel + { + public string Name { get; set; } + public int Number { get; set; } + } + + [TestMethod] + public void EmptyQueryableHasNoStages() + { + var queryable = Queryable.AsQueryable(new[] { + new QueryTestModel() + }); + + var stages = QueryMapping.FromExpression(queryable.Expression); + Assert.AreEqual(0, stages.Count()); + } + + [TestMethod] + public void Queryable_Where() + { + var queryable = Queryable.AsQueryable(new[] { + new QueryTestModel() + }).Where(q => q.Id == ""); + + var stages = QueryMapping.FromExpression(queryable.Expression); + Assert.AreEqual(1, stages.Count()); + } + + [TestMethod] + public void Queryable_Where_OrderBy() + { + var queryable = Queryable.AsQueryable(new[] { + new QueryTestModel() + }).Where(q => q.Id == "").OrderBy(q => q.Id); + + var stages = QueryMapping.FromExpression(queryable.Expression); + Assert.AreEqual(2, stages.Count()); + } + + [TestMethod] + public void Queryable_Where_OrderBy_Select() + { + var queryable = Queryable.AsQueryable(new[] { + new QueryTestModel() + }).Where(q => q.Id == "").OrderBy(q => q.Id).Select(q => q.SomeNumberField); + + var stages = QueryMapping.FromExpression(queryable.Expression); + Assert.AreEqual(3, stages.Count()); + } + } +} From 01207f2523838c75c3cc6dff629c0207e4c993c9 Mon Sep 17 00:00:00 2001 From: Turnerj Date: Tue, 12 Nov 2019 11:44:19 +1030 Subject: [PATCH 02/18] Continuing the custom LINQ engine path Tries to standardise behind one "MethodParser" system. Adds a whole bunch of parsing logic for many types of expressions. Major things to do: - Support projection ("new BlahBlah" as well as properties) - Support groupby - Support ThenOrderBy (though will need a post-processor or something to group statements unless I un-standardise it and have no queries-within-queries - I mean, I probably don't want that anyway) --- .../Querying/CallConverters/WhereConverter.cs | 21 -- .../Querying/ExpressionHelper.cs | 37 +- .../Querying/ExpressionParser.cs | 352 ++++++++++++++++++ .../{ICallConverter.cs => IMethodParser.cs} | 4 +- .../Querying/MethodParsers/OrderByParser.cs | 42 +++ .../Querying/MethodParsers/SelectParser.cs | 35 ++ .../Querying/MethodParsers/WhereParser.cs | 26 ++ .../Querying/MethodQueryResult.cs | 12 + .../Infrastructure/Querying/QueryMapping.cs | 59 --- .../Infrastructure/Querying/StageBuilder.cs | 27 ++ .../Querying/QueryMappingTests.cs | 19 +- 11 files changed, 512 insertions(+), 122 deletions(-) delete mode 100644 src/MongoFramework/Infrastructure/Querying/CallConverters/WhereConverter.cs create mode 100644 src/MongoFramework/Infrastructure/Querying/ExpressionParser.cs rename src/MongoFramework/Infrastructure/Querying/{ICallConverter.cs => IMethodParser.cs} (66%) create mode 100644 src/MongoFramework/Infrastructure/Querying/MethodParsers/OrderByParser.cs create mode 100644 src/MongoFramework/Infrastructure/Querying/MethodParsers/SelectParser.cs create mode 100644 src/MongoFramework/Infrastructure/Querying/MethodParsers/WhereParser.cs create mode 100644 src/MongoFramework/Infrastructure/Querying/MethodQueryResult.cs delete mode 100644 src/MongoFramework/Infrastructure/Querying/QueryMapping.cs create mode 100644 src/MongoFramework/Infrastructure/Querying/StageBuilder.cs diff --git a/src/MongoFramework/Infrastructure/Querying/CallConverters/WhereConverter.cs b/src/MongoFramework/Infrastructure/Querying/CallConverters/WhereConverter.cs deleted file mode 100644 index a2c11e0c..00000000 --- a/src/MongoFramework/Infrastructure/Querying/CallConverters/WhereConverter.cs +++ /dev/null @@ -1,21 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Text; -using MongoDB.Bson; - -namespace MongoFramework.Infrastructure.Querying.CallConverters -{ - public class WhereConverter : ICallConverter - { - public static ICallConverter Instance { get; } = new WhereConverter(); - - public BsonDocument Convert(MethodCallExpression expression) - { - return new BsonDocument - { - { "$match", ExpressionHelper.Where(expression.Arguments[1]) } - }; - } - } -} diff --git a/src/MongoFramework/Infrastructure/Querying/ExpressionHelper.cs b/src/MongoFramework/Infrastructure/Querying/ExpressionHelper.cs index 3f5d3bef..fc454f9e 100644 --- a/src/MongoFramework/Infrastructure/Querying/ExpressionHelper.cs +++ b/src/MongoFramework/Infrastructure/Querying/ExpressionHelper.cs @@ -10,24 +10,7 @@ namespace MongoFramework.Infrastructure.Querying { public static class ExpressionHelper { - public static Expression> Stub() - { - return null; - } - public static Expression> Stub() - { - return null; - } - public static Expression> Stub() - { - return null; - } - public static Expression> Stub() - { - return null; - } - - public static MethodInfo GetGenericMethodInfo(Expression expression) + public static MethodInfo GetMethodDefinition(Expression expression) { if (expression.Body.NodeType == ExpressionType.Call) { @@ -37,23 +20,5 @@ public static MethodInfo GetGenericMethodInfo(Expression expression) throw new InvalidOperationException("The provided expression does not call a method"); } - - public static BsonDocument Where(LambdaExpression expression) - { - if (expression.ReturnType != typeof(bool)) - { - throw new ArgumentException("Expression must return a boolean"); - } - - var incomingType = expression.Parameters[0].Type; - - if (EntityMapping.IsRegistered(incomingType)) - { - var definition = EntityMapping.GetOrCreateDefinition(incomingType); - - - } - return null; - } } } diff --git a/src/MongoFramework/Infrastructure/Querying/ExpressionParser.cs b/src/MongoFramework/Infrastructure/Querying/ExpressionParser.cs new file mode 100644 index 00000000..b6858ba7 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Querying/ExpressionParser.cs @@ -0,0 +1,352 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using MongoDB.Bson; +using MongoFramework.Infrastructure.Mapping; +using MongoFramework.Infrastructure.Querying.MethodParsers; + +namespace MongoFramework.Infrastructure.Querying +{ + public static class ExpressionParser + { + private static readonly IReadOnlyDictionary ComparatorToStringMap = new Dictionary + { + { ExpressionType.Equal, "$eq" }, + { ExpressionType.NotEqual, "$nq" }, + { ExpressionType.LessThan, "$lt" }, + { ExpressionType.GreaterThan, "$gt" }, + { ExpressionType.LessThanOrEqual, "$lte" }, + { ExpressionType.GreaterThanOrEqual, "$gte" } + }; + + private static readonly IReadOnlyDictionary NumericComparatorInversionMap = new Dictionary + { + { ExpressionType.LessThan, ExpressionType.GreaterThan }, + { ExpressionType.GreaterThan, ExpressionType.LessThan }, + { ExpressionType.LessThanOrEqual, ExpressionType.GreaterThanOrEqual }, + { ExpressionType.GreaterThanOrEqual, ExpressionType.LessThanOrEqual } + }; + + private static object MethodParserLockObj = new object(); + + private static readonly Dictionary MethodParserMap = new Dictionary(); + + static ExpressionParser() + { + AddMethodParser(new WhereParser(), WhereParser.GetSupportedMethods()); + AddMethodParser(new OrderByParser(), OrderByParser.GetSupportedMethods()); + AddMethodParser(new SelectParser(), SelectParser.GetSupportedMethods()); + } + + public static void AddMethodParser(IMethodParser parser, IEnumerable methods) + { + lock (MethodParserLockObj) + { + foreach (var method in methods) + { + MethodParserMap.Add(method, parser); + } + } + } + + public static BsonValue BuildPartialQuery(Expression expression) + { + if (expression is BinaryExpression binaryExpression) + { + if (expression.NodeType == ExpressionType.AndAlso) + { + var unwrappedQuery = new BsonArray(); + UnwrapBinaryQuery(unwrappedQuery, ExpressionType.AndAlso, binaryExpression); + + var elements = new BsonElement[unwrappedQuery.Count]; + + for (int i = 0, l = unwrappedQuery.Count; i < l; i++) + { + elements[i] = unwrappedQuery[i].AsBsonDocument.GetElement(0); + } + + return new BsonDocument((IEnumerable)elements); + } + else if (expression.NodeType == ExpressionType.OrElse) + { + var unwrappedQuery = new BsonArray(); + UnwrapBinaryQuery(unwrappedQuery, ExpressionType.OrElse, binaryExpression); + return new BsonDocument + { + { "$or", unwrappedQuery } + }; + } + else if (expression.NodeType == ExpressionType.ArrayIndex) + { + return BuildMemberAccessQuery(binaryExpression); + } + else + { + return BuildPartialEquality(binaryExpression); + } + } + else if (expression is MethodCallExpression methodCallExpression) + { + return BuildMethodQuery(methodCallExpression); + } + else if (expression is ConstantExpression constantExpression) + { + return BsonValue.Create(constantExpression.Value); + } + else if (expression is MemberExpression memberExpression) + { + return BuildMemberAccessQuery(memberExpression); + } + else if (expression is ParameterExpression) + { + return null; + } + else if (expression is UnaryExpression unaryExpression) + { + if (unaryExpression.NodeType == ExpressionType.Not) + { + string operatorName; + if (unaryExpression.Operand.NodeType == ExpressionType.OrElse) + { + operatorName = "$nor"; + } + else + { + operatorName = "$not"; + } + + return new BsonDocument + { + { operatorName, BuildPartialQuery(unaryExpression.Operand) } + }; + } + else if (unaryExpression.NodeType == ExpressionType.Quote) + { + var nestedLambda = unaryExpression.Operand as LambdaExpression; + return BuildPartialQuery(nestedLambda.Body); + } + } + + throw new ArgumentException($"Unexpected expression type {expression}"); + } + + private static BsonValue BuildMemberAccessQuery(Expression expression) + { + var walkedExpressions = new Stack(); + var currentUnrolledExpression = expression; + + MethodCallExpression outerMostMethodExpression = null; + + while (true) + { + if (currentUnrolledExpression is BinaryExpression binaryExpression) + { + if (binaryExpression.NodeType == ExpressionType.ArrayIndex) + { + //The index is on the right (a ConstantExpression) + walkedExpressions.Push(binaryExpression.Right); + + //The parent expression is on the left + currentUnrolledExpression = binaryExpression.Left; + } + else + { + throw new ArgumentException($"Unexpected node type {expression.NodeType}. Expected {ExpressionType.ArrayIndex}."); + } + } + else if (currentUnrolledExpression is MethodCallExpression methodCallExpression) + { + if (outerMostMethodExpression == null) + { + outerMostMethodExpression = methodCallExpression; + } + + walkedExpressions.Push(methodCallExpression); + currentUnrolledExpression = methodCallExpression.Object; + } + else if (currentUnrolledExpression is MemberExpression memberExpression) + { + walkedExpressions.Push(memberExpression); + currentUnrolledExpression = memberExpression.Expression; + } + else if (currentUnrolledExpression is ParameterExpression || currentUnrolledExpression is ConstantExpression) + { + walkedExpressions.Push(currentUnrolledExpression); + break; + } + else + { + throw new ArgumentException($"Unexpected node type {expression.NodeType}. Expected {ExpressionType.ArrayIndex}."); + } + } + + var firstExpression = walkedExpressions.Peek(); + + if (firstExpression is ParameterExpression) + { + //ParameterExpression means we can't determine the value + //If we find a method in the path, discard everything after it and treat this as a method + if (outerMostMethodExpression != null) + { + return BuildMethodQuery(outerMostMethodExpression); + } + else + { + //When no methods are found, treat this as a straight field name + var namePieces = new List(); + + //Remove the parameter expression, we don't need it as part of the field name + walkedExpressions.Pop(); + + foreach (var currentExpression in walkedExpressions) + { + if (currentExpression is MemberExpression memberExpression) + { + var member = memberExpression.Member; + var entityDefinition = EntityMapping.GetOrCreateDefinition(member.DeclaringType); + var entityProperty = entityDefinition.GetProperty(member.Name); + namePieces.Add(entityProperty.ElementName); + } + else if (currentExpression is ConstantExpression constantExpression) + { + var arrayIndex = constantExpression.Value.ToString(); + namePieces.Add(arrayIndex); + } + else + { + throw new ArgumentException($"Unexpected expression type {currentExpression} for a field name."); + } + } + + return new BsonString(string.Join(".", namePieces)); + } + } + else + { + var constantValue = GetValueFromExpression(expression); + return BsonValue.Create(constantValue); + } + } + + private static BsonValue BuildMethodQuery(MethodCallExpression expression) + { + var methodDefinition = expression.Method; + if (methodDefinition.IsGenericMethod) + { + methodDefinition = methodDefinition.GetGenericMethodDefinition(); + } + + IMethodParser methodParser; + lock (MethodParserLockObj) + { + if (!MethodParserMap.TryGetValue(methodDefinition, out methodParser)) + { + throw new InvalidOperationException($"No method parser found for {expression.Method}"); + } + } + + return methodParser.ParseMethod(expression); + } + + private static object GetValueFromExpression(Expression expression) + { + var objectMember = Expression.Convert(expression, typeof(object)); + var getterLambda = Expression.Lambda>(objectMember); + var getter = getterLambda.Compile(); + return getter(); + + //TODO: Leaving the code below as it might perform faster than compiling the lambda + + //var expressionStack = new Stack(expressions); + //var currentValue = (expressionStack.Pop() as ConstantExpression).Value; + + //Expression currentExpression; + //while ((currentExpression = expressionStack.Pop()) != null) + //{ + // if (currentExpression is MemberExpression memberExpression) + // { + // var memberInfo = memberExpression.Member; + // if (memberInfo is PropertyInfo propertyInfo) + // { + // if (expressionStack.Peek() is ConstantExpression constantExpression) + // { + // currentValue = propertyInfo.GetValue(currentValue, new[] { }); + // } + // else + // { + // currentValue = propertyInfo.GetValue(currentValue); + // } + // } + // else if (memberInfo is FieldInfo fieldInfo) + // { + // currentValue = fieldInfo.GetValue(currentValue); + // } + // } + // else + // { + // var constantExpression = currentExpression as ConstantExpression; + // constantExpression. + // //TODO: get value from the array index + // } + //} + } + + private static BsonValue BuildPartialEquality(BinaryExpression binaryExpression) + { + var leftValue = BuildPartialQuery(binaryExpression.Left); + var rightValue = BuildPartialQuery(binaryExpression.Right); + + string fieldName; + BsonValue value; + var expressionType = binaryExpression.NodeType; + + if (binaryExpression.Left.NodeType != ExpressionType.MemberAccess) + { + if (binaryExpression.Right.NodeType == ExpressionType.MemberAccess) + { + //For expressions like "3 < myEntity.MyValue", we need to flip that around + //This flip is because the field name is "left" of the value + //When flipping it, we need to invert the expression to "myEntity.MyValue > 3" + + fieldName = rightValue.AsString; + value = leftValue; + + if (expressionType != ExpressionType.Equal && expressionType != ExpressionType.NotEqual) + { + expressionType = NumericComparatorInversionMap[expressionType]; + } + } + else + { + throw new ArgumentException($"Expected expression type {ExpressionType.MemberAccess} but received {binaryExpression.Right.NodeType}"); + } + } + else + { + fieldName = leftValue.AsString; + value = rightValue; + } + + var expressionOperator = ComparatorToStringMap[expressionType]; + var valueComparison = new BsonDocument { { expressionOperator, value } }; + return new BsonDocument { { fieldName, valueComparison } }; + } + + private static void UnwrapBinaryQuery(BsonArray target, ExpressionType expressionType, BinaryExpression expression) + { + if (expression.Left.NodeType == expressionType) + { + UnwrapBinaryQuery(target, expressionType, expression.Left as BinaryExpression); + } + else + { + target.Add(BuildPartialQuery(expression.Left)); + } + + target.Add(BuildPartialQuery(expression.Right)); + } + } +} diff --git a/src/MongoFramework/Infrastructure/Querying/ICallConverter.cs b/src/MongoFramework/Infrastructure/Querying/IMethodParser.cs similarity index 66% rename from src/MongoFramework/Infrastructure/Querying/ICallConverter.cs rename to src/MongoFramework/Infrastructure/Querying/IMethodParser.cs index 5b4918ef..eec8ba36 100644 --- a/src/MongoFramework/Infrastructure/Querying/ICallConverter.cs +++ b/src/MongoFramework/Infrastructure/Querying/IMethodParser.cs @@ -6,8 +6,8 @@ namespace MongoFramework.Infrastructure.Querying { - public interface ICallConverter + public interface IMethodParser { - BsonDocument Convert(MethodCallExpression expression); + BsonValue ParseMethod(MethodCallExpression expression); } } diff --git a/src/MongoFramework/Infrastructure/Querying/MethodParsers/OrderByParser.cs b/src/MongoFramework/Infrastructure/Querying/MethodParsers/OrderByParser.cs new file mode 100644 index 00000000..d5e222f3 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Querying/MethodParsers/OrderByParser.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using MongoDB.Bson; + +namespace MongoFramework.Infrastructure.Querying.MethodParsers +{ + public class OrderByParser : IMethodParser + { + public static IEnumerable GetSupportedMethods() + { + yield return ExpressionHelper.GetMethodDefinition(() => Queryable.OrderBy(null, (Expression>)null)); + yield return ExpressionHelper.GetMethodDefinition(() => Queryable.OrderByDescending(null, (Expression>)null)); + } + + public BsonValue ParseMethod(MethodCallExpression expression) + { + var direction = 1; + if (expression.Method.Name.EndsWith("Descending")) + { + direction = -1; + } + + return new BsonDocument + { + { + "$sort", + new BsonDocument + { + { + ExpressionParser.BuildPartialQuery(expression.Arguments[1]).AsString, + direction + } + } + } + }; + } + } +} diff --git a/src/MongoFramework/Infrastructure/Querying/MethodParsers/SelectParser.cs b/src/MongoFramework/Infrastructure/Querying/MethodParsers/SelectParser.cs new file mode 100644 index 00000000..07475410 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Querying/MethodParsers/SelectParser.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using MongoDB.Bson; + +namespace MongoFramework.Infrastructure.Querying.MethodParsers +{ + public class SelectParser : IMethodParser + { + public static IEnumerable GetSupportedMethods() + { + yield return ExpressionHelper.GetMethodDefinition(() => Queryable.Select(null, (Expression>)null)); + } + + public BsonValue ParseMethod(MethodCallExpression expression) + { + return new BsonDocument + { + { + "$project", + new BsonDocument + { + { + ExpressionParser.BuildPartialQuery(expression.Arguments[1]).AsString, + "" + } + } + } + }; + } + } +} diff --git a/src/MongoFramework/Infrastructure/Querying/MethodParsers/WhereParser.cs b/src/MongoFramework/Infrastructure/Querying/MethodParsers/WhereParser.cs new file mode 100644 index 00000000..6decaa96 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Querying/MethodParsers/WhereParser.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using MongoDB.Bson; + +namespace MongoFramework.Infrastructure.Querying.MethodParsers +{ + public class WhereParser : IMethodParser + { + public static IEnumerable GetSupportedMethods() + { + yield return ExpressionHelper.GetMethodDefinition(() => Queryable.Where(null, (Expression>)null)); + } + + public BsonValue ParseMethod(MethodCallExpression expression) + { + return new BsonDocument + { + { "$match", ExpressionParser.BuildPartialQuery(expression.Arguments[1]) } + }; + } + } +} diff --git a/src/MongoFramework/Infrastructure/Querying/MethodQueryResult.cs b/src/MongoFramework/Infrastructure/Querying/MethodQueryResult.cs new file mode 100644 index 00000000..02e25012 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Querying/MethodQueryResult.cs @@ -0,0 +1,12 @@ +using System; +using System.Collections.Generic; +using System.Text; +using MongoDB.Bson; + +namespace MongoFramework.Infrastructure.Querying +{ + public class MethodQueryResult + { + public BsonValue Value { get; set; } + } +} diff --git a/src/MongoFramework/Infrastructure/Querying/QueryMapping.cs b/src/MongoFramework/Infrastructure/Querying/QueryMapping.cs deleted file mode 100644 index 1cd84bc2..00000000 --- a/src/MongoFramework/Infrastructure/Querying/QueryMapping.cs +++ /dev/null @@ -1,59 +0,0 @@ -using System; -using System.Collections.Concurrent; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; -using MongoDB.Bson; -using MongoFramework.Infrastructure.Querying.CallConverters; - -namespace MongoFramework.Infrastructure.Querying -{ - public static class QueryMapping - { - public static ConcurrentDictionary CallConverters { get; } = new ConcurrentDictionary(); - - static QueryMapping() - { - AddConverter(WhereConverter.Instance, () => Queryable.Where(default, ExpressionHelper.Stub())); - } - - public static void AddConverter(ICallConverter callConverter, params Expression[] methodExpressions) - { - foreach (var expression in methodExpressions) - { - var methodInfo = ExpressionHelper.GetGenericMethodInfo(expression); - CallConverters.TryAdd(methodInfo, callConverter); - } - } - - public static IEnumerable FromExpression(Expression expression) - { - var currentExpression = expression; - var stages = new Stack(); - - while (currentExpression is MethodCallExpression callExpression) - { - var methodDefinition = callExpression.Method; - if (methodDefinition.IsGenericMethod) - { - methodDefinition = methodDefinition.GetGenericMethodDefinition(); - } - - if (CallConverters.TryGetValue(methodDefinition, out var converter)) - { - var queryStage = converter.Convert(callExpression); - stages.Push(queryStage); - currentExpression = callExpression.Arguments[0]; - } - else - { - throw new InvalidOperationException($"No converter has been configured for {callExpression.Method}"); - } - } - - return stages; - } - } -} diff --git a/src/MongoFramework/Infrastructure/Querying/StageBuilder.cs b/src/MongoFramework/Infrastructure/Querying/StageBuilder.cs new file mode 100644 index 00000000..18a57cb8 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Querying/StageBuilder.cs @@ -0,0 +1,27 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; +using MongoDB.Bson; + +namespace MongoFramework.Infrastructure.Querying +{ + public static class StageBuilder + { + public static IEnumerable BuildFromExpression(Expression expression) + { + var currentExpression = expression; + var stages = new Stack(); + + while (currentExpression is MethodCallExpression methodCallExpression) + { + var stage = ExpressionParser.BuildPartialQuery(currentExpression).AsBsonDocument; + stages.Push(stage); + + currentExpression = methodCallExpression.Arguments[0]; + } + + return stages; + } + } +} diff --git a/tests/MongoFramework.Tests/Infrastructure/Querying/QueryMappingTests.cs b/tests/MongoFramework.Tests/Infrastructure/Querying/QueryMappingTests.cs index ce1ee733..2c48dfbf 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Querying/QueryMappingTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Querying/QueryMappingTests.cs @@ -32,7 +32,7 @@ public void EmptyQueryableHasNoStages() new QueryTestModel() }); - var stages = QueryMapping.FromExpression(queryable.Expression); + var stages = StageBuilder.BuildFromExpression(queryable.Expression); Assert.AreEqual(0, stages.Count()); } @@ -43,7 +43,7 @@ public void Queryable_Where() new QueryTestModel() }).Where(q => q.Id == ""); - var stages = QueryMapping.FromExpression(queryable.Expression); + var stages = StageBuilder.BuildFromExpression(queryable.Expression); Assert.AreEqual(1, stages.Count()); } @@ -54,7 +54,18 @@ public void Queryable_Where_OrderBy() new QueryTestModel() }).Where(q => q.Id == "").OrderBy(q => q.Id); - var stages = QueryMapping.FromExpression(queryable.Expression); + var stages = StageBuilder.BuildFromExpression(queryable.Expression); + Assert.AreEqual(2, stages.Count()); + } + + [TestMethod] + public void Queryable_Where_OrderByDescending() + { + var queryable = Queryable.AsQueryable(new[] { + new QueryTestModel() + }).Where(q => q.Id == "").OrderByDescending(q => q.Id); + + var stages = StageBuilder.BuildFromExpression(queryable.Expression); Assert.AreEqual(2, stages.Count()); } @@ -65,7 +76,7 @@ public void Queryable_Where_OrderBy_Select() new QueryTestModel() }).Where(q => q.Id == "").OrderBy(q => q.Id).Select(q => q.SomeNumberField); - var stages = QueryMapping.FromExpression(queryable.Expression); + var stages = StageBuilder.BuildFromExpression(queryable.Expression); Assert.AreEqual(3, stages.Count()); } } From c7a52e12e2a1f4ceac160f8399bb36c424066218 Mon Sep 17 00:00:00 2001 From: Turnerj Date: Wed, 13 Nov 2019 11:29:40 +1030 Subject: [PATCH 03/18] Tweaks to support super basic select statements PropertyMappingProcessor needed to ignore CanWrite so the properties can be picked up for anonymous types. --- .../Processors/PropertyMappingProcessor.cs | 2 +- .../Querying/ExpressionParser.cs | 25 +++++++++++++++++++ .../Querying/MethodParsers/SelectParser.cs | 10 ++------ .../Querying/QueryMappingTests.cs | 17 ++++++++++++- 4 files changed, 44 insertions(+), 10 deletions(-) diff --git a/src/MongoFramework/Infrastructure/Mapping/Processors/PropertyMappingProcessor.cs b/src/MongoFramework/Infrastructure/Mapping/Processors/PropertyMappingProcessor.cs index 355de1e9..e8a7695d 100644 --- a/src/MongoFramework/Infrastructure/Mapping/Processors/PropertyMappingProcessor.cs +++ b/src/MongoFramework/Infrastructure/Mapping/Processors/PropertyMappingProcessor.cs @@ -18,7 +18,7 @@ public void ApplyMapping(IEntityDefinition definition, BsonClassMap classMap) foreach (var property in properties) { - if (!property.CanRead || !property.CanWrite) + if (!property.CanRead) { continue; } diff --git a/src/MongoFramework/Infrastructure/Querying/ExpressionParser.cs b/src/MongoFramework/Infrastructure/Querying/ExpressionParser.cs index b6858ba7..49730ab2 100644 --- a/src/MongoFramework/Infrastructure/Querying/ExpressionParser.cs +++ b/src/MongoFramework/Infrastructure/Querying/ExpressionParser.cs @@ -100,6 +100,10 @@ public static BsonValue BuildPartialQuery(Expression expression) { return BuildMemberAccessQuery(memberExpression); } + else if (expression is NewExpression newExpression) + { + return BuildInstantiationQuery(newExpression); + } else if (expression is ParameterExpression) { return null; @@ -133,6 +137,27 @@ public static BsonValue BuildPartialQuery(Expression expression) throw new ArgumentException($"Unexpected expression type {expression}"); } + private static BsonValue BuildInstantiationQuery(NewExpression newExpression) + { + var projectionDocument = new BsonDocument + { + { "_id", 0 } + }; + + for (var i = 0; i < newExpression.Members.Count; i++) + { + var fromExpression = BuildPartialQuery(newExpression.Arguments[i]); + + var member = newExpression.Members[i]; + var entityDefinition = EntityMapping.GetOrCreateDefinition(member.DeclaringType); + var entityProperty = entityDefinition.GetProperty(member.Name); + + projectionDocument.Add(entityProperty.ElementName, fromExpression); + } + + return projectionDocument; + } + private static BsonValue BuildMemberAccessQuery(Expression expression) { var walkedExpressions = new Stack(); diff --git a/src/MongoFramework/Infrastructure/Querying/MethodParsers/SelectParser.cs b/src/MongoFramework/Infrastructure/Querying/MethodParsers/SelectParser.cs index 07475410..94247728 100644 --- a/src/MongoFramework/Infrastructure/Querying/MethodParsers/SelectParser.cs +++ b/src/MongoFramework/Infrastructure/Querying/MethodParsers/SelectParser.cs @@ -20,14 +20,8 @@ public BsonValue ParseMethod(MethodCallExpression expression) return new BsonDocument { { - "$project", - new BsonDocument - { - { - ExpressionParser.BuildPartialQuery(expression.Arguments[1]).AsString, - "" - } - } + "$project", + ExpressionParser.BuildPartialQuery(expression.Arguments[1]) } }; } diff --git a/tests/MongoFramework.Tests/Infrastructure/Querying/QueryMappingTests.cs b/tests/MongoFramework.Tests/Infrastructure/Querying/QueryMappingTests.cs index 2c48dfbf..9fb6713b 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Querying/QueryMappingTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Querying/QueryMappingTests.cs @@ -70,7 +70,7 @@ public void Queryable_Where_OrderByDescending() } [TestMethod] - public void Queryable_Where_OrderBy_Select() + public void Queryable_Where_OrderBy_Select_Property() { var queryable = Queryable.AsQueryable(new[] { new QueryTestModel() @@ -79,5 +79,20 @@ public void Queryable_Where_OrderBy_Select() var stages = StageBuilder.BuildFromExpression(queryable.Expression); Assert.AreEqual(3, stages.Count()); } + + [TestMethod] + public void Queryable_Where_OrderBy_Select_New() + { + var queryable = Queryable.AsQueryable(new[] { + new QueryTestModel() + }).Where(q => q.Id == "").OrderBy(q => q.Id).Select(q => new + { + MyOwnCustomId = q.Id, + MyNestedProperty = q.NestedModel.Name + }); + + var stages = StageBuilder.BuildFromExpression(queryable.Expression); + Assert.AreEqual(3, stages.Count()); + } } } From 8d634cbd42786d2eb0aff45028efbe4dcb940cba Mon Sep 17 00:00:00 2001 From: Turnerj Date: Thu, 14 Nov 2019 23:24:45 +1030 Subject: [PATCH 04/18] Overhaul of expression parsing system Second (or third, depending how you count it) of building an expression system base. Supports custom method, member and even expression type translators. --- .../Querying/ExpressionParser.cs | 377 -------------- .../Querying/ExpressionTranslation.cs | 489 ++++++++++++++++++ .../Infrastructure/Querying/IMethodParser.cs | 13 - .../Querying/MethodParsers/OrderByParser.cs | 42 -- .../Querying/MethodParsers/SelectParser.cs | 29 -- .../Querying/MethodParsers/WhereParser.cs | 26 - .../Querying/MethodQueryResult.cs | 12 - .../Infrastructure/Querying/StageBuilder.cs | 2 +- ...pressionHelper.cs => TranslationHelper.cs} | 2 +- .../Querying/TranslatorInterfaces.cs | 23 + .../Querying/Translators/OrderByTranslator.cs | 42 ++ .../Querying/Translators/SelectTranslator.cs | 54 ++ .../Querying/Translators/WhereTranslator.cs | 26 + .../Querying/ExpressionTranslationTests.cs | 89 ++++ .../Querying/QueryMappingTests.cs | 98 ---- .../Infrastructure/Querying/QueryTestBase.cs | 43 ++ .../Querying/StageBuilderTests.cs | 22 + .../Translators/WhereTranslatorTests.cs | 35 ++ 18 files changed, 825 insertions(+), 599 deletions(-) delete mode 100644 src/MongoFramework/Infrastructure/Querying/ExpressionParser.cs create mode 100644 src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs delete mode 100644 src/MongoFramework/Infrastructure/Querying/IMethodParser.cs delete mode 100644 src/MongoFramework/Infrastructure/Querying/MethodParsers/OrderByParser.cs delete mode 100644 src/MongoFramework/Infrastructure/Querying/MethodParsers/SelectParser.cs delete mode 100644 src/MongoFramework/Infrastructure/Querying/MethodParsers/WhereParser.cs delete mode 100644 src/MongoFramework/Infrastructure/Querying/MethodQueryResult.cs rename src/MongoFramework/Infrastructure/Querying/{ExpressionHelper.cs => TranslationHelper.cs} (94%) create mode 100644 src/MongoFramework/Infrastructure/Querying/TranslatorInterfaces.cs create mode 100644 src/MongoFramework/Infrastructure/Querying/Translators/OrderByTranslator.cs create mode 100644 src/MongoFramework/Infrastructure/Querying/Translators/SelectTranslator.cs create mode 100644 src/MongoFramework/Infrastructure/Querying/Translators/WhereTranslator.cs create mode 100644 tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs delete mode 100644 tests/MongoFramework.Tests/Infrastructure/Querying/QueryMappingTests.cs create mode 100644 tests/MongoFramework.Tests/Infrastructure/Querying/QueryTestBase.cs create mode 100644 tests/MongoFramework.Tests/Infrastructure/Querying/StageBuilderTests.cs create mode 100644 tests/MongoFramework.Tests/Infrastructure/Querying/Translators/WhereTranslatorTests.cs diff --git a/src/MongoFramework/Infrastructure/Querying/ExpressionParser.cs b/src/MongoFramework/Infrastructure/Querying/ExpressionParser.cs deleted file mode 100644 index 49730ab2..00000000 --- a/src/MongoFramework/Infrastructure/Querying/ExpressionParser.cs +++ /dev/null @@ -1,377 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; -using MongoDB.Bson; -using MongoFramework.Infrastructure.Mapping; -using MongoFramework.Infrastructure.Querying.MethodParsers; - -namespace MongoFramework.Infrastructure.Querying -{ - public static class ExpressionParser - { - private static readonly IReadOnlyDictionary ComparatorToStringMap = new Dictionary - { - { ExpressionType.Equal, "$eq" }, - { ExpressionType.NotEqual, "$nq" }, - { ExpressionType.LessThan, "$lt" }, - { ExpressionType.GreaterThan, "$gt" }, - { ExpressionType.LessThanOrEqual, "$lte" }, - { ExpressionType.GreaterThanOrEqual, "$gte" } - }; - - private static readonly IReadOnlyDictionary NumericComparatorInversionMap = new Dictionary - { - { ExpressionType.LessThan, ExpressionType.GreaterThan }, - { ExpressionType.GreaterThan, ExpressionType.LessThan }, - { ExpressionType.LessThanOrEqual, ExpressionType.GreaterThanOrEqual }, - { ExpressionType.GreaterThanOrEqual, ExpressionType.LessThanOrEqual } - }; - - private static object MethodParserLockObj = new object(); - - private static readonly Dictionary MethodParserMap = new Dictionary(); - - static ExpressionParser() - { - AddMethodParser(new WhereParser(), WhereParser.GetSupportedMethods()); - AddMethodParser(new OrderByParser(), OrderByParser.GetSupportedMethods()); - AddMethodParser(new SelectParser(), SelectParser.GetSupportedMethods()); - } - - public static void AddMethodParser(IMethodParser parser, IEnumerable methods) - { - lock (MethodParserLockObj) - { - foreach (var method in methods) - { - MethodParserMap.Add(method, parser); - } - } - } - - public static BsonValue BuildPartialQuery(Expression expression) - { - if (expression is BinaryExpression binaryExpression) - { - if (expression.NodeType == ExpressionType.AndAlso) - { - var unwrappedQuery = new BsonArray(); - UnwrapBinaryQuery(unwrappedQuery, ExpressionType.AndAlso, binaryExpression); - - var elements = new BsonElement[unwrappedQuery.Count]; - - for (int i = 0, l = unwrappedQuery.Count; i < l; i++) - { - elements[i] = unwrappedQuery[i].AsBsonDocument.GetElement(0); - } - - return new BsonDocument((IEnumerable)elements); - } - else if (expression.NodeType == ExpressionType.OrElse) - { - var unwrappedQuery = new BsonArray(); - UnwrapBinaryQuery(unwrappedQuery, ExpressionType.OrElse, binaryExpression); - return new BsonDocument - { - { "$or", unwrappedQuery } - }; - } - else if (expression.NodeType == ExpressionType.ArrayIndex) - { - return BuildMemberAccessQuery(binaryExpression); - } - else - { - return BuildPartialEquality(binaryExpression); - } - } - else if (expression is MethodCallExpression methodCallExpression) - { - return BuildMethodQuery(methodCallExpression); - } - else if (expression is ConstantExpression constantExpression) - { - return BsonValue.Create(constantExpression.Value); - } - else if (expression is MemberExpression memberExpression) - { - return BuildMemberAccessQuery(memberExpression); - } - else if (expression is NewExpression newExpression) - { - return BuildInstantiationQuery(newExpression); - } - else if (expression is ParameterExpression) - { - return null; - } - else if (expression is UnaryExpression unaryExpression) - { - if (unaryExpression.NodeType == ExpressionType.Not) - { - string operatorName; - if (unaryExpression.Operand.NodeType == ExpressionType.OrElse) - { - operatorName = "$nor"; - } - else - { - operatorName = "$not"; - } - - return new BsonDocument - { - { operatorName, BuildPartialQuery(unaryExpression.Operand) } - }; - } - else if (unaryExpression.NodeType == ExpressionType.Quote) - { - var nestedLambda = unaryExpression.Operand as LambdaExpression; - return BuildPartialQuery(nestedLambda.Body); - } - } - - throw new ArgumentException($"Unexpected expression type {expression}"); - } - - private static BsonValue BuildInstantiationQuery(NewExpression newExpression) - { - var projectionDocument = new BsonDocument - { - { "_id", 0 } - }; - - for (var i = 0; i < newExpression.Members.Count; i++) - { - var fromExpression = BuildPartialQuery(newExpression.Arguments[i]); - - var member = newExpression.Members[i]; - var entityDefinition = EntityMapping.GetOrCreateDefinition(member.DeclaringType); - var entityProperty = entityDefinition.GetProperty(member.Name); - - projectionDocument.Add(entityProperty.ElementName, fromExpression); - } - - return projectionDocument; - } - - private static BsonValue BuildMemberAccessQuery(Expression expression) - { - var walkedExpressions = new Stack(); - var currentUnrolledExpression = expression; - - MethodCallExpression outerMostMethodExpression = null; - - while (true) - { - if (currentUnrolledExpression is BinaryExpression binaryExpression) - { - if (binaryExpression.NodeType == ExpressionType.ArrayIndex) - { - //The index is on the right (a ConstantExpression) - walkedExpressions.Push(binaryExpression.Right); - - //The parent expression is on the left - currentUnrolledExpression = binaryExpression.Left; - } - else - { - throw new ArgumentException($"Unexpected node type {expression.NodeType}. Expected {ExpressionType.ArrayIndex}."); - } - } - else if (currentUnrolledExpression is MethodCallExpression methodCallExpression) - { - if (outerMostMethodExpression == null) - { - outerMostMethodExpression = methodCallExpression; - } - - walkedExpressions.Push(methodCallExpression); - currentUnrolledExpression = methodCallExpression.Object; - } - else if (currentUnrolledExpression is MemberExpression memberExpression) - { - walkedExpressions.Push(memberExpression); - currentUnrolledExpression = memberExpression.Expression; - } - else if (currentUnrolledExpression is ParameterExpression || currentUnrolledExpression is ConstantExpression) - { - walkedExpressions.Push(currentUnrolledExpression); - break; - } - else - { - throw new ArgumentException($"Unexpected node type {expression.NodeType}. Expected {ExpressionType.ArrayIndex}."); - } - } - - var firstExpression = walkedExpressions.Peek(); - - if (firstExpression is ParameterExpression) - { - //ParameterExpression means we can't determine the value - //If we find a method in the path, discard everything after it and treat this as a method - if (outerMostMethodExpression != null) - { - return BuildMethodQuery(outerMostMethodExpression); - } - else - { - //When no methods are found, treat this as a straight field name - var namePieces = new List(); - - //Remove the parameter expression, we don't need it as part of the field name - walkedExpressions.Pop(); - - foreach (var currentExpression in walkedExpressions) - { - if (currentExpression is MemberExpression memberExpression) - { - var member = memberExpression.Member; - var entityDefinition = EntityMapping.GetOrCreateDefinition(member.DeclaringType); - var entityProperty = entityDefinition.GetProperty(member.Name); - namePieces.Add(entityProperty.ElementName); - } - else if (currentExpression is ConstantExpression constantExpression) - { - var arrayIndex = constantExpression.Value.ToString(); - namePieces.Add(arrayIndex); - } - else - { - throw new ArgumentException($"Unexpected expression type {currentExpression} for a field name."); - } - } - - return new BsonString(string.Join(".", namePieces)); - } - } - else - { - var constantValue = GetValueFromExpression(expression); - return BsonValue.Create(constantValue); - } - } - - private static BsonValue BuildMethodQuery(MethodCallExpression expression) - { - var methodDefinition = expression.Method; - if (methodDefinition.IsGenericMethod) - { - methodDefinition = methodDefinition.GetGenericMethodDefinition(); - } - - IMethodParser methodParser; - lock (MethodParserLockObj) - { - if (!MethodParserMap.TryGetValue(methodDefinition, out methodParser)) - { - throw new InvalidOperationException($"No method parser found for {expression.Method}"); - } - } - - return methodParser.ParseMethod(expression); - } - - private static object GetValueFromExpression(Expression expression) - { - var objectMember = Expression.Convert(expression, typeof(object)); - var getterLambda = Expression.Lambda>(objectMember); - var getter = getterLambda.Compile(); - return getter(); - - //TODO: Leaving the code below as it might perform faster than compiling the lambda - - //var expressionStack = new Stack(expressions); - //var currentValue = (expressionStack.Pop() as ConstantExpression).Value; - - //Expression currentExpression; - //while ((currentExpression = expressionStack.Pop()) != null) - //{ - // if (currentExpression is MemberExpression memberExpression) - // { - // var memberInfo = memberExpression.Member; - // if (memberInfo is PropertyInfo propertyInfo) - // { - // if (expressionStack.Peek() is ConstantExpression constantExpression) - // { - // currentValue = propertyInfo.GetValue(currentValue, new[] { }); - // } - // else - // { - // currentValue = propertyInfo.GetValue(currentValue); - // } - // } - // else if (memberInfo is FieldInfo fieldInfo) - // { - // currentValue = fieldInfo.GetValue(currentValue); - // } - // } - // else - // { - // var constantExpression = currentExpression as ConstantExpression; - // constantExpression. - // //TODO: get value from the array index - // } - //} - } - - private static BsonValue BuildPartialEquality(BinaryExpression binaryExpression) - { - var leftValue = BuildPartialQuery(binaryExpression.Left); - var rightValue = BuildPartialQuery(binaryExpression.Right); - - string fieldName; - BsonValue value; - var expressionType = binaryExpression.NodeType; - - if (binaryExpression.Left.NodeType != ExpressionType.MemberAccess) - { - if (binaryExpression.Right.NodeType == ExpressionType.MemberAccess) - { - //For expressions like "3 < myEntity.MyValue", we need to flip that around - //This flip is because the field name is "left" of the value - //When flipping it, we need to invert the expression to "myEntity.MyValue > 3" - - fieldName = rightValue.AsString; - value = leftValue; - - if (expressionType != ExpressionType.Equal && expressionType != ExpressionType.NotEqual) - { - expressionType = NumericComparatorInversionMap[expressionType]; - } - } - else - { - throw new ArgumentException($"Expected expression type {ExpressionType.MemberAccess} but received {binaryExpression.Right.NodeType}"); - } - } - else - { - fieldName = leftValue.AsString; - value = rightValue; - } - - var expressionOperator = ComparatorToStringMap[expressionType]; - var valueComparison = new BsonDocument { { expressionOperator, value } }; - return new BsonDocument { { fieldName, valueComparison } }; - } - - private static void UnwrapBinaryQuery(BsonArray target, ExpressionType expressionType, BinaryExpression expression) - { - if (expression.Left.NodeType == expressionType) - { - UnwrapBinaryQuery(target, expressionType, expression.Left as BinaryExpression); - } - else - { - target.Add(BuildPartialQuery(expression.Left)); - } - - target.Add(BuildPartialQuery(expression.Right)); - } - } -} diff --git a/src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs b/src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs new file mode 100644 index 00000000..b98242e1 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs @@ -0,0 +1,489 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using MongoDB.Bson; +using MongoFramework.Infrastructure.Mapping; +using MongoFramework.Infrastructure.Querying.Translators; + +namespace MongoFramework.Infrastructure.Querying +{ + public static class ExpressionTranslation + { + private static readonly HashSet DefaultSupportedTypes = new HashSet + { + ExpressionType.Equal, + ExpressionType.NotEqual, + ExpressionType.LessThan, + ExpressionType.GreaterThan, + ExpressionType.LessThanOrEqual, + ExpressionType.GreaterThanOrEqual, + ExpressionType.OrElse, + ExpressionType.AndAlso, + ExpressionType.ArrayIndex + }; + + private static readonly IReadOnlyDictionary ComparatorToStringMap = new Dictionary + { + { ExpressionType.Equal, "$eq" }, + { ExpressionType.NotEqual, "$nq" }, + { ExpressionType.LessThan, "$lt" }, + { ExpressionType.GreaterThan, "$gt" }, + { ExpressionType.LessThanOrEqual, "$lte" }, + { ExpressionType.GreaterThanOrEqual, "$gte" } + }; + + private static readonly IReadOnlyDictionary NumericComparatorInversionMap = new Dictionary + { + { ExpressionType.LessThan, ExpressionType.GreaterThan }, + { ExpressionType.GreaterThan, ExpressionType.LessThan }, + { ExpressionType.LessThanOrEqual, ExpressionType.GreaterThanOrEqual }, + { ExpressionType.GreaterThanOrEqual, ExpressionType.LessThanOrEqual } + }; + + private static readonly Dictionary MethodTranslatorMap = new Dictionary(); + private static readonly Dictionary MemberTranslatorMap = new Dictionary(); + private static readonly Dictionary BinaryTranslatorMap = new Dictionary(); + + static ExpressionTranslation() + { + AddTranslator(new WhereTranslator(), WhereTranslator.GetSupportedMethods()); + AddTranslator(new OrderByTranslator(), OrderByTranslator.GetSupportedMethods()); + AddTranslator(new SelectTranslator(), SelectTranslator.GetSupportedMethods()); + } + + public static void AddTranslator(IMethodTranslator translator, IEnumerable methods) + { + lock (MethodTranslatorMap) + { + foreach (var method in methods) + { + MethodTranslatorMap.Add(method, translator); + } + } + } + + public static void AddTranslator(IMemberTranslator translator, IEnumerable members) + { + lock (MemberTranslatorMap) + { + foreach (var member in members) + { + MemberTranslatorMap.Add(member, translator); + } + } + } + + public static void AddTranslator(IBinaryExpressionTranslator translator, IEnumerable expressionTypes) + { + + lock (BinaryTranslatorMap) + { + foreach (var expressionType in expressionTypes) + { + if (DefaultSupportedTypes.Contains(expressionType)) + { + throw new ArgumentException($"{expressionType} is a default expression type and can not have a custom translator"); + } + + BinaryTranslatorMap.Add(expressionType, translator); + } + } + } + + public static BsonValue TranslateSubExpression(Expression expression) + { + var unwrappedExpression = UnwrapLambda(expression); + var sourceExpression = GetMemberSource(unwrappedExpression); + + if (sourceExpression is ConstantExpression) + { + return TranslateConstant(unwrappedExpression); + } + else + { + if ( + (unwrappedExpression is BinaryExpression && unwrappedExpression.NodeType == ExpressionType.ArrayIndex) || + unwrappedExpression is MemberExpression + ) + { + return TranslateMember(unwrappedExpression); + } + else if (unwrappedExpression is BinaryExpression binaryExpression && !DefaultSupportedTypes.Contains(unwrappedExpression.NodeType)) + { + IBinaryExpressionTranslator binaryExpressionTranslator; + lock (BinaryTranslatorMap) + { + if (!BinaryTranslatorMap.TryGetValue(unwrappedExpression.NodeType, out binaryExpressionTranslator)) + { + throw new ArgumentException($"No binary expression translator found for {unwrappedExpression.NodeType}"); + } + } + + return binaryExpressionTranslator.TranslateBinary(binaryExpression); + } + else if (unwrappedExpression is MethodCallExpression methodCallExpression) + { + return TranslateMethod(methodCallExpression); + } + + throw new ArgumentException($"Unexpected expression type {unwrappedExpression}"); + } + } + + private static string GetFieldNameFromMember(MemberInfo memberInfo) + { + var entityDefinition = EntityMapping.GetOrCreateDefinition(memberInfo.DeclaringType); + var entityProperty = entityDefinition.GetProperty(memberInfo.Name); + return entityProperty.ElementName; + } + + public static BsonString GetFieldName(Expression expression) + { + var partialNamePieces = new Stack(); + var currentExpression = expression; + + while (true) + { + if (currentExpression is BinaryExpression binaryExpression && expression.NodeType == ExpressionType.ArrayIndex) + { + //The index is on the right + var arrayIndex = TranslateSubExpression(binaryExpression.Right); + partialNamePieces.Push(arrayIndex.AsString); + + //The parent expression is on the left + currentExpression = binaryExpression.Left; + } + else if (currentExpression is MemberExpression memberExpression) + { + var fieldName = GetFieldNameFromMember(memberExpression.Member); + partialNamePieces.Push(fieldName); + + currentExpression = memberExpression.Expression; + } + else if (currentExpression is ParameterExpression || currentExpression is ConstantExpression) + { + return string.Join(".", partialNamePieces); + } + else + { + throw new ArgumentException($"Unexpected node type {expression.NodeType}."); + } + } + } + + public static Expression GetMemberSource(Expression expression) + { + var currentExpression = expression; + while (currentExpression != null) + { + if (currentExpression is MemberExpression memberExpression) + { + currentExpression = memberExpression.Expression; + } + else if (currentExpression is BinaryExpression binaryExpression && binaryExpression.NodeType == ExpressionType.ArrayIndex) + { + currentExpression = binaryExpression.Left; + } + else if (currentExpression is MethodCallExpression methodCallExpression) + { + if (methodCallExpression.Object != null) + { + currentExpression = methodCallExpression.Object; + } + else if (methodCallExpression.Arguments.Count > 0) + { + currentExpression = methodCallExpression.Arguments[0]; + } + else + { + return currentExpression; + } + } + else if (currentExpression is ParameterExpression || currentExpression is ConstantExpression) + { + return currentExpression; + } + else + { + throw new ArgumentException($"Unable to determine source expression for {currentExpression}"); + } + } + + return currentExpression; + } + + public static BsonDocument TranslateConditional(Expression expression) + { + var localExpression = UnwrapLambda(expression); + + static void UnwrapBinaryQuery(BsonArray target, ExpressionType expressionType, BinaryExpression expression) + { + if (expression.Left.NodeType == expressionType) + { + UnwrapBinaryQuery(target, expressionType, expression.Left as BinaryExpression); + } + else + { + target.Add(TranslateSubExpression(expression.Left)); + } + + target.Add(TranslateSubExpression(expression.Right)); + } + + if (localExpression is BinaryExpression binaryExpression) + { + if (localExpression.NodeType == ExpressionType.AndAlso) + { + var unwrappedQuery = new BsonArray(); + UnwrapBinaryQuery(unwrappedQuery, ExpressionType.AndAlso, binaryExpression); + + var elements = new BsonElement[unwrappedQuery.Count]; + + for (int i = 0, l = unwrappedQuery.Count; i < l; i++) + { + elements[i] = unwrappedQuery[i].AsBsonDocument.GetElement(0); + } + + return new BsonDocument((IEnumerable)elements); + } + else if (localExpression.NodeType == ExpressionType.OrElse) + { + var unwrappedQuery = new BsonArray(); + UnwrapBinaryQuery(unwrappedQuery, ExpressionType.OrElse, binaryExpression); + return new BsonDocument + { + { "$or", unwrappedQuery } + }; + } + else if (ComparatorToStringMap.Keys.Contains(localExpression.NodeType)) + { + var leftValue = TranslateSubExpression(binaryExpression.Left); + var rightValue = TranslateSubExpression(binaryExpression.Right); + + string fieldName; + BsonValue value; + var expressionType = binaryExpression.NodeType; + + if (binaryExpression.Left.NodeType == ExpressionType.Constant) + { + //For expressions like "3 < myEntity.MyValue", we need to flip that around + //This flip is because the field name is "left" of the value + //When flipping it, we need to invert the expression to "myEntity.MyValue > 3" + + fieldName = rightValue.AsString; + value = leftValue; + + if (expressionType != ExpressionType.Equal && expressionType != ExpressionType.NotEqual) + { + expressionType = NumericComparatorInversionMap[expressionType]; + } + } + else + { + fieldName = leftValue.AsString; + value = rightValue; + } + + var expressionOperator = ComparatorToStringMap[expressionType]; + var valueComparison = new BsonDocument { { expressionOperator, value } }; + return new BsonDocument { { fieldName, valueComparison } }; + } + } + else if (localExpression is UnaryExpression unaryExpression && unaryExpression.NodeType == ExpressionType.Not) + { + string operatorName; + if (unaryExpression.Operand.NodeType == ExpressionType.OrElse) + { + operatorName = "$nor"; + } + else + { + operatorName = "$not"; + } + + return new BsonDocument + { + { operatorName, TranslateConditional(unaryExpression.Operand) } + }; + } + + throw new ArgumentException($"Unexpected node type {expression.NodeType} for a conditional statement"); + } + + public static BsonValue TranslateConstant(Expression expression) + { + object value; + if (expression is ConstantExpression constantExpression) + { + value = constantExpression.Value; + } + else + { + var objectMember = Expression.Convert(expression, typeof(object)); + var getterLambda = Expression.Lambda>(objectMember); + var getter = getterLambda.Compile(); + value = getter(); + + //TODO: Leaving the code below as it might perform faster than compiling the lambda + + //var expressionStack = new Stack(expressions); + //var currentValue = (expressionStack.Pop() as ConstantExpression).Value; + + //Expression currentExpression; + //while ((currentExpression = expressionStack.Pop()) != null) + //{ + // if (currentExpression is MemberExpression memberExpression) + // { + // var memberInfo = memberExpression.Member; + // if (memberInfo is PropertyInfo propertyInfo) + // { + // if (expressionStack.Peek() is ConstantExpression constantExpression) + // { + // currentValue = propertyInfo.GetValue(currentValue, new[] { }); + // } + // else + // { + // currentValue = propertyInfo.GetValue(currentValue); + // } + // } + // else if (memberInfo is FieldInfo fieldInfo) + // { + // currentValue = fieldInfo.GetValue(currentValue); + // } + // } + // else + // { + // var constantExpression = currentExpression as ConstantExpression; + // constantExpression. + // //TODO: get value from the array index + // } + //} + } + + return BsonValue.Create(value); + } + + public static BsonDocument TranslateInstantiation(Expression expression) + { + var result = new BsonDocument(); + + if (expression is MemberInitExpression memberInitExpression) + { + for (var i = 0; i < memberInitExpression.Bindings.Count; i++) + { + var binding = memberInitExpression.Bindings[i]; + + if (binding.BindingType != MemberBindingType.Assignment) + { + throw new ArgumentException($"Unexpected binding type {binding.BindingType}", nameof(expression)); + } + else if (binding is MemberAssignment memberAssignment) + { + var mappedName = GetFieldNameFromMember(memberAssignment.Member); + result.Add(mappedName, TranslateSubExpression(memberAssignment.Expression)); + } + } + } + else if (expression is NewExpression newExpression) + { + for (var i = 0; i < newExpression.Members.Count; i++) + { + var mappedName = GetFieldNameFromMember(newExpression.Members[i]); + result.Add(mappedName, TranslateSubExpression(newExpression.Arguments[i])); + } + } + else + { + throw new ArgumentException($"Unsupported type of instantiation {expression}"); + } + + return result; + } + + public static BsonValue TranslateMember(Expression expression) + { + var walkedExpressions = new Stack(); + var currentExpression = expression; + + while (currentExpression != null) + { + if (currentExpression is BinaryExpression binaryExpression && binaryExpression.NodeType == ExpressionType.ArrayIndex) + { + walkedExpressions.Push(currentExpression); + currentExpression = binaryExpression.Left; + } + else if (currentExpression is MethodCallExpression methodCallExpression) + { + return TranslateMethod(methodCallExpression, walkedExpressions); + } + else if (currentExpression is MemberExpression memberExpression) + { + IMemberTranslator memberParser; + lock (MemberTranslatorMap) + { + MemberTranslatorMap.TryGetValue(memberExpression.Member, out memberParser); + } + + if (memberParser != null) + { + return memberParser.TranslateMember(memberExpression, walkedExpressions); + } + else + { + walkedExpressions.Push(currentExpression); + currentExpression = memberExpression.Expression; + } + } + else if (currentExpression is ParameterExpression) + { + return GetFieldName(expression); + } + else + { + throw new ArgumentException($"Unexpected node type {expression.NodeType}"); + } + } + + return BsonNull.Value; + } + + public static BsonValue TranslateMethod(MethodCallExpression expression, IEnumerable methodSuffixExpressions = default) + { + var methodDefinition = expression.Method; + if (methodDefinition.IsGenericMethod) + { + methodDefinition = methodDefinition.GetGenericMethodDefinition(); + } + + IMethodTranslator methodParser; + lock (MethodTranslatorMap) + { + if (!MethodTranslatorMap.TryGetValue(methodDefinition, out methodParser)) + { + throw new InvalidOperationException($"No method translator found for {expression.Method}"); + } + } + + return methodParser.TranslateMethod(expression, methodSuffixExpressions); + } + + public static Expression UnwrapLambda(Expression expression) + { + var localExpression = expression; + if (localExpression.NodeType == ExpressionType.Quote && localExpression is UnaryExpression unaryExpression) + { + localExpression = unaryExpression.Operand; + } + + if (localExpression.NodeType == ExpressionType.Lambda && localExpression is LambdaExpression lambdaExpression) + { + localExpression = lambdaExpression.Body; + } + + return localExpression; + } + } +} diff --git a/src/MongoFramework/Infrastructure/Querying/IMethodParser.cs b/src/MongoFramework/Infrastructure/Querying/IMethodParser.cs deleted file mode 100644 index eec8ba36..00000000 --- a/src/MongoFramework/Infrastructure/Querying/IMethodParser.cs +++ /dev/null @@ -1,13 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq.Expressions; -using System.Text; -using MongoDB.Bson; - -namespace MongoFramework.Infrastructure.Querying -{ - public interface IMethodParser - { - BsonValue ParseMethod(MethodCallExpression expression); - } -} diff --git a/src/MongoFramework/Infrastructure/Querying/MethodParsers/OrderByParser.cs b/src/MongoFramework/Infrastructure/Querying/MethodParsers/OrderByParser.cs deleted file mode 100644 index d5e222f3..00000000 --- a/src/MongoFramework/Infrastructure/Querying/MethodParsers/OrderByParser.cs +++ /dev/null @@ -1,42 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; -using MongoDB.Bson; - -namespace MongoFramework.Infrastructure.Querying.MethodParsers -{ - public class OrderByParser : IMethodParser - { - public static IEnumerable GetSupportedMethods() - { - yield return ExpressionHelper.GetMethodDefinition(() => Queryable.OrderBy(null, (Expression>)null)); - yield return ExpressionHelper.GetMethodDefinition(() => Queryable.OrderByDescending(null, (Expression>)null)); - } - - public BsonValue ParseMethod(MethodCallExpression expression) - { - var direction = 1; - if (expression.Method.Name.EndsWith("Descending")) - { - direction = -1; - } - - return new BsonDocument - { - { - "$sort", - new BsonDocument - { - { - ExpressionParser.BuildPartialQuery(expression.Arguments[1]).AsString, - direction - } - } - } - }; - } - } -} diff --git a/src/MongoFramework/Infrastructure/Querying/MethodParsers/SelectParser.cs b/src/MongoFramework/Infrastructure/Querying/MethodParsers/SelectParser.cs deleted file mode 100644 index 94247728..00000000 --- a/src/MongoFramework/Infrastructure/Querying/MethodParsers/SelectParser.cs +++ /dev/null @@ -1,29 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; -using MongoDB.Bson; - -namespace MongoFramework.Infrastructure.Querying.MethodParsers -{ - public class SelectParser : IMethodParser - { - public static IEnumerable GetSupportedMethods() - { - yield return ExpressionHelper.GetMethodDefinition(() => Queryable.Select(null, (Expression>)null)); - } - - public BsonValue ParseMethod(MethodCallExpression expression) - { - return new BsonDocument - { - { - "$project", - ExpressionParser.BuildPartialQuery(expression.Arguments[1]) - } - }; - } - } -} diff --git a/src/MongoFramework/Infrastructure/Querying/MethodParsers/WhereParser.cs b/src/MongoFramework/Infrastructure/Querying/MethodParsers/WhereParser.cs deleted file mode 100644 index 6decaa96..00000000 --- a/src/MongoFramework/Infrastructure/Querying/MethodParsers/WhereParser.cs +++ /dev/null @@ -1,26 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Linq.Expressions; -using System.Reflection; -using System.Text; -using MongoDB.Bson; - -namespace MongoFramework.Infrastructure.Querying.MethodParsers -{ - public class WhereParser : IMethodParser - { - public static IEnumerable GetSupportedMethods() - { - yield return ExpressionHelper.GetMethodDefinition(() => Queryable.Where(null, (Expression>)null)); - } - - public BsonValue ParseMethod(MethodCallExpression expression) - { - return new BsonDocument - { - { "$match", ExpressionParser.BuildPartialQuery(expression.Arguments[1]) } - }; - } - } -} diff --git a/src/MongoFramework/Infrastructure/Querying/MethodQueryResult.cs b/src/MongoFramework/Infrastructure/Querying/MethodQueryResult.cs deleted file mode 100644 index 02e25012..00000000 --- a/src/MongoFramework/Infrastructure/Querying/MethodQueryResult.cs +++ /dev/null @@ -1,12 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Text; -using MongoDB.Bson; - -namespace MongoFramework.Infrastructure.Querying -{ - public class MethodQueryResult - { - public BsonValue Value { get; set; } - } -} diff --git a/src/MongoFramework/Infrastructure/Querying/StageBuilder.cs b/src/MongoFramework/Infrastructure/Querying/StageBuilder.cs index 18a57cb8..f7a6bf5a 100644 --- a/src/MongoFramework/Infrastructure/Querying/StageBuilder.cs +++ b/src/MongoFramework/Infrastructure/Querying/StageBuilder.cs @@ -15,7 +15,7 @@ public static IEnumerable BuildFromExpression(Expression expressio while (currentExpression is MethodCallExpression methodCallExpression) { - var stage = ExpressionParser.BuildPartialQuery(currentExpression).AsBsonDocument; + var stage = ExpressionTranslation.TranslateMethod(methodCallExpression).AsBsonDocument; stages.Push(stage); currentExpression = methodCallExpression.Arguments[0]; diff --git a/src/MongoFramework/Infrastructure/Querying/ExpressionHelper.cs b/src/MongoFramework/Infrastructure/Querying/TranslationHelper.cs similarity index 94% rename from src/MongoFramework/Infrastructure/Querying/ExpressionHelper.cs rename to src/MongoFramework/Infrastructure/Querying/TranslationHelper.cs index fc454f9e..f53f502c 100644 --- a/src/MongoFramework/Infrastructure/Querying/ExpressionHelper.cs +++ b/src/MongoFramework/Infrastructure/Querying/TranslationHelper.cs @@ -8,7 +8,7 @@ namespace MongoFramework.Infrastructure.Querying { - public static class ExpressionHelper + public static class TranslationHelper { public static MethodInfo GetMethodDefinition(Expression expression) { diff --git a/src/MongoFramework/Infrastructure/Querying/TranslatorInterfaces.cs b/src/MongoFramework/Infrastructure/Querying/TranslatorInterfaces.cs new file mode 100644 index 00000000..6e953b26 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Querying/TranslatorInterfaces.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Text; +using MongoDB.Bson; + +namespace MongoFramework.Infrastructure.Querying +{ + public interface IMethodTranslator + { + BsonValue TranslateMethod(MethodCallExpression expression, IEnumerable suffixExpressions = default); + } + + public interface IMemberTranslator + { + BsonValue TranslateMember(MemberExpression expression, IEnumerable suffixExpressions = default); + } + + public interface IBinaryExpressionTranslator + { + BsonValue TranslateBinary(BinaryExpression expression); + } +} diff --git a/src/MongoFramework/Infrastructure/Querying/Translators/OrderByTranslator.cs b/src/MongoFramework/Infrastructure/Querying/Translators/OrderByTranslator.cs new file mode 100644 index 00000000..9d3861ef --- /dev/null +++ b/src/MongoFramework/Infrastructure/Querying/Translators/OrderByTranslator.cs @@ -0,0 +1,42 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using MongoDB.Bson; + +namespace MongoFramework.Infrastructure.Querying.Translators +{ + public class OrderByTranslator : IMethodTranslator + { + public static IEnumerable GetSupportedMethods() + { + yield return TranslationHelper.GetMethodDefinition(() => Queryable.OrderBy(null, (Expression>)null)); + yield return TranslationHelper.GetMethodDefinition(() => Queryable.OrderByDescending(null, (Expression>)null)); + } + + public BsonValue TranslateMethod(MethodCallExpression expression, IEnumerable methodSuffixExpressions = default) + { + var direction = 1; + if (expression.Method.Name.EndsWith("Descending")) + { + direction = -1; + } + + return new BsonDocument + { + { + "$sort", + new BsonDocument + { + { + ExpressionTranslation.TranslateSubExpression(expression.Arguments[1]).AsString, + direction + } + } + } + }; + } + } +} diff --git a/src/MongoFramework/Infrastructure/Querying/Translators/SelectTranslator.cs b/src/MongoFramework/Infrastructure/Querying/Translators/SelectTranslator.cs new file mode 100644 index 00000000..c4055a65 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Querying/Translators/SelectTranslator.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using MongoDB.Bson; + +namespace MongoFramework.Infrastructure.Querying.Translators +{ + public class SelectTranslator : IMethodTranslator + { + public static IEnumerable GetSupportedMethods() + { + yield return TranslationHelper.GetMethodDefinition(() => Queryable.Select(null, (Expression>)null)); + } + + public BsonValue TranslateMethod(MethodCallExpression expression, IEnumerable methodSuffixExpressions = default) + { + return new BsonDocument + { + { + "$project", + ParseExpression(expression.Arguments[1]) + } + }; + } + + private BsonDocument ParseExpression(Expression expression) + { + var localExpression = ExpressionTranslation.UnwrapLambda(expression); + + BsonDocument document; + if (localExpression is MemberExpression memberExpression) + { + //eg: Select(e => e.MyProperty.CanBeNested) + var fieldName = ExpressionTranslation.GetFieldName(memberExpression).AsString; + document = new BsonDocument + { + { fieldName, "$" + fieldName } + }; + } + else + { + //eg: Select(e => new { MyProperty = e.SomeOtherProperty.CanBeNested }) + //eg: Select(e => new SomeKnownType { MyProperty = e.SomeOtherProperty.CanBeNested }) + document = ExpressionTranslation.TranslateInstantiation(localExpression); + } + + document.Add("_id", 0); + return document; + } + } +} diff --git a/src/MongoFramework/Infrastructure/Querying/Translators/WhereTranslator.cs b/src/MongoFramework/Infrastructure/Querying/Translators/WhereTranslator.cs new file mode 100644 index 00000000..45221ca6 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Querying/Translators/WhereTranslator.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using MongoDB.Bson; + +namespace MongoFramework.Infrastructure.Querying.Translators +{ + public class WhereTranslator : IMethodTranslator + { + public static IEnumerable GetSupportedMethods() + { + yield return TranslationHelper.GetMethodDefinition(() => Queryable.Where(null, (Expression>)null)); + } + + public BsonValue TranslateMethod(MethodCallExpression expression, IEnumerable methodSuffixExpressions = default) + { + return new BsonDocument + { + { "$match", ExpressionTranslation.TranslateConditional(expression.Arguments[1]) } + }; + } + } +} diff --git a/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs b/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs new file mode 100644 index 00000000..51aeb0d7 --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Bson; +using MongoFramework.Infrastructure.Querying; +using MongoFramework.Infrastructure.Querying.Translators; + +namespace MongoFramework.Tests.Infrastructure.Querying +{ + [TestClass] + public class ExpressionTranslationTests : QueryTestBase + { + [TestMethod] + public void TranslateConditional_Equals() + { + var expression = GetConditional(e => e.Id == ""); + var result = ExpressionTranslation.TranslateConditional(expression); + var expected = new BsonDocument + { + { "Id", new BsonDocument { { "$eq", "" } } } + }; + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void TranslateConditional_NotEquals() + { + var expression = GetConditional(e => e.Id != ""); + var result = ExpressionTranslation.TranslateConditional(expression); + var expected = new BsonDocument + { + { "Id", new BsonDocument { { "$nq", "" } } } + }; + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void TranslateConditional_LessThan() + { + var expression = GetConditional(e => e.SingleNumber < 5); + var result = ExpressionTranslation.TranslateConditional(expression); + var expected = new BsonDocument + { + { "SingleNumber", new BsonDocument { { "$lt", 5 } } } + }; + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void TranslateConditional_GreaterThan() + { + var expression = GetConditional(e => e.SingleNumber > 5); + var result = ExpressionTranslation.TranslateConditional(expression); + var expected = new BsonDocument + { + { "SingleNumber", new BsonDocument { { "$gt", 5 } } } + }; + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void TranslateConditional_LessThanOrEqual() + { + var expression = GetConditional(e => e.SingleNumber <= 5); + var result = ExpressionTranslation.TranslateConditional(expression); + var expected = new BsonDocument + { + { "SingleNumber", new BsonDocument { { "$lte", 5 } } } + }; + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void TranslateConditional_GreaterThanOrEqual() + { + var expression = GetConditional(e => e.SingleNumber >= 5); + var result = ExpressionTranslation.TranslateConditional(expression); + var expected = new BsonDocument + { + { "SingleNumber", new BsonDocument { { "$gte", 5 } } } + }; + Assert.AreEqual(expected, result); + } + } +} diff --git a/tests/MongoFramework.Tests/Infrastructure/Querying/QueryMappingTests.cs b/tests/MongoFramework.Tests/Infrastructure/Querying/QueryMappingTests.cs deleted file mode 100644 index 9fb6713b..00000000 --- a/tests/MongoFramework.Tests/Infrastructure/Querying/QueryMappingTests.cs +++ /dev/null @@ -1,98 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; -using Microsoft.VisualStudio.TestTools.UnitTesting; -using MongoFramework.Infrastructure.Querying; - -namespace MongoFramework.Tests.Infrastructure.Querying -{ - [TestClass] - public class QueryMappingTests : TestBase - { - private class QueryTestModel - { - public string Id { get; set; } - public int SomeNumberField { get; set; } - public string AnotherStringField { get; set; } - public NestedQueryTestModel NestedModel { get; set; } - } - - private class NestedQueryTestModel - { - public string Name { get; set; } - public int Number { get; set; } - } - - [TestMethod] - public void EmptyQueryableHasNoStages() - { - var queryable = Queryable.AsQueryable(new[] { - new QueryTestModel() - }); - - var stages = StageBuilder.BuildFromExpression(queryable.Expression); - Assert.AreEqual(0, stages.Count()); - } - - [TestMethod] - public void Queryable_Where() - { - var queryable = Queryable.AsQueryable(new[] { - new QueryTestModel() - }).Where(q => q.Id == ""); - - var stages = StageBuilder.BuildFromExpression(queryable.Expression); - Assert.AreEqual(1, stages.Count()); - } - - [TestMethod] - public void Queryable_Where_OrderBy() - { - var queryable = Queryable.AsQueryable(new[] { - new QueryTestModel() - }).Where(q => q.Id == "").OrderBy(q => q.Id); - - var stages = StageBuilder.BuildFromExpression(queryable.Expression); - Assert.AreEqual(2, stages.Count()); - } - - [TestMethod] - public void Queryable_Where_OrderByDescending() - { - var queryable = Queryable.AsQueryable(new[] { - new QueryTestModel() - }).Where(q => q.Id == "").OrderByDescending(q => q.Id); - - var stages = StageBuilder.BuildFromExpression(queryable.Expression); - Assert.AreEqual(2, stages.Count()); - } - - [TestMethod] - public void Queryable_Where_OrderBy_Select_Property() - { - var queryable = Queryable.AsQueryable(new[] { - new QueryTestModel() - }).Where(q => q.Id == "").OrderBy(q => q.Id).Select(q => q.SomeNumberField); - - var stages = StageBuilder.BuildFromExpression(queryable.Expression); - Assert.AreEqual(3, stages.Count()); - } - - [TestMethod] - public void Queryable_Where_OrderBy_Select_New() - { - var queryable = Queryable.AsQueryable(new[] { - new QueryTestModel() - }).Where(q => q.Id == "").OrderBy(q => q.Id).Select(q => new - { - MyOwnCustomId = q.Id, - MyNestedProperty = q.NestedModel.Name - }); - - var stages = StageBuilder.BuildFromExpression(queryable.Expression); - Assert.AreEqual(3, stages.Count()); - } - } -} diff --git a/tests/MongoFramework.Tests/Infrastructure/Querying/QueryTestBase.cs b/tests/MongoFramework.Tests/Infrastructure/Querying/QueryTestBase.cs new file mode 100644 index 00000000..967bb6f6 --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Querying/QueryTestBase.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using MongoFramework.Infrastructure.Querying; + +namespace MongoFramework.Tests.Infrastructure.Querying +{ + public abstract class QueryTestBase : TestBase + { + protected class QueryTestModel + { + public string Id { get; set; } + public string SingleString { get; set; } + public DateTime SingleDateTime { get; set; } + public TimeSpan SingleTimeSpan { get; set; } + public Uri SingleUri { get; set; } + public int SingleNumber { get; set; } + + public string[] ArrayOfStrings { get; set; } + public int[] ArrayOfNumbers { get; set; } + + public QueryTestModel SingleModel { get; set; } + public QueryTestModel[] ArrayOfModels { get; set; } + public IEnumerable EnumerableOfModels { get; set; } + public Dictionary DictionaryOfStrings { get; set; } + } + + protected static Expression GetExpression(Func, IQueryable> query) + { + var queryable = Queryable.AsQueryable(Array.Empty()); + var userQueryable = query(queryable); + return ExpressionTranslation.UnwrapLambda(userQueryable.Expression); + } + + protected static Expression GetConditional(Expression> expression) + { + return ExpressionTranslation.UnwrapLambda(expression); + } + } +} diff --git a/tests/MongoFramework.Tests/Infrastructure/Querying/StageBuilderTests.cs b/tests/MongoFramework.Tests/Infrastructure/Querying/StageBuilderTests.cs new file mode 100644 index 00000000..f9216696 --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Querying/StageBuilderTests.cs @@ -0,0 +1,22 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoFramework.Infrastructure.Querying; + +namespace MongoFramework.Tests.Infrastructure.Querying +{ + [TestClass] + public class StageBuilderTests : QueryTestBase + { + [TestMethod] + public void EmptyQueryableHasNoStages() + { + var expression = GetExpression(q => q); + var stages = StageBuilder.BuildFromExpression(expression); + Assert.AreEqual(0, stages.Count()); + } + } +} diff --git a/tests/MongoFramework.Tests/Infrastructure/Querying/Translators/WhereTranslatorTests.cs b/tests/MongoFramework.Tests/Infrastructure/Querying/Translators/WhereTranslatorTests.cs new file mode 100644 index 00000000..2630725a --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Querying/Translators/WhereTranslatorTests.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Bson; +using MongoFramework.Infrastructure.Querying.Translators; + +namespace MongoFramework.Tests.Infrastructure.Querying.Translators +{ + [TestClass] + public class WhereTranslatorTests : QueryTestBase + { + [TestMethod] + public void WrapsConditionalStatement() + { + var expression = GetExpression(q => q.Where(e => e.Id == "")); + var result = new WhereTranslator().TranslateMethod(expression as MethodCallExpression); + var expected = new BsonDocument + { + { + "$match", + new BsonDocument + { + { "Id", new BsonDocument { { "$eq", "" } } } + } + } + }; + + Assert.AreEqual(expected, result); + } + } +} From aeebb418328f5a68c5e115335a541c3a8cde5bfa Mon Sep 17 00:00:00 2001 From: Turnerj Date: Sat, 16 Nov 2019 18:08:05 +1030 Subject: [PATCH 05/18] Fixing negation implementation Negating "AndAlso" isn't the same as negating an "OrElse". The "OrElse" can use a "$nor" whereas the "AndAlso" needs to have the individual operations use "$not" around their operator expression. --- .../Querying/ExpressionTranslation.cs | 39 ++++++----- .../Querying/ExpressionTranslationTests.cs | 64 +++++++++++++++++++ 2 files changed, 88 insertions(+), 15 deletions(-) diff --git a/src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs b/src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs index b98242e1..cbb271fe 100644 --- a/src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs +++ b/src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs @@ -215,22 +215,22 @@ public static Expression GetMemberSource(Expression expression) return currentExpression; } - public static BsonDocument TranslateConditional(Expression expression) + public static BsonDocument TranslateConditional(Expression expression, bool negated = false) { var localExpression = UnwrapLambda(expression); - static void UnwrapBinaryQuery(BsonArray target, ExpressionType expressionType, BinaryExpression expression) + static void UnwrapBinaryQuery(BsonArray target, ExpressionType expressionType, BinaryExpression expression, bool negated) { if (expression.Left.NodeType == expressionType) { - UnwrapBinaryQuery(target, expressionType, expression.Left as BinaryExpression); + UnwrapBinaryQuery(target, expressionType, expression.Left as BinaryExpression, negated); } else { - target.Add(TranslateSubExpression(expression.Left)); + target.Add(TranslateConditional(expression.Left, negated)); } - target.Add(TranslateSubExpression(expression.Right)); + target.Add(TranslateConditional(expression.Right, negated)); } if (localExpression is BinaryExpression binaryExpression) @@ -238,7 +238,7 @@ static void UnwrapBinaryQuery(BsonArray target, ExpressionType expressionType, B if (localExpression.NodeType == ExpressionType.AndAlso) { var unwrappedQuery = new BsonArray(); - UnwrapBinaryQuery(unwrappedQuery, ExpressionType.AndAlso, binaryExpression); + UnwrapBinaryQuery(unwrappedQuery, ExpressionType.AndAlso, binaryExpression, negated); var elements = new BsonElement[unwrappedQuery.Count]; @@ -252,7 +252,7 @@ static void UnwrapBinaryQuery(BsonArray target, ExpressionType expressionType, B else if (localExpression.NodeType == ExpressionType.OrElse) { var unwrappedQuery = new BsonArray(); - UnwrapBinaryQuery(unwrappedQuery, ExpressionType.OrElse, binaryExpression); + UnwrapBinaryQuery(unwrappedQuery, ExpressionType.OrElse, binaryExpression, negated); return new BsonDocument { { "$or", unwrappedQuery } @@ -289,25 +289,34 @@ static void UnwrapBinaryQuery(BsonArray target, ExpressionType expressionType, B var expressionOperator = ComparatorToStringMap[expressionType]; var valueComparison = new BsonDocument { { expressionOperator, value } }; + + if (negated) + { + valueComparison = new BsonDocument + { + { "$not", valueComparison } + }; + } + return new BsonDocument { { fieldName, valueComparison } }; } } else if (localExpression is UnaryExpression unaryExpression && unaryExpression.NodeType == ExpressionType.Not) { - string operatorName; if (unaryExpression.Operand.NodeType == ExpressionType.OrElse) { - operatorName = "$nor"; + var translatedInnerExpression = TranslateConditional(unaryExpression.Operand, false); + var valueItems = translatedInnerExpression.GetElement("$or").Value; + + return new BsonDocument + { + { "$nor", valueItems } + }; } else { - operatorName = "$not"; + return TranslateConditional(unaryExpression.Operand, !negated); } - - return new BsonDocument - { - { operatorName, TranslateConditional(unaryExpression.Operand) } - }; } throw new ArgumentException($"Unexpected node type {expression.NodeType} for a conditional statement"); diff --git a/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs b/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs index 51aeb0d7..95f06c1e 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs @@ -85,5 +85,69 @@ public void TranslateConditional_GreaterThanOrEqual() }; Assert.AreEqual(expected, result); } + + [TestMethod] + public void TranslateConditional_AndAlso() + { + var expression = GetConditional(e => e.Id == "" && e.SingleNumber >= 5); + var result = ExpressionTranslation.TranslateConditional(expression); + var expected = new BsonDocument + { + { "Id", new BsonDocument { { "$eq", "" } } }, + { "SingleNumber", new BsonDocument { { "$gte", 5 } } } + }; + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void TranslateConditional_OrElse() + { + var expression = GetConditional(e => e.Id == "" || e.SingleNumber >= 5); + var result = ExpressionTranslation.TranslateConditional(expression); + var expected = new BsonDocument + { + { + "$or", + new BsonArray + { + new BsonDocument { { "Id", new BsonDocument { { "$eq", "" } } } }, + new BsonDocument { { "SingleNumber", new BsonDocument { { "$gte", 5 } } } } + } + } + }; + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void TranslateConditional_Not_AndAlso() + { + var expression = GetConditional(e => !(e.Id == "" && e.SingleNumber >= 5)); + var result = ExpressionTranslation.TranslateConditional(expression); + var expected = new BsonDocument + { + { "Id", new BsonDocument { { "$not", new BsonDocument { { "$eq", "" } } } } }, + { "SingleNumber", new BsonDocument { { "$not", new BsonDocument { { "$gte", 5 } } } } } + }; + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void TranslateConditional_Not_OrElse() + { + var expression = GetConditional(e => !(e.Id == "" || e.SingleNumber >= 5)); + var result = ExpressionTranslation.TranslateConditional(expression); + var expected = new BsonDocument + { + { + "$nor", + new BsonArray + { + new BsonDocument { { "Id", new BsonDocument { { "$eq", "" } } } }, + new BsonDocument { { "SingleNumber", new BsonDocument { { "$gte", 5 } } } } + } + } + }; + Assert.AreEqual(expected, result); + } } } From 3373830cb640a6d7bab74e6a12939aaf7d6133cf Mon Sep 17 00:00:00 2001 From: Turnerj Date: Sun, 17 Nov 2019 01:11:46 +1030 Subject: [PATCH 06/18] Added external constants test --- .../Querying/ExpressionTranslationTests.cs | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs b/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs index 95f06c1e..dc46b02e 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs @@ -149,5 +149,19 @@ public void TranslateConditional_Not_OrElse() }; Assert.AreEqual(expected, result); } + + [TestMethod] + public void TranslateConditional_ExternalConstants() + { + var externalData = new BsonDocument { { "Data", "Hello World" } }; + + var expression = GetConditional(e => e.Id == externalData["Data"].AsString); + var result = ExpressionTranslation.TranslateConditional(expression); + var expected = new BsonDocument + { + { "Id", new BsonDocument { { "$eq", "Hello World" } } } + }; + Assert.AreEqual(expected, result); + } } } From 9aa0319be61be5867bac49c564507495a9f154ea Mon Sep 17 00:00:00 2001 From: Turnerj Date: Sun, 17 Nov 2019 17:17:28 +1030 Subject: [PATCH 07/18] Additional complex conditional expression tests --- .../Querying/ExpressionTranslationTests.cs | 79 ++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) diff --git a/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs b/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs index dc46b02e..e02af95d 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs @@ -99,6 +99,20 @@ public void TranslateConditional_AndAlso() Assert.AreEqual(expected, result); } + [TestMethod] + public void TranslateConditional_AndAlso_AndAlso() + { + var expression = GetConditional(e => e.Id == "" && e.SingleNumber >= 5 && e.SingleString == "ABC"); + var result = ExpressionTranslation.TranslateConditional(expression); + var expected = new BsonDocument + { + { "Id", new BsonDocument { { "$eq", "" } } }, + { "SingleNumber", new BsonDocument { { "$gte", 5 } } }, + { "SingleString", new BsonDocument { { "$eq", "ABC" } } } + }; + Assert.AreEqual(expected, result); + } + [TestMethod] public void TranslateConditional_OrElse() { @@ -106,7 +120,7 @@ public void TranslateConditional_OrElse() var result = ExpressionTranslation.TranslateConditional(expression); var expected = new BsonDocument { - { + { "$or", new BsonArray { @@ -118,6 +132,69 @@ public void TranslateConditional_OrElse() Assert.AreEqual(expected, result); } + [TestMethod] + public void TranslateConditional_OrElse_OrElse() + { + var expression = GetConditional(e => e.Id == "" || e.SingleNumber >= 5 || e.SingleString == "ABC"); + var result = ExpressionTranslation.TranslateConditional(expression); + var expected = new BsonDocument + { + { + "$or", + new BsonArray + { + new BsonDocument { { "Id", new BsonDocument { { "$eq", "" } } } }, + new BsonDocument { { "SingleNumber", new BsonDocument { { "$gte", 5 } } } }, + new BsonDocument { { "SingleString", new BsonDocument { { "$eq", "ABC" } } } } + } + } + }; + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void TranslateConditional_AndAlso_OrElse() + { + var expression = GetConditional(e => e.Id == "" && (e.SingleNumber >= 5 || e.SingleString == "ABC")); + var result = ExpressionTranslation.TranslateConditional(expression); + var expected = new BsonDocument + { + { "Id", new BsonDocument { { "$eq", "" } } }, + { + "$or", + new BsonArray + { + new BsonDocument { { "SingleNumber", new BsonDocument { { "$gte", 5 } } } }, + new BsonDocument { { "SingleString", new BsonDocument { { "$eq", "ABC" } } } } + } + } + }; + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void TranslateConditional_OrElse_AndAlso() + { + var expression = GetConditional(e => e.Id == "" || (e.SingleNumber >= 5 && e.SingleString == "ABC")); + var result = ExpressionTranslation.TranslateConditional(expression); + var expected = new BsonDocument + { + { + "$or", + new BsonArray + { + new BsonDocument { { "Id", new BsonDocument { { "$eq", "" } } } }, + new BsonDocument + { + { "SingleNumber", new BsonDocument { { "$gte", 5 } } }, + { "SingleString", new BsonDocument { { "$eq", "ABC" } } } + } + } + } + }; + Assert.AreEqual(expected, result); + } + [TestMethod] public void TranslateConditional_Not_AndAlso() { From 74014eda755ef886388be9fd7b602a1768b3ff31 Mon Sep 17 00:00:00 2001 From: Turnerj Date: Sun, 17 Nov 2019 17:32:57 +1030 Subject: [PATCH 08/18] Added TranslateInstantiation tests --- .../Querying/ExpressionTranslationTests.cs | 41 +++++++++++++++++++ .../Infrastructure/Querying/QueryTestBase.cs | 5 +++ 2 files changed, 46 insertions(+) diff --git a/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs b/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs index e02af95d..9e857cde 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs @@ -240,5 +240,46 @@ public void TranslateConditional_ExternalConstants() }; Assert.AreEqual(expected, result); } + + [TestMethod] + public void TranslateInstantiation_Anonymous() + { + var expression = GetTransform(e => new + { + e.Id, + MyNumber = e.SingleNumber + }); + var result = ExpressionTranslation.TranslateInstantiation(expression); + var expected = new BsonDocument + { + { "Id", "Id" }, + { "MyNumber", "SingleNumber" } + }; + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void TranslateInstantiation_RealType() + { + var expression = GetTransform(e => new QueryTestModel + { + Id = e.Id, + SingleNumber = e.SingleNumber + }); + var result = ExpressionTranslation.TranslateInstantiation(expression); + var expected = new BsonDocument + { + { "Id", "Id" }, + { "SingleNumber", "SingleNumber" } + }; + Assert.AreEqual(expected, result); + } + + [TestMethod, ExpectedException(typeof(ArgumentException))] + public void TranslateInstantiation_InvalidExpression() + { + var expression = GetTransform(e => e.SingleNumber); + ExpressionTranslation.TranslateInstantiation(expression); + } } } diff --git a/tests/MongoFramework.Tests/Infrastructure/Querying/QueryTestBase.cs b/tests/MongoFramework.Tests/Infrastructure/Querying/QueryTestBase.cs index 967bb6f6..2cc18c35 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Querying/QueryTestBase.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Querying/QueryTestBase.cs @@ -39,5 +39,10 @@ protected static Expression GetConditional(Expression { return ExpressionTranslation.UnwrapLambda(expression); } + + protected static Expression GetTransform(Expression> expression) + { + return ExpressionTranslation.UnwrapLambda(expression); + } } } From be42fcaf766325fa91a3cbfb392a95486f5a3f3a Mon Sep 17 00:00:00 2001 From: Turnerj Date: Sun, 17 Nov 2019 17:43:18 +1030 Subject: [PATCH 09/18] Added TranslateMember tests --- .../Querying/ExpressionTranslationTests.cs | 35 +++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs b/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs index 9e857cde..bcb8872c 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Querying/ExpressionTranslationTests.cs @@ -281,5 +281,40 @@ public void TranslateInstantiation_InvalidExpression() var expression = GetTransform(e => e.SingleNumber); ExpressionTranslation.TranslateInstantiation(expression); } + + [TestMethod] + public void TranslateMember_SingleLevelMember() + { + var expression = GetTransform(e => e.SingleString); + var result = ExpressionTranslation.TranslateMember(expression); + var expected = new BsonString("SingleString"); + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void TranslateMember_MultiLevelMember() + { + var expression = GetTransform(e => e.SingleModel.SingleNumber); + var result = ExpressionTranslation.TranslateMember(expression); + var expected = new BsonString("SingleModel.SingleNumber"); + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void TranslateMember_MemberWithArrayIndex_AtStart() + { + var expression = GetTransform(e => e.ArrayOfModels[3].SingleNumber); + var result = ExpressionTranslation.TranslateMember(expression); + var expected = new BsonString("ArrayOfModels.3.SingleNumber"); + Assert.AreEqual(expected, result); + } + [TestMethod] + public void TranslateMember_MemberWithArrayIndex_AtEnd() + { + var expression = GetTransform(e => e.SingleModel.ArrayOfModels[2]); + var result = ExpressionTranslation.TranslateMember(expression); + var expected = new BsonString("SingleModel.ArrayOfModels.2"); + Assert.AreEqual(expected, result); + } } } From 6e6883a54f224fc99e7bdb21e3a0ca6f63cf549a Mon Sep 17 00:00:00 2001 From: Turnerj Date: Sun, 17 Nov 2019 17:43:40 +1030 Subject: [PATCH 10/18] Fixed issues around array index expressions --- .../Infrastructure/Querying/ExpressionTranslation.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs b/src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs index cbb271fe..03ad35ce 100644 --- a/src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs +++ b/src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs @@ -147,11 +147,11 @@ public static BsonString GetFieldName(Expression expression) while (true) { - if (currentExpression is BinaryExpression binaryExpression && expression.NodeType == ExpressionType.ArrayIndex) + if (currentExpression is BinaryExpression binaryExpression && currentExpression.NodeType == ExpressionType.ArrayIndex) { //The index is on the right var arrayIndex = TranslateSubExpression(binaryExpression.Right); - partialNamePieces.Push(arrayIndex.AsString); + partialNamePieces.Push(arrayIndex.ToString()); //The parent expression is on the left currentExpression = binaryExpression.Left; @@ -169,7 +169,7 @@ public static BsonString GetFieldName(Expression expression) } else { - throw new ArgumentException($"Unexpected node type {expression.NodeType}."); + throw new ArgumentException($"Unexpected node type {currentExpression.NodeType}."); } } } From 19d7072b4ad00d7da4932431a7b97dc3d2f90a8a Mon Sep 17 00:00:00 2001 From: Turnerj Date: Sun, 17 Nov 2019 18:11:19 +1030 Subject: [PATCH 11/18] Added DefaultTranslators support --- .../Querying/DefaultTranslators.cs | 17 +++++ .../Querying/ExpressionTranslation.cs | 68 ++++++++++++------- .../Querying/TranslatorInterfaces.cs | 11 ++- .../Querying/Translators/OrderByTranslator.cs | 2 +- .../Querying/Translators/SelectTranslator.cs | 2 +- .../Querying/Translators/WhereTranslator.cs | 2 +- 6 files changed, 72 insertions(+), 30 deletions(-) create mode 100644 src/MongoFramework/Infrastructure/Querying/DefaultTranslators.cs diff --git a/src/MongoFramework/Infrastructure/Querying/DefaultTranslators.cs b/src/MongoFramework/Infrastructure/Querying/DefaultTranslators.cs new file mode 100644 index 00000000..60a85623 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Querying/DefaultTranslators.cs @@ -0,0 +1,17 @@ +using System; +using System.Collections.Generic; +using System.Text; +using MongoFramework.Infrastructure.Querying.Translators; + +namespace MongoFramework.Infrastructure.Querying +{ + public static class DefaultTranslators + { + public static void AddTranslators() + { + ExpressionTranslation.AddTranslator(new WhereTranslator()); + ExpressionTranslation.AddTranslator(new OrderByTranslator()); + ExpressionTranslation.AddTranslator(new SelectTranslator()); + } + } +} diff --git a/src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs b/src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs index 03ad35ce..67befb21 100644 --- a/src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs +++ b/src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs @@ -49,47 +49,67 @@ public static class ExpressionTranslation static ExpressionTranslation() { - AddTranslator(new WhereTranslator(), WhereTranslator.GetSupportedMethods()); - AddTranslator(new OrderByTranslator(), OrderByTranslator.GetSupportedMethods()); - AddTranslator(new SelectTranslator(), SelectTranslator.GetSupportedMethods()); + DefaultTranslators.AddTranslators(); } - public static void AddTranslator(IMethodTranslator translator, IEnumerable methods) + public static void AddTranslator(IQueryTranslator translator) { - lock (MethodTranslatorMap) + if (translator is IMethodTranslator methodTranslator) { - foreach (var method in methods) + lock (MethodTranslatorMap) { - MethodTranslatorMap.Add(method, translator); + foreach (var method in methodTranslator.GetSupportedMethods()) + { + MethodTranslatorMap.Add(method, methodTranslator); + } } } - } - - public static void AddTranslator(IMemberTranslator translator, IEnumerable members) - { - lock (MemberTranslatorMap) + else if (translator is IMemberTranslator memberTranslator) { - foreach (var member in members) + lock (MemberTranslatorMap) { - MemberTranslatorMap.Add(member, translator); + foreach (var member in memberTranslator.GetSupportedMembers()) + { + MemberTranslatorMap.Add(member, memberTranslator); + } } } + else if (translator is IBinaryExpressionTranslator binaryExpressionTranslator) + { + lock (BinaryTranslatorMap) + { + foreach (var expressionType in binaryExpressionTranslator.GetSupportedExpressionTypes()) + { + if (DefaultSupportedTypes.Contains(expressionType)) + { + throw new ArgumentException($"{expressionType} is a default expression type and can not have a custom translator"); + } + + BinaryTranslatorMap.Add(expressionType, binaryExpressionTranslator); + } + } + } + else + { + throw new ArgumentException($"Invalid type of translator. It must implement {nameof(IMethodTranslator)}, {nameof(IMemberTranslator)} or {nameof(IBinaryExpressionTranslator)}.", nameof(translator)); + } } - public static void AddTranslator(IBinaryExpressionTranslator translator, IEnumerable expressionTypes) + public static void ClearTranslators() { + lock (MethodTranslatorMap) + { + MethodTranslatorMap.Clear(); + } - lock (BinaryTranslatorMap) + lock (MemberTranslatorMap) { - foreach (var expressionType in expressionTypes) - { - if (DefaultSupportedTypes.Contains(expressionType)) - { - throw new ArgumentException($"{expressionType} is a default expression type and can not have a custom translator"); - } + MemberTranslatorMap.Clear(); + } - BinaryTranslatorMap.Add(expressionType, translator); - } + lock (BinaryTranslatorMap) + { + BinaryTranslatorMap.Clear(); } } diff --git a/src/MongoFramework/Infrastructure/Querying/TranslatorInterfaces.cs b/src/MongoFramework/Infrastructure/Querying/TranslatorInterfaces.cs index 6e953b26..b15ec312 100644 --- a/src/MongoFramework/Infrastructure/Querying/TranslatorInterfaces.cs +++ b/src/MongoFramework/Infrastructure/Querying/TranslatorInterfaces.cs @@ -1,23 +1,28 @@ using System; using System.Collections.Generic; using System.Linq.Expressions; +using System.Reflection; using System.Text; using MongoDB.Bson; namespace MongoFramework.Infrastructure.Querying { - public interface IMethodTranslator + public interface IQueryTranslator { } + public interface IMethodTranslator : IQueryTranslator { + IEnumerable GetSupportedMethods(); BsonValue TranslateMethod(MethodCallExpression expression, IEnumerable suffixExpressions = default); } - public interface IMemberTranslator + public interface IMemberTranslator : IQueryTranslator { + IEnumerable GetSupportedMembers(); BsonValue TranslateMember(MemberExpression expression, IEnumerable suffixExpressions = default); } - public interface IBinaryExpressionTranslator + public interface IBinaryExpressionTranslator : IQueryTranslator { + IEnumerable GetSupportedExpressionTypes(); BsonValue TranslateBinary(BinaryExpression expression); } } diff --git a/src/MongoFramework/Infrastructure/Querying/Translators/OrderByTranslator.cs b/src/MongoFramework/Infrastructure/Querying/Translators/OrderByTranslator.cs index 9d3861ef..0a6c2784 100644 --- a/src/MongoFramework/Infrastructure/Querying/Translators/OrderByTranslator.cs +++ b/src/MongoFramework/Infrastructure/Querying/Translators/OrderByTranslator.cs @@ -10,7 +10,7 @@ namespace MongoFramework.Infrastructure.Querying.Translators { public class OrderByTranslator : IMethodTranslator { - public static IEnumerable GetSupportedMethods() + public IEnumerable GetSupportedMethods() { yield return TranslationHelper.GetMethodDefinition(() => Queryable.OrderBy(null, (Expression>)null)); yield return TranslationHelper.GetMethodDefinition(() => Queryable.OrderByDescending(null, (Expression>)null)); diff --git a/src/MongoFramework/Infrastructure/Querying/Translators/SelectTranslator.cs b/src/MongoFramework/Infrastructure/Querying/Translators/SelectTranslator.cs index c4055a65..7215340e 100644 --- a/src/MongoFramework/Infrastructure/Querying/Translators/SelectTranslator.cs +++ b/src/MongoFramework/Infrastructure/Querying/Translators/SelectTranslator.cs @@ -10,7 +10,7 @@ namespace MongoFramework.Infrastructure.Querying.Translators { public class SelectTranslator : IMethodTranslator { - public static IEnumerable GetSupportedMethods() + public IEnumerable GetSupportedMethods() { yield return TranslationHelper.GetMethodDefinition(() => Queryable.Select(null, (Expression>)null)); } diff --git a/src/MongoFramework/Infrastructure/Querying/Translators/WhereTranslator.cs b/src/MongoFramework/Infrastructure/Querying/Translators/WhereTranslator.cs index 45221ca6..4de38dc6 100644 --- a/src/MongoFramework/Infrastructure/Querying/Translators/WhereTranslator.cs +++ b/src/MongoFramework/Infrastructure/Querying/Translators/WhereTranslator.cs @@ -10,7 +10,7 @@ namespace MongoFramework.Infrastructure.Querying.Translators { public class WhereTranslator : IMethodTranslator { - public static IEnumerable GetSupportedMethods() + public IEnumerable GetSupportedMethods() { yield return TranslationHelper.GetMethodDefinition(() => Queryable.Where(null, (Expression>)null)); } From 37f822866349a71ea6d56d621fa21148c158cbe1 Mon Sep 17 00:00:00 2001 From: Turnerj Date: Sun, 17 Nov 2019 18:12:45 +1030 Subject: [PATCH 12/18] Re-structuring classes under "Linq" folder --- .../{Querying => Linq/Translation}/DefaultTranslators.cs | 6 +++--- .../{Querying => Linq/Translation}/ExpressionTranslation.cs | 3 +-- .../{Querying => Linq/Translation}/StageBuilder.cs | 2 +- .../{Querying => Linq/Translation}/TranslationHelper.cs | 2 +- .../{Querying => Linq/Translation}/TranslatorInterfaces.cs | 2 +- .../Translation}/Translators/OrderByTranslator.cs | 4 ++-- .../Translation}/Translators/SelectTranslator.cs | 2 +- .../Translation}/Translators/WhereTranslator.cs | 2 +- .../Translation}/ExpressionTranslationTests.cs | 5 ++--- .../{Querying => Linq/Translation}/QueryTestBase.cs | 6 +++--- .../{Querying => Linq/Translation}/StageBuilderTests.cs | 4 ++-- .../Translation}/Translators/WhereTranslatorTests.cs | 4 ++-- 12 files changed, 20 insertions(+), 22 deletions(-) rename src/MongoFramework/Infrastructure/{Querying => Linq/Translation}/DefaultTranslators.cs (70%) rename src/MongoFramework/Infrastructure/{Querying => Linq/Translation}/ExpressionTranslation.cs (99%) rename src/MongoFramework/Infrastructure/{Querying => Linq/Translation}/StageBuilder.cs (91%) rename src/MongoFramework/Infrastructure/{Querying => Linq/Translation}/TranslationHelper.cs (91%) rename src/MongoFramework/Infrastructure/{Querying => Linq/Translation}/TranslatorInterfaces.cs (93%) rename src/MongoFramework/Infrastructure/{Querying => Linq/Translation}/Translators/OrderByTranslator.cs (92%) rename src/MongoFramework/Infrastructure/{Querying => Linq/Translation}/Translators/SelectTranslator.cs (95%) rename src/MongoFramework/Infrastructure/{Querying => Linq/Translation}/Translators/WhereTranslator.cs (90%) rename tests/MongoFramework.Tests/Infrastructure/{Querying => Linq/Translation}/ExpressionTranslationTests.cs (98%) rename tests/MongoFramework.Tests/Infrastructure/{Querying => Linq/Translation}/QueryTestBase.cs (91%) rename tests/MongoFramework.Tests/Infrastructure/{Querying => Linq/Translation}/StageBuilderTests.cs (79%) rename tests/MongoFramework.Tests/Infrastructure/{Querying => Linq/Translation}/Translators/WhereTranslatorTests.cs (83%) diff --git a/src/MongoFramework/Infrastructure/Querying/DefaultTranslators.cs b/src/MongoFramework/Infrastructure/Linq/Translation/DefaultTranslators.cs similarity index 70% rename from src/MongoFramework/Infrastructure/Querying/DefaultTranslators.cs rename to src/MongoFramework/Infrastructure/Linq/Translation/DefaultTranslators.cs index 60a85623..40e2827d 100644 --- a/src/MongoFramework/Infrastructure/Querying/DefaultTranslators.cs +++ b/src/MongoFramework/Infrastructure/Linq/Translation/DefaultTranslators.cs @@ -1,9 +1,9 @@ -using System; +using MongoFramework.Infrastructure.Linq.Translation.Translators; +using System; using System.Collections.Generic; using System.Text; -using MongoFramework.Infrastructure.Querying.Translators; -namespace MongoFramework.Infrastructure.Querying +namespace MongoFramework.Infrastructure.Linq.Translation { public static class DefaultTranslators { diff --git a/src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs b/src/MongoFramework/Infrastructure/Linq/Translation/ExpressionTranslation.cs similarity index 99% rename from src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs rename to src/MongoFramework/Infrastructure/Linq/Translation/ExpressionTranslation.cs index 67befb21..1fdedd04 100644 --- a/src/MongoFramework/Infrastructure/Querying/ExpressionTranslation.cs +++ b/src/MongoFramework/Infrastructure/Linq/Translation/ExpressionTranslation.cs @@ -6,9 +6,8 @@ using System.Text; using MongoDB.Bson; using MongoFramework.Infrastructure.Mapping; -using MongoFramework.Infrastructure.Querying.Translators; -namespace MongoFramework.Infrastructure.Querying +namespace MongoFramework.Infrastructure.Linq.Translation { public static class ExpressionTranslation { diff --git a/src/MongoFramework/Infrastructure/Querying/StageBuilder.cs b/src/MongoFramework/Infrastructure/Linq/Translation/StageBuilder.cs similarity index 91% rename from src/MongoFramework/Infrastructure/Querying/StageBuilder.cs rename to src/MongoFramework/Infrastructure/Linq/Translation/StageBuilder.cs index f7a6bf5a..0e289920 100644 --- a/src/MongoFramework/Infrastructure/Querying/StageBuilder.cs +++ b/src/MongoFramework/Infrastructure/Linq/Translation/StageBuilder.cs @@ -4,7 +4,7 @@ using System.Text; using MongoDB.Bson; -namespace MongoFramework.Infrastructure.Querying +namespace MongoFramework.Infrastructure.Linq.Translation { public static class StageBuilder { diff --git a/src/MongoFramework/Infrastructure/Querying/TranslationHelper.cs b/src/MongoFramework/Infrastructure/Linq/Translation/TranslationHelper.cs similarity index 91% rename from src/MongoFramework/Infrastructure/Querying/TranslationHelper.cs rename to src/MongoFramework/Infrastructure/Linq/Translation/TranslationHelper.cs index f53f502c..f8922149 100644 --- a/src/MongoFramework/Infrastructure/Querying/TranslationHelper.cs +++ b/src/MongoFramework/Infrastructure/Linq/Translation/TranslationHelper.cs @@ -6,7 +6,7 @@ using MongoDB.Bson; using MongoFramework.Infrastructure.Mapping; -namespace MongoFramework.Infrastructure.Querying +namespace MongoFramework.Infrastructure.Linq.Translation { public static class TranslationHelper { diff --git a/src/MongoFramework/Infrastructure/Querying/TranslatorInterfaces.cs b/src/MongoFramework/Infrastructure/Linq/Translation/TranslatorInterfaces.cs similarity index 93% rename from src/MongoFramework/Infrastructure/Querying/TranslatorInterfaces.cs rename to src/MongoFramework/Infrastructure/Linq/Translation/TranslatorInterfaces.cs index b15ec312..70bb5f37 100644 --- a/src/MongoFramework/Infrastructure/Querying/TranslatorInterfaces.cs +++ b/src/MongoFramework/Infrastructure/Linq/Translation/TranslatorInterfaces.cs @@ -5,7 +5,7 @@ using System.Text; using MongoDB.Bson; -namespace MongoFramework.Infrastructure.Querying +namespace MongoFramework.Infrastructure.Linq.Translation { public interface IQueryTranslator { } public interface IMethodTranslator : IQueryTranslator diff --git a/src/MongoFramework/Infrastructure/Querying/Translators/OrderByTranslator.cs b/src/MongoFramework/Infrastructure/Linq/Translation/Translators/OrderByTranslator.cs similarity index 92% rename from src/MongoFramework/Infrastructure/Querying/Translators/OrderByTranslator.cs rename to src/MongoFramework/Infrastructure/Linq/Translation/Translators/OrderByTranslator.cs index 0a6c2784..fe57cf2c 100644 --- a/src/MongoFramework/Infrastructure/Querying/Translators/OrderByTranslator.cs +++ b/src/MongoFramework/Infrastructure/Linq/Translation/Translators/OrderByTranslator.cs @@ -6,7 +6,7 @@ using System.Text; using MongoDB.Bson; -namespace MongoFramework.Infrastructure.Querying.Translators +namespace MongoFramework.Infrastructure.Linq.Translation.Translators { public class OrderByTranslator : IMethodTranslator { @@ -30,7 +30,7 @@ public BsonValue TranslateMethod(MethodCallExpression expression, IEnumerable Date: Sun, 17 Nov 2019 18:19:59 +1030 Subject: [PATCH 13/18] Split the jumbo test class into small test classes --- ...ExpressionTranslationTests_Conditional.cs} | 78 +------------------ ...xpressionTranslationTests_Instantiation.cs | 57 ++++++++++++++ .../ExpressionTranslationTests_Member.cs | 51 ++++++++++++ 3 files changed, 109 insertions(+), 77 deletions(-) rename tests/MongoFramework.Tests/Infrastructure/Linq/Translation/{ExpressionTranslationTests.cs => ExpressionTranslationTests_Conditional.cs} (75%) create mode 100644 tests/MongoFramework.Tests/Infrastructure/Linq/Translation/ExpressionTranslationTests_Instantiation.cs create mode 100644 tests/MongoFramework.Tests/Infrastructure/Linq/Translation/ExpressionTranslationTests_Member.cs diff --git a/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/ExpressionTranslationTests.cs b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/ExpressionTranslationTests_Conditional.cs similarity index 75% rename from tests/MongoFramework.Tests/Infrastructure/Linq/Translation/ExpressionTranslationTests.cs rename to tests/MongoFramework.Tests/Infrastructure/Linq/Translation/ExpressionTranslationTests_Conditional.cs index 8ebb6410..0c812dcd 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/ExpressionTranslationTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/ExpressionTranslationTests_Conditional.cs @@ -11,7 +11,7 @@ namespace MongoFramework.Tests.Infrastructure.Linq.Translation { [TestClass] - public class ExpressionTranslationTests : QueryTestBase + public class ExpressionTranslationTests_Conditional : QueryTestBase { [TestMethod] public void TranslateConditional_Equals() @@ -239,81 +239,5 @@ public void TranslateConditional_ExternalConstants() }; Assert.AreEqual(expected, result); } - - [TestMethod] - public void TranslateInstantiation_Anonymous() - { - var expression = GetTransform(e => new - { - e.Id, - MyNumber = e.SingleNumber - }); - var result = ExpressionTranslation.TranslateInstantiation(expression); - var expected = new BsonDocument - { - { "Id", "Id" }, - { "MyNumber", "SingleNumber" } - }; - Assert.AreEqual(expected, result); - } - - [TestMethod] - public void TranslateInstantiation_RealType() - { - var expression = GetTransform(e => new QueryTestModel - { - Id = e.Id, - SingleNumber = e.SingleNumber - }); - var result = ExpressionTranslation.TranslateInstantiation(expression); - var expected = new BsonDocument - { - { "Id", "Id" }, - { "SingleNumber", "SingleNumber" } - }; - Assert.AreEqual(expected, result); - } - - [TestMethod, ExpectedException(typeof(ArgumentException))] - public void TranslateInstantiation_InvalidExpression() - { - var expression = GetTransform(e => e.SingleNumber); - ExpressionTranslation.TranslateInstantiation(expression); - } - - [TestMethod] - public void TranslateMember_SingleLevelMember() - { - var expression = GetTransform(e => e.SingleString); - var result = ExpressionTranslation.TranslateMember(expression); - var expected = new BsonString("SingleString"); - Assert.AreEqual(expected, result); - } - - [TestMethod] - public void TranslateMember_MultiLevelMember() - { - var expression = GetTransform(e => e.SingleModel.SingleNumber); - var result = ExpressionTranslation.TranslateMember(expression); - var expected = new BsonString("SingleModel.SingleNumber"); - Assert.AreEqual(expected, result); - } - - [TestMethod] - public void TranslateMember_MemberWithArrayIndex_AtStart() - { - var expression = GetTransform(e => e.ArrayOfModels[3].SingleNumber); - var result = ExpressionTranslation.TranslateMember(expression); - var expected = new BsonString("ArrayOfModels.3.SingleNumber"); - Assert.AreEqual(expected, result); - } - [TestMethod] - public void TranslateMember_MemberWithArrayIndex_AtEnd() - { - var expression = GetTransform(e => e.SingleModel.ArrayOfModels[2]); - var result = ExpressionTranslation.TranslateMember(expression); - var expected = new BsonString("SingleModel.ArrayOfModels.2"); - Assert.AreEqual(expected, result); - } } } diff --git a/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/ExpressionTranslationTests_Instantiation.cs b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/ExpressionTranslationTests_Instantiation.cs new file mode 100644 index 00000000..f5a37415 --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/ExpressionTranslationTests_Instantiation.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Bson; +using MongoFramework.Infrastructure.Linq.Translation; + +namespace MongoFramework.Tests.Infrastructure.Linq.Translation +{ + [TestClass] + public class ExpressionTranslationTests_Instantiation : QueryTestBase + { + [TestMethod] + public void TranslateInstantiation_Anonymous() + { + var expression = GetTransform(e => new + { + e.Id, + MyNumber = e.SingleNumber + }); + var result = ExpressionTranslation.TranslateInstantiation(expression); + var expected = new BsonDocument + { + { "Id", "Id" }, + { "MyNumber", "SingleNumber" } + }; + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void TranslateInstantiation_RealType() + { + var expression = GetTransform(e => new QueryTestModel + { + Id = e.Id, + SingleNumber = e.SingleNumber + }); + var result = ExpressionTranslation.TranslateInstantiation(expression); + var expected = new BsonDocument + { + { "Id", "Id" }, + { "SingleNumber", "SingleNumber" } + }; + Assert.AreEqual(expected, result); + } + + [TestMethod, ExpectedException(typeof(ArgumentException))] + public void TranslateInstantiation_InvalidExpression() + { + var expression = GetTransform(e => e.SingleNumber); + ExpressionTranslation.TranslateInstantiation(expression); + } + } +} diff --git a/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/ExpressionTranslationTests_Member.cs b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/ExpressionTranslationTests_Member.cs new file mode 100644 index 00000000..175ce093 --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/ExpressionTranslationTests_Member.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Bson; +using MongoFramework.Infrastructure.Linq.Translation; + +namespace MongoFramework.Tests.Infrastructure.Linq.Translation +{ + [TestClass] + public class ExpressionTranslationTests_Member : QueryTestBase + { + [TestMethod] + public void TranslateMember_SingleLevelMember() + { + var expression = GetTransform(e => e.SingleString); + var result = ExpressionTranslation.TranslateMember(expression); + var expected = new BsonString("SingleString"); + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void TranslateMember_MultiLevelMember() + { + var expression = GetTransform(e => e.SingleModel.SingleNumber); + var result = ExpressionTranslation.TranslateMember(expression); + var expected = new BsonString("SingleModel.SingleNumber"); + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void TranslateMember_MemberWithArrayIndex_AtStart() + { + var expression = GetTransform(e => e.ArrayOfModels[3].SingleNumber); + var result = ExpressionTranslation.TranslateMember(expression); + var expected = new BsonString("ArrayOfModels.3.SingleNumber"); + Assert.AreEqual(expected, result); + } + [TestMethod] + public void TranslateMember_MemberWithArrayIndex_AtEnd() + { + var expression = GetTransform(e => e.SingleModel.ArrayOfModels[2]); + var result = ExpressionTranslation.TranslateMember(expression); + var expected = new BsonString("SingleModel.ArrayOfModels.2"); + Assert.AreEqual(expected, result); + } + } +} From fa2df65fdc46b3be532d85eb1aff5e79c151a912 Mon Sep 17 00:00:00 2001 From: Turnerj Date: Wed, 9 Dec 2020 21:10:38 +1030 Subject: [PATCH 14/18] Add skip and take translators --- .../Translation/Translators/SkipTranslator.cs | 26 +++++++++++++++++++ .../Translation/Translators/TakeTranslator.cs | 26 +++++++++++++++++++ 2 files changed, 52 insertions(+) create mode 100644 src/MongoFramework/Infrastructure/Linq/Translation/Translators/SkipTranslator.cs create mode 100644 src/MongoFramework/Infrastructure/Linq/Translation/Translators/TakeTranslator.cs diff --git a/src/MongoFramework/Infrastructure/Linq/Translation/Translators/SkipTranslator.cs b/src/MongoFramework/Infrastructure/Linq/Translation/Translators/SkipTranslator.cs new file mode 100644 index 00000000..dbfbe129 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Linq/Translation/Translators/SkipTranslator.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using MongoDB.Bson; + +namespace MongoFramework.Infrastructure.Linq.Translation.Translators +{ + public class SkipTranslator : IMethodTranslator + { + public IEnumerable GetSupportedMethods() + { + yield return TranslationHelper.GetMethodDefinition(() => Queryable.Skip((IQueryable)null, 0)); + } + + public BsonValue TranslateMethod(MethodCallExpression expression, IEnumerable methodSuffixExpressions = default) + { + return new BsonDocument + { + { "$skip", ExpressionTranslation.TranslateConstant(expression.Arguments[1]) } + }; + } + } +} diff --git a/src/MongoFramework/Infrastructure/Linq/Translation/Translators/TakeTranslator.cs b/src/MongoFramework/Infrastructure/Linq/Translation/Translators/TakeTranslator.cs new file mode 100644 index 00000000..ab0c7d73 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Linq/Translation/Translators/TakeTranslator.cs @@ -0,0 +1,26 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using MongoDB.Bson; + +namespace MongoFramework.Infrastructure.Linq.Translation.Translators +{ + public class TakeTranslator : IMethodTranslator + { + public IEnumerable GetSupportedMethods() + { + yield return TranslationHelper.GetMethodDefinition(() => Queryable.Take((IQueryable)null, 0)); + } + + public BsonValue TranslateMethod(MethodCallExpression expression, IEnumerable methodSuffixExpressions = default) + { + return new BsonDocument + { + { "$limit", ExpressionTranslation.TranslateConstant(expression.Arguments[1]) } + }; + } + } +} From 779d96cc50228fcb514af0e9819667b372e78b72 Mon Sep 17 00:00:00 2001 From: Turnerj Date: Wed, 9 Dec 2020 21:20:42 +1030 Subject: [PATCH 15/18] Update default translators --- .../Infrastructure/Linq/Translation/DefaultTranslators.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/MongoFramework/Infrastructure/Linq/Translation/DefaultTranslators.cs b/src/MongoFramework/Infrastructure/Linq/Translation/DefaultTranslators.cs index 40e2827d..e87320d7 100644 --- a/src/MongoFramework/Infrastructure/Linq/Translation/DefaultTranslators.cs +++ b/src/MongoFramework/Infrastructure/Linq/Translation/DefaultTranslators.cs @@ -12,6 +12,8 @@ public static void AddTranslators() ExpressionTranslation.AddTranslator(new WhereTranslator()); ExpressionTranslation.AddTranslator(new OrderByTranslator()); ExpressionTranslation.AddTranslator(new SelectTranslator()); + ExpressionTranslation.AddTranslator(new SkipTranslator()); + ExpressionTranslation.AddTranslator(new TakeTranslator()); } } } From c6670c16226e4a997570c89983605b76dfe44a9b Mon Sep 17 00:00:00 2001 From: Turnerj Date: Wed, 9 Dec 2020 21:23:28 +1030 Subject: [PATCH 16/18] Added tests for additional translators --- .../Translators/OrderByTranslatorTests.cs | 54 +++++++++++++++++++ .../Translators/SkipTranslatorTests.cs | 32 +++++++++++ .../Translators/TakeTranslatorTests.cs | 32 +++++++++++ 3 files changed, 118 insertions(+) create mode 100644 tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/OrderByTranslatorTests.cs create mode 100644 tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/SkipTranslatorTests.cs create mode 100644 tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/TakeTranslatorTests.cs diff --git a/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/OrderByTranslatorTests.cs b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/OrderByTranslatorTests.cs new file mode 100644 index 00000000..efb2a124 --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/OrderByTranslatorTests.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Bson; +using MongoFramework.Infrastructure.Linq.Translation.Translators; + +namespace MongoFramework.Tests.Infrastructure.Linq.Translation.Translators +{ + [TestClass] + public class OrderByTranslatorTests : QueryTestBase + { + [TestMethod] + public void OrderBy() + { + var expression = GetExpression(q => q.OrderBy(e => e.Id)); + var result = new OrderByTranslator().TranslateMethod(expression as MethodCallExpression); + var expected = new BsonDocument + { + { + "$sort", + new BsonDocument + { + { "Id", 1 } + } + } + }; + + Assert.AreEqual(expected, result); + } + + [TestMethod] + public void OrderByDescending() + { + var expression = GetExpression(q => q.OrderByDescending(e => e.Id)); + var result = new OrderByTranslator().TranslateMethod(expression as MethodCallExpression); + var expected = new BsonDocument + { + { + "$sort", + new BsonDocument + { + { "Id", -1 } + } + } + }; + + Assert.AreEqual(expected, result); + } + } +} diff --git a/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/SkipTranslatorTests.cs b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/SkipTranslatorTests.cs new file mode 100644 index 00000000..2506d08b --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/SkipTranslatorTests.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Bson; +using MongoFramework.Infrastructure.Linq.Translation.Translators; + +namespace MongoFramework.Tests.Infrastructure.Linq.Translation.Translators +{ + [TestClass] + public class SkipTranslatorTests : QueryTestBase + { + [TestMethod] + public void Skip() + { + var expression = GetExpression(q => q.Skip(5)); + var result = new SkipTranslator().TranslateMethod(expression as MethodCallExpression); + var expected = new BsonDocument + { + { + "$skip", + 5 + } + }; + + Assert.AreEqual(expected, result); + } + } +} diff --git a/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/TakeTranslatorTests.cs b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/TakeTranslatorTests.cs new file mode 100644 index 00000000..06c058c4 --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/TakeTranslatorTests.cs @@ -0,0 +1,32 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Bson; +using MongoFramework.Infrastructure.Linq.Translation.Translators; + +namespace MongoFramework.Tests.Infrastructure.Linq.Translation.Translators +{ + [TestClass] + public class TakeTranslatorTests : QueryTestBase + { + [TestMethod] + public void Take() + { + var expression = GetExpression(q => q.Take(5)); + var result = new TakeTranslator().TranslateMethod(expression as MethodCallExpression); + var expected = new BsonDocument + { + { + "$limit", + 5 + } + }; + + Assert.AreEqual(expected, result); + } + } +} From 9713d20ec86d68230553ed0387a03d0c8cc1debf Mon Sep 17 00:00:00 2001 From: Turnerj Date: Wed, 9 Dec 2020 21:37:06 +1030 Subject: [PATCH 17/18] Added initial versions of select translator tests --- .../Translators/SelectTranslatorTests.cs | 57 +++++++++++++++++++ 1 file changed, 57 insertions(+) create mode 100644 tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/SelectTranslatorTests.cs diff --git a/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/SelectTranslatorTests.cs b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/SelectTranslatorTests.cs new file mode 100644 index 00000000..3de14b53 --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/SelectTranslatorTests.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; +using Microsoft.VisualStudio.TestTools.UnitTesting; +using MongoDB.Bson; +using MongoFramework.Infrastructure.Linq.Translation.Translators; + +namespace MongoFramework.Tests.Infrastructure.Linq.Translation.Translators +{ + [TestClass] + public class SelectorTranslatorTests : QueryTestBase + { + [TestMethod] + public void SelectProperty() + { + var expression = GetExpression(q => q.Select(e => e.Id)); + var result = new SelectTranslator().TranslateMethod(expression as MethodCallExpression); + var expected = new BsonDocument + { + { + "$project", + new BsonDocument + { + { "Id", "$Id" }, + { "_id", 0 } + } + } + }; + + Assert.AreEqual(expected, result); + } + + + [TestMethod] + public void SelectNewAnonymousType() + { + var expression = GetExpression(q => q.Select(e => new { CustomPropertyName = e.Id })); + var result = new SelectTranslator().TranslateMethod(expression as MethodCallExpression); + var expected = new BsonDocument + { + { + "$project", + new BsonDocument + { + { "CustomPropertyName", "Id" }, + { "_id", 0 } + } + } + }; + + Assert.AreEqual(expected, result); + } + } +} From 4ec1b631fe5ad3ce1e0af7dff392d3a7ba2303b6 Mon Sep 17 00:00:00 2001 From: Turnerj Date: Thu, 10 Dec 2020 11:29:39 +1030 Subject: [PATCH 18/18] Updated SelectProperty expected result to match official driver --- .../Linq/Translation/Translators/SelectTranslatorTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/SelectTranslatorTests.cs b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/SelectTranslatorTests.cs index 3de14b53..752a697c 100644 --- a/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/SelectTranslatorTests.cs +++ b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/SelectTranslatorTests.cs @@ -24,8 +24,7 @@ public void SelectProperty() "$project", new BsonDocument { - { "Id", "$Id" }, - { "_id", 0 } + { "_id", "$_id" } } } };