diff --git a/src/MongoFramework/Infrastructure/Linq/Translation/DefaultTranslators.cs b/src/MongoFramework/Infrastructure/Linq/Translation/DefaultTranslators.cs new file mode 100644 index 00000000..e87320d7 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Linq/Translation/DefaultTranslators.cs @@ -0,0 +1,19 @@ +using MongoFramework.Infrastructure.Linq.Translation.Translators; +using System; +using System.Collections.Generic; +using System.Text; + +namespace MongoFramework.Infrastructure.Linq.Translation +{ + public static class DefaultTranslators + { + public static void AddTranslators() + { + ExpressionTranslation.AddTranslator(new WhereTranslator()); + ExpressionTranslation.AddTranslator(new OrderByTranslator()); + ExpressionTranslation.AddTranslator(new SelectTranslator()); + ExpressionTranslation.AddTranslator(new SkipTranslator()); + ExpressionTranslation.AddTranslator(new TakeTranslator()); + } + } +} diff --git a/src/MongoFramework/Infrastructure/Linq/Translation/ExpressionTranslation.cs b/src/MongoFramework/Infrastructure/Linq/Translation/ExpressionTranslation.cs new file mode 100644 index 00000000..1fdedd04 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Linq/Translation/ExpressionTranslation.cs @@ -0,0 +1,517 @@ +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; + +namespace MongoFramework.Infrastructure.Linq.Translation +{ + 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() + { + DefaultTranslators.AddTranslators(); + } + + public static void AddTranslator(IQueryTranslator translator) + { + if (translator is IMethodTranslator methodTranslator) + { + lock (MethodTranslatorMap) + { + foreach (var method in methodTranslator.GetSupportedMethods()) + { + MethodTranslatorMap.Add(method, methodTranslator); + } + } + } + else if (translator is IMemberTranslator memberTranslator) + { + lock (MemberTranslatorMap) + { + 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 ClearTranslators() + { + lock (MethodTranslatorMap) + { + MethodTranslatorMap.Clear(); + } + + lock (MemberTranslatorMap) + { + MemberTranslatorMap.Clear(); + } + + lock (BinaryTranslatorMap) + { + BinaryTranslatorMap.Clear(); + } + } + + 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 && currentExpression.NodeType == ExpressionType.ArrayIndex) + { + //The index is on the right + var arrayIndex = TranslateSubExpression(binaryExpression.Right); + partialNamePieces.Push(arrayIndex.ToString()); + + //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 {currentExpression.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, bool negated = false) + { + var localExpression = UnwrapLambda(expression); + + static void UnwrapBinaryQuery(BsonArray target, ExpressionType expressionType, BinaryExpression expression, bool negated) + { + if (expression.Left.NodeType == expressionType) + { + UnwrapBinaryQuery(target, expressionType, expression.Left as BinaryExpression, negated); + } + else + { + target.Add(TranslateConditional(expression.Left, negated)); + } + + target.Add(TranslateConditional(expression.Right, negated)); + } + + if (localExpression is BinaryExpression binaryExpression) + { + if (localExpression.NodeType == ExpressionType.AndAlso) + { + var unwrappedQuery = new BsonArray(); + UnwrapBinaryQuery(unwrappedQuery, ExpressionType.AndAlso, binaryExpression, negated); + + 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, negated); + 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 } }; + + if (negated) + { + valueComparison = new BsonDocument + { + { "$not", valueComparison } + }; + } + + return new BsonDocument { { fieldName, valueComparison } }; + } + } + else if (localExpression is UnaryExpression unaryExpression && unaryExpression.NodeType == ExpressionType.Not) + { + if (unaryExpression.Operand.NodeType == ExpressionType.OrElse) + { + var translatedInnerExpression = TranslateConditional(unaryExpression.Operand, false); + var valueItems = translatedInnerExpression.GetElement("$or").Value; + + return new BsonDocument + { + { "$nor", valueItems } + }; + } + else + { + return TranslateConditional(unaryExpression.Operand, !negated); + } + } + + 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/Linq/Translation/StageBuilder.cs b/src/MongoFramework/Infrastructure/Linq/Translation/StageBuilder.cs new file mode 100644 index 00000000..0e289920 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Linq/Translation/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.Linq.Translation +{ + public static class StageBuilder + { + public static IEnumerable BuildFromExpression(Expression expression) + { + var currentExpression = expression; + var stages = new Stack(); + + while (currentExpression is MethodCallExpression methodCallExpression) + { + var stage = ExpressionTranslation.TranslateMethod(methodCallExpression).AsBsonDocument; + stages.Push(stage); + + currentExpression = methodCallExpression.Arguments[0]; + } + + return stages; + } + } +} diff --git a/src/MongoFramework/Infrastructure/Linq/Translation/TranslationHelper.cs b/src/MongoFramework/Infrastructure/Linq/Translation/TranslationHelper.cs new file mode 100644 index 00000000..f8922149 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Linq/Translation/TranslationHelper.cs @@ -0,0 +1,24 @@ +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.Linq.Translation +{ + public static class TranslationHelper + { + public static MethodInfo GetMethodDefinition(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"); + } + } +} diff --git a/src/MongoFramework/Infrastructure/Linq/Translation/TranslatorInterfaces.cs b/src/MongoFramework/Infrastructure/Linq/Translation/TranslatorInterfaces.cs new file mode 100644 index 00000000..70bb5f37 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Linq/Translation/TranslatorInterfaces.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq.Expressions; +using System.Reflection; +using System.Text; +using MongoDB.Bson; + +namespace MongoFramework.Infrastructure.Linq.Translation +{ + public interface IQueryTranslator { } + public interface IMethodTranslator : IQueryTranslator + { + IEnumerable GetSupportedMethods(); + BsonValue TranslateMethod(MethodCallExpression expression, IEnumerable suffixExpressions = default); + } + + public interface IMemberTranslator : IQueryTranslator + { + IEnumerable GetSupportedMembers(); + BsonValue TranslateMember(MemberExpression expression, IEnumerable suffixExpressions = default); + } + + public interface IBinaryExpressionTranslator : IQueryTranslator + { + IEnumerable GetSupportedExpressionTypes(); + BsonValue TranslateBinary(BinaryExpression expression); + } +} diff --git a/src/MongoFramework/Infrastructure/Linq/Translation/Translators/OrderByTranslator.cs b/src/MongoFramework/Infrastructure/Linq/Translation/Translators/OrderByTranslator.cs new file mode 100644 index 00000000..fe57cf2c --- /dev/null +++ b/src/MongoFramework/Infrastructure/Linq/Translation/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.Linq.Translation.Translators +{ + public class OrderByTranslator : IMethodTranslator + { + public 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/Linq/Translation/Translators/SelectTranslator.cs b/src/MongoFramework/Infrastructure/Linq/Translation/Translators/SelectTranslator.cs new file mode 100644 index 00000000..0b18a5c1 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Linq/Translation/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.Linq.Translation.Translators +{ + public class SelectTranslator : IMethodTranslator + { + public 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/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]) } + }; + } + } +} diff --git a/src/MongoFramework/Infrastructure/Linq/Translation/Translators/WhereTranslator.cs b/src/MongoFramework/Infrastructure/Linq/Translation/Translators/WhereTranslator.cs new file mode 100644 index 00000000..f3f2ef13 --- /dev/null +++ b/src/MongoFramework/Infrastructure/Linq/Translation/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.Linq.Translation.Translators +{ + public class WhereTranslator : IMethodTranslator + { + public 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/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/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/ExpressionTranslationTests_Conditional.cs b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/ExpressionTranslationTests_Conditional.cs new file mode 100644 index 00000000..0c812dcd --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/ExpressionTranslationTests_Conditional.cs @@ -0,0 +1,243 @@ +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_Conditional : 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); + } + + [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_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() + { + 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_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() + { + 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); + } + + [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); + } + } +} 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); + } + } +} diff --git a/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/QueryTestBase.cs b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/QueryTestBase.cs new file mode 100644 index 00000000..1efb2a68 --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/QueryTestBase.cs @@ -0,0 +1,48 @@ +using MongoFramework.Infrastructure.Linq.Translation; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Linq.Expressions; +using System.Text; +using System.Threading.Tasks; + +namespace MongoFramework.Tests.Infrastructure.Linq.Translation +{ + 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); + } + + protected static Expression GetTransform(Expression> expression) + { + return ExpressionTranslation.UnwrapLambda(expression); + } + } +} diff --git a/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/StageBuilderTests.cs b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/StageBuilderTests.cs new file mode 100644 index 00000000..6d0b7ed7 --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/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.Linq.Translation; + +namespace MongoFramework.Tests.Infrastructure.Linq.Translation +{ + [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/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/SelectTranslatorTests.cs b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/SelectTranslatorTests.cs new file mode 100644 index 00000000..752a697c --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/SelectTranslatorTests.cs @@ -0,0 +1,56 @@ +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" } + } + } + }; + + 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); + } + } +} 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); + } + } +} diff --git a/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/WhereTranslatorTests.cs b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/Translators/WhereTranslatorTests.cs new file mode 100644 index 00000000..f5a4a7a6 --- /dev/null +++ b/tests/MongoFramework.Tests/Infrastructure/Linq/Translation/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.Linq.Translation.Translators; + +namespace MongoFramework.Tests.Infrastructure.Linq.Translation.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); + } + } +}