diff --git a/src/System.Linq.Dynamic.Core/DynamicExpressionParser.cs b/src/System.Linq.Dynamic.Core/DynamicExpressionParser.cs index cb95d3e3..c02fd6bb 100644 --- a/src/System.Linq.Dynamic.Core/DynamicExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/DynamicExpressionParser.cs @@ -9,6 +9,24 @@ namespace System.Linq.Dynamic.Core /// public static class DynamicExpressionParser { + /// + /// Parses a expression into a LambdaExpression. (Also create a constructor for all the parameters.) + /// + /// The main type from the dynamic class expression. + /// Type of the result. If not specified, it will be generated dynamically. + /// The expression. + /// An object array that contains zero or more objects which are used as replacement values. + /// The generated + public static LambdaExpression ParseLambda([CanBeNull] Type resultType, [NotNull] string expression, params object[] values) + { + Check.NotEmpty(expression, nameof(expression)); + + + var parser = new ExpressionParser(new ParameterExpression[0], expression, values); + + return Expression.Lambda(parser.Parse(resultType, true)); + } + /// /// Parses a expression into a LambdaExpression. (Also create a constructor for all the parameters.) /// diff --git a/src/System.Linq.Dynamic.Core/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/ExpressionParser.cs index 2aa87631..a923d124 100644 --- a/src/System.Linq.Dynamic.Core/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/ExpressionParser.cs @@ -149,6 +149,7 @@ interface IEnumerableSignatures void Take(int count); void TakeWhile(bool predicate); void Distinct(); + void GroupBy(object selector); //Executors void Single(); @@ -222,6 +223,7 @@ interface IEnumerableSignatures readonly Dictionary _symbols; IDictionary _externals; + readonly Dictionary _internals; readonly Dictionary _literals; ParameterExpression _it; ParameterExpression _parent; @@ -269,6 +271,7 @@ public ExpressionParser(ParameterExpression[] parameters, string expression, obj { if (_keywords == null) _keywords = CreateKeywords(); _symbols = new Dictionary(StringComparer.OrdinalIgnoreCase); + _internals = new Dictionary(); _literals = new Dictionary(); if (parameters != null) ProcessParameters(parameters); if (values != null) ProcessValues(values); @@ -388,7 +391,7 @@ Expression ParseExpression() // ?? (null-coalescing) operator Expression ParseNullCoalescing() { - Expression expr = ParseConditionalOr(); + Expression expr = ParseLambda(); if (_textParser.CurrentToken.Id == TokenId.NullCoalescing) { _textParser.NextToken(); @@ -398,6 +401,24 @@ Expression ParseNullCoalescing() return expr; } + // => operator - Added Support for projection operator + Expression ParseLambda() + { + Expression expr = ParseConditionalOr(); + if (_textParser.CurrentToken.Id == TokenId.Lambda && _it.Type == expr.Type) + { + _textParser.NextToken(); + if (_textParser.CurrentToken.Id == TokenId.Identifier || + _textParser.CurrentToken.Id == TokenId.OpenParen) + { + var right = ParseExpression(); + return Expression.Lambda(right, new[] {(ParameterExpression) expr}); + } + _textParser.ValidateToken(TokenId.OpenParen, Res.OpenParenExpected); + } + return expr; + } + // isnull(a,b) operator Expression ParseIsNull() { @@ -1003,7 +1024,8 @@ Expression ParseIdentifier() } if (_symbols.TryGetValue(_textParser.CurrentToken.Text, out value) || - _externals != null && _externals.TryGetValue(_textParser.CurrentToken.Text, out value)) + _externals != null && _externals.TryGetValue(_textParser.CurrentToken.Text, out value) || + _internals.TryGetValue(_textParser.CurrentToken.Text, out value)) { Expression expr = value as Expression; if (expr == null) @@ -1093,7 +1115,24 @@ Expression GenerateConditional(Expression test, Expression expr1, Expression exp Expression ParseNew() { _textParser.NextToken(); - _textParser.ValidateToken(TokenId.OpenParen, Res.OpenParenExpected); + if (_textParser.CurrentToken.Id != TokenId.OpenParen && + _textParser.CurrentToken.Id != TokenId.OpenCurlyParen && + _textParser.CurrentToken.Id != TokenId.Identifier) + throw ParseError(Res.OpenParenOrIdentifierExpected); + + Type newType = null; + if (_textParser.CurrentToken.Id == TokenId.Identifier) + { + var newTypeName = _textParser.CurrentToken.Text; + newType = FindType(newTypeName); + if (newType == null) + throw ParseError(_textParser.CurrentToken.Pos, Res.TypeNotFound, newTypeName); + _textParser.NextToken(); + if (_textParser.CurrentToken.Id != TokenId.OpenParen && + _textParser.CurrentToken.Id != TokenId.OpenCurlyParen) + throw ParseError(Res.OpenParenExpected); + } + _textParser.NextToken(); var properties = new List(); @@ -1125,16 +1164,18 @@ Expression ParseNew() _textParser.NextToken(); } - _textParser.ValidateToken(TokenId.CloseParen, Res.CloseParenOrCommaExpected); + if (_textParser.CurrentToken.Id != TokenId.CloseParen && + _textParser.CurrentToken.Id != TokenId.CloseCurlyParen) + throw ParseError(Res.CloseParenOrCommaExpected); _textParser.NextToken(); - return CreateNewExpression(properties, expressions); + return CreateNewExpression(properties, expressions, newType); } - private Expression CreateNewExpression(List properties, List expressions) + private Expression CreateNewExpression(List properties, List expressions, Type newType) { // http://solutionizing.net/category/linq/ - Type type = _resultType; + Type type = newType ?? _resultType; if (type == null) { @@ -1323,7 +1364,19 @@ Expression ParseMemberAccess(Type type, Expression instance) MemberInfo member = FindPropertyOrField(type, id, instance == null); if (member == null) { - throw ParseError(errorPos, Res.UnknownPropertyOrField, id, GetTypeName(type)); + if (_textParser.CurrentToken.Id == TokenId.Lambda && _it.Type == type) + { + // This might be an internal variable for use within a lambda expression, so store it as such + _internals.Add(id, _it); + _textParser.NextToken(); + var right = ParseExpression(); + return right; + } + else + { + throw ParseError(errorPos, Res.UnknownPropertyOrField, id, GetTypeName(type)); + } + } var property = member as PropertyInfo; @@ -1352,6 +1405,28 @@ static Type FindGenericType(Type generic, Type type) return null; } + Type FindType(string name) + { + object type; + _keywords.TryGetValue(name, out type); + var result = type as Type; + if (result != null) + return result; + if (_it != null && _it.Type.Name == name) + return _it.Type; + if (_parent != null && _parent.Type.Name == name) + return _parent.Type; + if (_root != null && _root.Type.Name == name) + return _root.Type; + if (_it != null && _it.Type.Namespace + "." + _it.Type.Name == name) + return _it.Type; + if (_parent != null && _parent.Type.Namespace + "." + _parent.Type.Name == name) + return _parent.Type; + if (_root != null && _root.Type.Namespace + "." + _root.Type.Name == name) + return _root.Type; + return null; + } + Expression ParseAggregate(Expression instance, Type elementType, string methodName, int errorPos) { var oldParent = _parent; @@ -1381,7 +1456,7 @@ Expression ParseAggregate(Expression instance, Type elementType, string methodNa throw ParseError(errorPos, Res.NoApplicableAggregate, methodName); Type[] typeArgs; - if (new[] { "Min", "Max", "Select", "OrderBy", "OrderByDescending", "ThenBy", "ThenByDescending" }.Contains(signature.Name)) + if (new[] { "Min", "Max", "Select", "OrderBy", "OrderByDescending", "ThenBy", "ThenByDescending", "GroupBy" }.Contains(signature.Name)) { typeArgs = new[] { elementType, args[0].Type }; } @@ -2360,10 +2435,6 @@ static Expression OptimizeStringForEqualityIfPossible(string text, Type type) return null; } - - - - bool TokenIdentifierIs(string id) { return _textParser.CurrentToken.Id == TokenId.Identifier && string.Equals(id, _textParser.CurrentToken.Text, StringComparison.OrdinalIgnoreCase); diff --git a/src/System.Linq.Dynamic.Core/Res.cs b/src/System.Linq.Dynamic.Core/Res.cs index 2cc2cc1f..531c2b58 100644 --- a/src/System.Linq.Dynamic.Core/Res.cs +++ b/src/System.Linq.Dynamic.Core/Res.cs @@ -55,5 +55,6 @@ internal static class Res public const string IdentifierExpected = "Identifier expected"; public const string OpenParenOrIdentifierExpected = "'(' or Identifier expected"; public const string IdentifierImplementingInterfaceExpected = "Identifier implementing interface '{0}' expected"; + public const string TypeNotFound = "Type '{0}' not Found"; } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs b/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs index e41eae55..7de69480 100644 --- a/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs +++ b/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs @@ -100,6 +100,16 @@ public void NextToken() t = TokenId.CloseParen; break; + case '{': + NextChar(); + t = TokenId.OpenCurlyParen; + break; + + case '}': + NextChar(); + t = TokenId.CloseCurlyParen; + break; + case '*': NextChar(); t = TokenId.Asterisk; @@ -164,6 +174,11 @@ public void NextToken() NextChar(); t = TokenId.DoubleEqual; } + else if (_ch == '>') + { + NextChar(); + t = TokenId.Lambda; + } else { t = TokenId.Equal; diff --git a/src/System.Linq.Dynamic.Core/Tokenizer/TokenId.cs b/src/System.Linq.Dynamic.Core/Tokenizer/TokenId.cs index cee90bbf..1d8fce1d 100644 --- a/src/System.Linq.Dynamic.Core/Tokenizer/TokenId.cs +++ b/src/System.Linq.Dynamic.Core/Tokenizer/TokenId.cs @@ -13,6 +13,8 @@ internal enum TokenId Amphersand, OpenParen, CloseParen, + OpenCurlyParen, + CloseCurlyParen, Asterisk, Plus, Comma, @@ -36,6 +38,7 @@ internal enum TokenId DoubleBar, DoubleGreaterThan, DoubleLessThan, - NullCoalescing + NullCoalescing, + Lambda } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/ComplexTests.cs b/test/System.Linq.Dynamic.Core.Tests/ComplexTests.cs index 9816afb7..ed9a78ed 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ComplexTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ComplexTests.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq.Dynamic.Core.Tests.Helpers.Models; +using System.Linq.Expressions; using Xunit; namespace System.Linq.Dynamic.Core.Tests @@ -94,5 +95,37 @@ public void GroupByAndSelect_TestDynamicSelectMember() selectQry.AsEnumerable().Select(x => x.TotalIncome).Cast().ToArray()); #endif } + + [Fact] + public void ComplexString1() + { + //Arrange + var testList = User.GenerateSampleModels(51); + var qry = testList.AsQueryable(); + + var externals = new Dictionary(); + externals.Add("Users", qry); + + var query = "Users.GroupBy(x => new { x.Profile.Age }).OrderBy(gg => gg.Key.Age).Select(j => new (j.Key.Age, j.Sum(k=>k.Income) As TotalIncome))"; + var expression = DynamicExpressionParser.ParseLambda(null, query, externals); + var del = expression.Compile(); + var selectQry = del.DynamicInvoke(); + } + + [Fact] + public void ComplexString2() + { + //Arrange + var testList = User.GenerateSampleModels(51); + var qry = testList.AsQueryable(); + + var externals = new Dictionary(); + externals.Add("Users", qry); + + var query = "Users.Select(j => new User(j.Income As Income))"; + var expression = DynamicExpressionParser.ParseLambda(null, query, externals); + var del = expression.Compile(); + var res = del.DynamicInvoke(); + } } }