From c59391a944a46532a194c2dd41552f4c488b4e2a Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sat, 19 Feb 2022 16:12:54 +0100 Subject: [PATCH] Add support for property for Is,As and Cast (#567) * Add support for property for Is,As and Cast * . * . --- .../Parser/ExpressionParser.cs | 83 ++++++++++++------- .../Parser/TypeFinder.cs | 3 +- src/System.Linq.Dynamic.Core/Res.cs | 3 +- .../Entities/EmployeeWrapper.cs | 7 ++ .../QueryableTests.Is,OfType,As,Cast.cs | 26 +++++- 5 files changed, 88 insertions(+), 34 deletions(-) create mode 100644 test/System.Linq.Dynamic.Core.Tests/Entities/EmployeeWrapper.cs diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 19ce95df..4a5187a3 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -904,10 +904,9 @@ Expression ParseIdentifier() } else { - var lambda = expr as LambdaExpression; - if (lambda != null) + if (expr is LambdaExpression lambdaExpression) { - return ParseLambdaInvocation(lambda); + return ParseLambdaInvocation(lambdaExpression); } } @@ -1021,14 +1020,25 @@ Expression ParseFunctionIs() Expression[] args = ParseArgumentList(); - if (args.Length != 1) + if (args.Length != 1 && args.Length != 2) { - throw ParseError(errorPos, Res.FunctionRequiresOneArg, functionName); + throw ParseError(errorPos, Res.FunctionRequiresOneOrTwoArgs, functionName); } - Type resolvedType = ResolveTypeFromArgumentExpression(functionName, args[0]); + Expression typeArgument; + Expression it; + if (args.Length == 1) + { + typeArgument = args[0]; + it = _it; + } + else + { + typeArgument = args[1]; + it = args[0]; + } - return Expression.TypeIs(_it, resolvedType); + return Expression.TypeIs(it, ResolveTypeFromArgumentExpression(functionName, typeArgument, args.Length)); } // As(...) function @@ -1040,14 +1050,25 @@ Expression ParseFunctionAs() Expression[] args = ParseArgumentList(); - if (args.Length != 1) + if (args.Length != 1 && args.Length != 2) { - throw ParseError(errorPos, Res.FunctionRequiresOneArg, functionName); + throw ParseError(errorPos, Res.FunctionRequiresOneOrTwoArgs, functionName); } - Type resolvedType = ResolveTypeFromArgumentExpression(functionName, args[0]); + Expression typeArgument; + Expression it; + if (args.Length == 1) + { + typeArgument = args[0]; + it = _it; + } + else + { + typeArgument = args[1]; + it = args[0]; + } - return Expression.TypeAs(_it, resolvedType); + return Expression.TypeAs(it, ResolveTypeFromArgumentExpression(functionName, typeArgument, args.Length)); } // Cast(...) function @@ -1059,14 +1080,25 @@ Expression ParseFunctionCast() Expression[] args = ParseArgumentList(); - if (args.Length != 1) + if (args.Length != 1 && args.Length != 2) { - throw ParseError(errorPos, Res.FunctionRequiresOneArg, functionName); + throw ParseError(errorPos, Res.FunctionRequiresOneOrTwoArgs, functionName); } - Type resolvedType = ResolveTypeFromArgumentExpression(functionName, args[0]); + Expression typeArgument; + Expression it; + if (args.Length == 1) + { + typeArgument = args[0]; + it = _it; + } + else + { + typeArgument = args[1]; + it = args[0]; + } - return Expression.ConvertChecked(_it, resolvedType); + return Expression.ConvertChecked(it, ResolveTypeFromArgumentExpression(functionName, typeArgument, args.Length)); } Expression GenerateConditional(Expression test, Expression expressionIfTrue, Expression expressionIfFalse, bool nullPropagating, int errorPos) @@ -1101,8 +1133,7 @@ Expression GenerateConditional(Expression test, Expression expressionIfTrue, Exp { // - create nullable constant from expressionIfTrue with type from expressionIfFalse // - convert expressionIfFalse to nullable (unless it's already nullable) - - Type nullableType = TypeHelper.ToNullableType(expressionIfFalse.Type); + var nullableType = TypeHelper.ToNullableType(expressionIfFalse.Type); expressionIfTrue = Expression.Constant(null, nullableType); if (!TypeHelper.IsNullableType(expressionIfFalse.Type)) @@ -1343,10 +1374,10 @@ private Expression CreateNewExpression(List properties, List propertyInfos = type.GetProperties(); + var propertyInfos = type.GetProperties(); if (type.GetTypeInfo().BaseType == typeof(DynamicClass)) { - propertyInfos = propertyInfos.Where(x => x.Name != "Item"); + propertyInfos = propertyInfos.Where(x => x.Name != "Item").ToArray(); } Type[] propertyTypes = propertyInfos.Select(p => p.PropertyType).ToArray(); @@ -1836,8 +1867,9 @@ Expression ParseEnumerable(Expression instance, Type elementType, string methodN return Expression.Call(callType, methodName, typeArgs, args); } - private Type ResolveTypeFromArgumentExpression(string functionName, Expression argumentExpression) + private Type ResolveTypeFromArgumentExpression(string functionName, Expression argumentExpression, int? arguments = null) { + string argument = arguments == null ? string.Empty : arguments == 1 ? "first " : "second "; switch (argumentExpression) { case ConstantExpression constantExpression: @@ -1850,21 +1882,16 @@ private Type ResolveTypeFromArgumentExpression(string functionName, Expression a return type; default: - throw ParseError(_textParser.CurrentToken.Pos, Res.FunctionRequiresOneNotNullArgOfType, functionName, "string or System.Type"); + throw ParseError(_textParser.CurrentToken.Pos, Res.FunctionRequiresNotNullArgOfType, functionName, argument, "string or System.Type"); } default: - throw ParseError(_textParser.CurrentToken.Pos, Res.FunctionRequiresOneNotNullArgOfType, functionName, "ConstantExpression"); + throw ParseError(_textParser.CurrentToken.Pos, Res.FunctionRequiresNotNullArgOfType, functionName, argument, "ConstantExpression"); } } private Type ResolveTypeStringFromArgument(string functionName, string typeName) { - if (string.IsNullOrEmpty(typeName)) - { - throw ParseError(_textParser.CurrentToken.Pos, Res.FunctionRequiresOneNotNullArg, functionName, typeName); - } - Type resultType = _typeFinder.FindTypeByName(typeName, new[] { _it, _parent, _root }, true); if (resultType == null) { @@ -1962,7 +1989,7 @@ static bool TryGetMemberName(Expression expression, out string memberName) var memberExpression = expression as MemberExpression; if (memberExpression == null && expression.NodeType == ExpressionType.Coalesce) { - memberExpression = (expression as BinaryExpression).Left as MemberExpression; + memberExpression = (expression as BinaryExpression)?.Left as MemberExpression; } if (memberExpression != null) diff --git a/src/System.Linq.Dynamic.Core/Parser/TypeFinder.cs b/src/System.Linq.Dynamic.Core/Parser/TypeFinder.cs index 77176b20..08e27cd6 100644 --- a/src/System.Linq.Dynamic.Core/Parser/TypeFinder.cs +++ b/src/System.Linq.Dynamic.Core/Parser/TypeFinder.cs @@ -24,8 +24,7 @@ public Type FindTypeByName(string name, ParameterExpression[] expressions, bool _keywordsHelper.TryGetValue(name, out object type); - Type result = type as Type; - if (result != null) + if (type is Type result) { return result; } diff --git a/src/System.Linq.Dynamic.Core/Res.cs b/src/System.Linq.Dynamic.Core/Res.cs index d21cca6e..2402f710 100644 --- a/src/System.Linq.Dynamic.Core/Res.cs +++ b/src/System.Linq.Dynamic.Core/Res.cs @@ -29,7 +29,8 @@ internal static class Res public const string FirstExprMustBeBool = "The first expression must be of type 'Boolean'"; public const string FunctionRequiresOneArg = "The '{0}' function requires one argument"; public const string FunctionRequiresOneNotNullArg = "The '{0}' function requires one argument which is not null."; - public const string FunctionRequiresOneNotNullArgOfType = "The '{0}' function requires one argument of type {1} which is not null."; + public const string FunctionRequiresNotNullArgOfType = "The '{0}' function requires the {1}argument to be not null and of type {2}."; + public const string FunctionRequiresOneOrTwoArgs = "The '{0}' function requires 1 or 2 arguments"; public const string HexCharExpected = "Hexadecimal character expected"; public const string IQueryableProviderNotAsync = "The provider for the source IQueryable doesn't implement IAsyncQueryProvider/IDbAsyncQueryProvider. Only providers that implement IAsyncQueryProvider/IDbAsyncQueryProvider can be used for Entity Framework asynchronous operations."; public const string IdentifierExpected = "Identifier expected"; diff --git a/test/System.Linq.Dynamic.Core.Tests/Entities/EmployeeWrapper.cs b/test/System.Linq.Dynamic.Core.Tests/Entities/EmployeeWrapper.cs new file mode 100644 index 00000000..bb070dbb --- /dev/null +++ b/test/System.Linq.Dynamic.Core.Tests/Entities/EmployeeWrapper.cs @@ -0,0 +1,7 @@ +namespace System.Linq.Dynamic.Core.Tests.Entities +{ + public class EmployeeWrapper + { + public BaseEmployee Employee { get; set; } + } +} diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Is,OfType,As,Cast.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Is,OfType,As,Cast.cs index dd76fb08..8f0b4b2c 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Is,OfType,As,Cast.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Is,OfType,As,Cast.cs @@ -289,7 +289,8 @@ public void CastToType_Dynamic_ActingOnIt() // Assign var qry = new BaseEmployee[] { - new Worker { Name = "1", Other = "x" }, new Worker { Name = "2" } + new Worker { Name = "1", Other = "x" }, + new Worker { Name = "2" } }.AsQueryable(); // Act @@ -306,17 +307,36 @@ public void IsAndCastToType_Dynamic_ActingOnIt_And_GetProperty() // Assign var qry = new BaseEmployee[] { - new Worker { Name = "1", Other = "x" }, new Boss { Name = "2", Function = "y" } + new Worker { Name = "1", Other = "x" }, + new Boss { Name = "2", Function = "y" } }.AsQueryable(); // Act - var cast = qry.Select(c => (c is Worker) ? (c as Worker).Other : "-").ToArray(); + var cast = qry.Select(c => c is Worker ? ((Worker)c).Other : "-").ToArray(); var castDynamic = qry.Select("iif(Is(\"System.Linq.Dynamic.Core.Tests.Entities.Worker\"), Cast(\"System.Linq.Dynamic.Core.Tests.Entities.Worker\").Other, \"-\")").ToDynamicArray(); // Assert Check.That(cast.Length).Equals(castDynamic.Length); } + [Fact] + public void IsAndCastToType_Dynamic_ActingOnProperty_And_GetProperty() + { + // Assign + var qry = new [] + { + new EmployeeWrapper { Employee = new Worker { Name = "1", Other = "x" } }, + new EmployeeWrapper { Employee = new Boss { Name = "2", Function = "y" } } + }.AsQueryable(); + + // Act + var cast = qry.Select(c => c.Employee is Worker ? ((Worker) c.Employee).Other : "-").ToArray(); + var castDynamic = qry.Select("iif(Is(Employee, \"System.Linq.Dynamic.Core.Tests.Entities.Worker\"), Cast(Employee, \"System.Linq.Dynamic.Core.Tests.Entities.Worker\").Other, \"-\")").ToDynamicArray(); + + // Assert + Check.That(cast.Length).Equals(castDynamic.Length); + } + [Fact] public void CastToType_Dynamic_ActingOnIt_WithType() {