diff --git a/src/System.Linq.Dynamic.Core/DynamicExpressionParser.cs b/src/System.Linq.Dynamic.Core/DynamicExpressionParser.cs index 5e7f627c..75c111d2 100644 --- a/src/System.Linq.Dynamic.Core/DynamicExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/DynamicExpressionParser.cs @@ -35,7 +35,26 @@ public static LambdaExpression ParseLambda(bool createParameterCtor, [CanBeNull] { Check.NotEmpty(expression, nameof(expression)); - var parser = new ExpressionParser(new ParameterExpression[0], expression, values); + var parser = new ExpressionParser(new ParameterExpression[0], expression, values, null); + + return Expression.Lambda(parser.Parse(resultType, true)); + } + + /// + /// Parses an expression into a LambdaExpression. + /// + /// if set to true then also create a constructor for all the parameters. Note that this doesn't work for Linq-to-Database entities. + /// Type of the result. If not specified, it will be generated dynamically. + /// The expression. + /// The Configuration for the parsing. + /// An object array that contains zero or more objects which are used as replacement values. + /// The generated + [PublicAPI] + public static LambdaExpression ParseLambda(bool createParameterCtor, [CanBeNull] Type resultType, string expression, ParsingConfig parsingConfig, params object[] values) + { + Check.NotEmpty(expression, nameof(expression)); + + var parser = new ExpressionParser(new ParameterExpression[0], expression, values, parsingConfig); return Expression.Lambda(parser.Parse(resultType, true)); } @@ -54,6 +73,21 @@ public static LambdaExpression ParseLambda([NotNull] Type itType, [CanBeNull] Ty return ParseLambda(true, itType, resultType, expression, values); } + /// + /// Parses an expression into a LambdaExpression. (Also create a constructor for all the parameters. Note that this doesn't work for Linq-to-Database entities.) + /// + /// The main type from the dynamic class expression. + /// Type of the result. If not specified, it will be generated dynamically. + /// The expression. + /// The Configuration for the parsing. + /// An object array that contains zero or more objects which are used as replacement values. + /// The generated + [PublicAPI] + public static LambdaExpression ParseLambda([NotNull] Type itType, [CanBeNull] Type resultType, string expression, ParsingConfig parsingConfig, params object[] values) + { + return ParseLambda(true, itType, resultType, expression, parsingConfig, values); + } + /// /// Parses an expression into a LambdaExpression. /// @@ -72,6 +106,25 @@ public static LambdaExpression ParseLambda(bool createParameterCtor, [NotNull] T return ParseLambda(createParameterCtor, new[] { Expression.Parameter(itType, "") }, resultType, expression, values); } + /// + /// Parses an expression into a LambdaExpression. + /// + /// if set to true then also create a constructor for all the parameters. Note that this doesn't work for Linq-to-Database entities. + /// The main type from the dynamic class expression. + /// Type of the result. If not specified, it will be generated dynamically. + /// The expression. + /// The Configuration for the parsing. + /// An object array that contains zero or more objects which are used as replacement values. + /// The generated + [PublicAPI] + public static LambdaExpression ParseLambda(bool createParameterCtor, [NotNull] Type itType, [CanBeNull] Type resultType, string expression, ParsingConfig parsingConfig, params object[] values) + { + Check.NotNull(itType, nameof(itType)); + Check.NotEmpty(expression, nameof(expression)); + + return ParseLambda(createParameterCtor, new[] { Expression.Parameter(itType, "") }, resultType, expression, parsingConfig, values); + } + /// /// Parses an expression into a LambdaExpression. (Also create a constructor for all the parameters. Note that this doesn't work for Linq-to-Database entities.) /// @@ -102,9 +155,31 @@ public static LambdaExpression ParseLambda(bool createParameterCtor, [NotNull] P Check.Condition(parameters, p => p.Count(x => x == null) == 0, nameof(parameters)); Check.NotEmpty(expression, nameof(expression)); - var parser = new ExpressionParser(parameters, expression, values); + var parser = new ExpressionParser(parameters, expression, values, null); + + return Expression.Lambda(parser.Parse(resultType, createParameterCtor), parameters); + } + + /// + /// Parses an expression into a LambdaExpression. + /// + /// if set to true then also create a constructor for all the parameters. Note that this doesn't work for Linq-to-Database entities. + /// A array from ParameterExpressions. + /// Type of the result. If not specified, it will be generated dynamically. + /// The expression. + /// The Configuration for the parsing. + /// An object array that contains zero or more objects which are used as replacement values. + /// The generated + [PublicAPI] + public static LambdaExpression ParseLambda(bool createParameterCtor, [NotNull] ParameterExpression[] parameters, [CanBeNull] Type resultType, string expression, ParsingConfig parsingConfig, params object[] values) + { + Check.NotNull(parameters, nameof(parameters)); + Check.Condition(parameters, p => p.Count(x => x == null) == 0, nameof(parameters)); + Check.NotEmpty(expression, nameof(expression)); + + var parser = new ExpressionParser(parameters, expression, values, parsingConfig); return Expression.Lambda(parser.Parse(resultType, createParameterCtor), parameters); } } -} \ No newline at end of file +} diff --git a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs index da0bbb22..08279849 100644 --- a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs +++ b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs @@ -861,7 +861,7 @@ public static IOrderedQueryable OrderBy([NotNull] this IQueryable source, [NotNu Check.NotEmpty(ordering, nameof(ordering)); ParameterExpression[] parameters = { Expression.Parameter(source.ElementType, "") }; - ExpressionParser parser = new ExpressionParser(parameters, ordering, args); + ExpressionParser parser = new ExpressionParser(parameters, ordering, args, null); IList dynamicOrderings = parser.ParseOrdering(); Expression queryExpr = source.Expression; @@ -1561,7 +1561,7 @@ public static IOrderedQueryable ThenBy([NotNull] this IOrderedQueryable source, Check.NotEmpty(ordering, nameof(ordering)); ParameterExpression[] parameters = { Expression.Parameter(source.ElementType, "") }; - ExpressionParser parser = new ExpressionParser(parameters, ordering, args); + ExpressionParser parser = new ExpressionParser(parameters, ordering, args, null); IList dynamicOrderings = parser.ParseOrdering(forceThenBy: true); Expression queryExpr = source.Expression; diff --git a/src/System.Linq.Dynamic.Core/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/ExpressionParser.cs index 887ffb62..3038c0da 100644 --- a/src/System.Linq.Dynamic.Core/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/ExpressionParser.cs @@ -351,7 +351,10 @@ private static void UpdatePredefinedTypes(string typeName, int x) } private readonly TextParser _textParser; - public ExpressionParser(ParameterExpression[] parameters, string expression, object[] values) + private readonly ParsingConfig _parsingConfig; + + + public ExpressionParser(ParameterExpression[] parameters, string expression, object[] values, ParsingConfig parsingConfig) { if (_keywords == null) _keywords = CreateKeywords(); @@ -1311,10 +1314,22 @@ Expression ParseNew() if (_textParser.CurrentToken.Id == TokenId.Identifier) { var newTypeName = _textParser.CurrentToken.Text; + _textParser.NextToken(); + + while (_textParser.CurrentToken.Id == TokenId.Dot || _textParser.CurrentToken.Id == TokenId.Plus) + { + var sep = _textParser.CurrentToken.Text; + _textParser.NextToken(); + if (_textParser.CurrentToken.Id != TokenId.Identifier) + throw ParseError(Res.IdentifierExpected); + newTypeName += sep + _textParser.CurrentToken.Text; + _textParser.NextToken(); + } + 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.OpenBracket && _textParser.CurrentToken.Id != TokenId.OpenCurlyParen) @@ -1405,40 +1420,63 @@ private Expression CreateNewExpression(List properties, List); - ConstructorInfo constructorForKeyValuePair = typeForKeyValuePair.GetTypeInfo().DeclaredConstructors.First(); - - var arrayIndexParams = new List(); - for (int i = 0; i < expressions.Count; i++) +#if !UAP10_0 + if (_parsingConfig != null && _parsingConfig.UseDynamicObjectClassForAnonymousTypes || + _parsingConfig == null && GlobalConfig.UseDynamicObjectClassForAnonymousTypes) { - // Just convert the expression always to an object expression. - UnaryExpression boxingExpression = Expression.Convert(expressions[i], typeof(object)); - NewExpression parameter = Expression.New(constructorForKeyValuePair, new[] { (Expression)Expression.Constant(properties[i].Name), boxingExpression }); +#endif + type = typeof(DynamicClass); + Type typeForKeyValuePair = typeof(KeyValuePair); +#if NET35 || NET40 + ConstructorInfo constructorForKeyValuePair = typeForKeyValuePair.GetConstructors().First(); +#else + ConstructorInfo constructorForKeyValuePair = typeForKeyValuePair.GetTypeInfo().DeclaredConstructors.First(); +#endif + var arrayIndexParams = new List(); + for (int i = 0; i < expressions.Count; i++) + { + // Just convert the expression always to an object expression. + UnaryExpression boxingExpression = Expression.Convert(expressions[i], typeof(object)); + NewExpression parameter = Expression.New(constructorForKeyValuePair, (Expression)Expression.Constant(properties[i].Name), boxingExpression); - arrayIndexParams.Add(parameter); - } + arrayIndexParams.Add(parameter); + } - // Create an expression tree that represents creating and initializing a one-dimensional array of type KeyValuePair. - NewArrayExpression newArrayExpression = Expression.NewArrayInit(typeof(KeyValuePair), arrayIndexParams); + // Create an expression tree that represents creating and initializing a one-dimensional array of type KeyValuePair. + NewArrayExpression newArrayExpression = Expression.NewArrayInit(typeof(KeyValuePair), arrayIndexParams); - // Get the "public DynamicClass(KeyValuePair[] propertylist)" constructor - ConstructorInfo constructor = type.GetTypeInfo().DeclaredConstructors.First(); - return Expression.New(constructor, newArrayExpression); + // Get the "public DynamicClass(KeyValuePair[] propertylist)" constructor +#if NET35 || NET40 + ConstructorInfo constructor = type.GetConstructors().First(); #else + ConstructorInfo constructor = type.GetTypeInfo().DeclaredConstructors.First(); +#endif + return Expression.New(constructor, newArrayExpression); +#if !UAP10_0 + } + type = DynamicClassFactory.CreateType(properties, _createParameterCtor); #endif } - Type[] propertyTypes = type.GetProperties().Select(p => p.PropertyType).ToArray(); + IEnumerable propertyInfos = type.GetProperties(); + if (type.GetTypeInfo().BaseType == typeof(DynamicClass)) + { + propertyInfos = propertyInfos.Where(x => x.Name != "Item"); + } + + Type[] propertyTypes = propertyInfos.Select(p => p.PropertyType).ToArray(); ConstructorInfo ctor = type.GetConstructor(propertyTypes); - if (ctor != null) - return Expression.New(ctor, expressions); + if (ctor != null && ctor.GetParameters().Length == expressions.Count) + { + return Expression.New(ctor, expressions, (IEnumerable)propertyInfos); + } MemberBinding[] bindings = new MemberBinding[properties.Count]; for (int i = 0; i < bindings.Length; i++) + { bindings[i] = Expression.Bind(type.GetProperty(properties[i].Name), expressions[i]); + } return Expression.MemberInit(Expression.New(type), bindings); } diff --git a/src/System.Linq.Dynamic.Core/GlobalConfig.cs b/src/System.Linq.Dynamic.Core/GlobalConfig.cs index db97e167..64a895f9 100644 --- a/src/System.Linq.Dynamic.Core/GlobalConfig.cs +++ b/src/System.Linq.Dynamic.Core/GlobalConfig.cs @@ -63,4 +63,4 @@ public static bool AreContextKeywordsEnabled /// public static bool UseDynamicObjectClassForAnonymousTypes { get; set; } } -} \ No newline at end of file +} diff --git a/src/System.Linq.Dynamic.Core/ParsingConfig.cs b/src/System.Linq.Dynamic.Core/ParsingConfig.cs new file mode 100644 index 00000000..255dfc14 --- /dev/null +++ b/src/System.Linq.Dynamic.Core/ParsingConfig.cs @@ -0,0 +1,18 @@ +namespace System.Linq.Dynamic.Core +{ + /// + /// Configuration class for Dynamic Linq. + /// + public class ParsingConfig + { + internal static ParsingConfig Default { get; } = new ParsingConfig(); + + /// + /// Gets or sets a value indicating whether to use dynamic object class for anonymous types. + /// + /// + /// true if wether to use dynamic object class for anonymous types; otherwise, false. + /// + public bool UseDynamicObjectClassForAnonymousTypes { get; set; } + } +} diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs index 48b908b1..8e59ddbf 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs @@ -1,7 +1,9 @@ using NFluent; using System.Collections.Generic; +using System.Linq.Dynamic.Core.CustomTypeProviders; using System.Linq.Dynamic.Core.Exceptions; using System.Linq.Expressions; +using System.Reflection; using Xunit; using User = System.Linq.Dynamic.Core.Tests.Helpers.Models.User; @@ -23,6 +25,29 @@ private class ComplexParseLambda1Result public int TotalIncome; } + [DynamicLinqType] + public class ComplexParseLambda3Result + { + public int? Age { get; set; } + public int TotalIncome { get; set; } + } + + private class TestCustomTypeProvider : AbstractDynamicLinqCustomTypeProvider, IDynamicLinkCustomTypeProvider + { + private HashSet _customTypes; + + public virtual HashSet GetCustomTypes() + { + if (_customTypes != null) + return _customTypes; + + _customTypes = + new HashSet( + FindTypesMarkedWithDynamicLinqTypeAttribute(new[] {this.GetType().GetTypeInfo().Assembly})); + return _customTypes; + } + } + [Fact] public void ParseLambda_ToList() { @@ -70,7 +95,6 @@ public void ParseLambda_Complex_1() Check.That(result.ToArray()[0]).Equals(expected[0]); } - [Fact] public void ParseLambda_Complex_2() { @@ -92,6 +116,36 @@ public void ParseLambda_Complex_2() Check.That(result.ToArray()[0]).Equals(expected[0]); } + [Fact] + public void ParseLambda_Complex_3() + { + GlobalConfig.CustomTypeProvider = new TestCustomTypeProvider(); + + // Arrange + var testList = User.GenerateSampleModels(51); + var qry = testList.AsQueryable(); + + var externals = new Dictionary + { + { "Users", qry } + }; + + // Act + string query = "Users.GroupBy(x => new { x.Profile.Age }).OrderBy(gg => gg.Key.Age).Select(j => new System.Linq.Dynamic.Core.Tests.DynamicExpressionParserTests+ComplexParseLambda3Result{j.Key.Age, j.Sum(k => k.Income) As TotalIncome})"; + LambdaExpression expression = DynamicExpressionParser.ParseLambda(null, query, externals); + Delegate del = expression.Compile(); + IEnumerable result = del.DynamicInvoke() as IEnumerable; + + var expected = qry.GroupBy(x => new { x.Profile.Age }).OrderBy(gg => gg.Key.Age).Select(j => new ComplexParseLambda3Result { Age = j.Key.Age, TotalIncome = j.Sum(k => k.Income) }).Cast().ToArray(); + + // Assert + Check.That(result).IsNotNull(); + Check.That(result).HasSize(expected.Length); + Check.That(result.ToArray()[0]).Equals(expected[0]); + + GlobalConfig.CustomTypeProvider = null; + } + [Fact] public void ParseLambda_Select_1() {