diff --git a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs index 8908434dd..fc1268654 100644 --- a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs +++ b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs @@ -1802,10 +1802,15 @@ public static IQueryable Select(this IQueryable source, Parsin bool createParameterCtor = config.EvaluateGroupByAtDatabase || SupportsLinqToObjects(config, source); LambdaExpression lambda = DynamicExpressionParser.ParseLambda(config, createParameterCtor, source.ElementType, typeof(TResult), selector, args); - var optimized = OptimizeExpression(Expression.Call( - typeof(Queryable), nameof(Queryable.Select), + var methodCallExpression = Expression.Call( + typeof(Queryable), + nameof(Queryable.Select), new[] { source.ElementType, typeof(TResult) }, - source.Expression, Expression.Quote(lambda))); + source.Expression, + Expression.Quote(lambda) + ); + + var optimized = OptimizeExpression(methodCallExpression); return source.Provider.CreateQuery(optimized); } diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index c16ec5bc0..ada4b2b95 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -20,6 +20,9 @@ namespace System.Linq.Dynamic.Core.Parser; /// public class ExpressionParser { + private static readonly string[] OutKeywords = { "out", "$out" }; + private const string DiscardVariable = "_"; + private const string MethodOrderBy = nameof(Queryable.OrderBy); private const string MethodOrderByDescending = nameof(Queryable.OrderByDescending); private const string MethodThenBy = nameof(Queryable.ThenBy); @@ -165,7 +168,31 @@ public Expression Parse(Type? resultType, bool createParameterCtor = true) return expr; } -#pragma warning disable 0219 + // out keyword + private Expression ParseOutKeyword() + { + if (_textParser.CurrentToken.Id == TokenId.Identifier && OutKeywords.Contains(_textParser.CurrentToken.Text)) + { + // Go to next token (which should be a '_') + _textParser.NextToken(); + + var variableName = _textParser.CurrentToken.Text; + if (variableName != DiscardVariable) + { + throw ParseError(_textParser.CurrentToken.Pos, Res.OutKeywordRequiresDiscard); + } + + // Advance to next token + _textParser.NextToken(); + + // Use MakeByRefType() to indicate that it's a by-reference type because C# uses this for both 'ref' and 'out' parameters. + // The "typeof(object).MakeByRefType()" is used, this will be changed later in the flow to the real type. + return Expression.Parameter(typeof(object).MakeByRefType(), variableName); + } + + return ParseConditionalOperator(); + } + internal IList ParseOrdering(bool forceThenBy = false) { var orderings = new List(); @@ -206,7 +233,6 @@ internal IList ParseOrdering(bool forceThenBy = false) _textParser.ValidateToken(TokenId.End, Res.SyntaxError); return orderings; } -#pragma warning restore 0219 // ?: operator private Expression ParseConditionalOperator() @@ -931,9 +957,9 @@ private Expression ParseIdentifier() var isValidKeyWord = _keywordsHelper.TryGetValue(_textParser.CurrentToken.Text, out var value); - + bool shouldPrioritizeType = true; - + if (_parsingConfig.PrioritizePropertyOrFieldOverTheType && value is Type) { bool isPropertyOrField = _it != null && FindPropertyOrField(_it.Type, _textParser.CurrentToken.Text, false) != null; @@ -1774,16 +1800,19 @@ private Expression ParseMemberAccess(Type? type, Expression? expression) throw ParseError(errorPos, Res.MethodsAreInaccessible, TypeHelper.GetTypeName(method.DeclaringType!)); } - if (method.IsGenericMethod) + MethodInfo methodToCall; + if (!method.IsGenericMethod) + { + methodToCall = method; + } + else { var genericParameters = method.GetParameters().Where(p => p.ParameterType.IsGenericParameter); var typeArguments = genericParameters.Select(a => args[a.Position].Type); - var constructedMethod = method.MakeGenericMethod(typeArguments.ToArray()); - - return Expression.Call(expression, constructedMethod, args); + methodToCall = method.MakeGenericMethod(typeArguments.ToArray()); } - - return Expression.Call(expression, method, args); + + return CallMethod(expression, methodToCall, args); default: throw ParseError(errorPos, Res.AmbiguousMethodInvocation, id, TypeHelper.GetTypeName(type)); @@ -1848,6 +1877,59 @@ private Expression ParseMemberAccess(Type? type, Expression? expression) throw ParseError(errorPos, Res.UnknownPropertyOrField, id, TypeHelper.GetTypeName(type)); } + private static Expression CallMethod(Expression? expression, MethodInfo methodToCall, Expression[] args) + { +#if NET35 + return Expression.Call(expression, methodToCall, args); +#else + if (!args.OfType().Any(p => p.IsByRef)) + { + return Expression.Call(expression, methodToCall, args); + } + + // A list which is used to store all method arguments. + var newList = new List(); + + // A list which contains the variable expression for the 'out' parameter, and also contains the returnValue variable. + var blockList = new List(); + + foreach (var arg in args) + { + if (arg is ParameterExpression { IsByRef: true } parameterExpression) + { + // Create a variable expression to hold the 'out' parameter. + var variable = Expression.Variable(parameterExpression.Type, parameterExpression.Name); + + newList.Add(variable); + blockList.Add(variable); + } + else + { + newList.Add(arg); + } + } + + // Create a method call expression to call the method + var methodCall = Expression.Call(expression, methodToCall, newList); + + // Create a variable to hold the return value + var returnValue = Expression.Variable(methodToCall.ReturnType); + + // Add this return variable to the blockList + blockList.Add(returnValue); + + // Create the block to return the boolean value. + var block = Expression.Block( + blockList.ToArray(), + Expression.Assign(returnValue, methodCall), + returnValue + ); + + // Create the lambda expression (note that expression must be a ParameterExpression). + return Expression.Lambda(block, (ParameterExpression)expression!); +#endif + } + private Expression ParseAsLambda(string id) { // This might be an internal variable for use within a lambda expression, so store it as such @@ -2021,7 +2103,7 @@ private Expression ParseEnumerable(Expression instance, Type elementType, string private Type ResolveTypeFromArgumentExpression(string functionName, Expression argumentExpression, int? arguments = null) { - string argument = arguments == null ? string.Empty : arguments == 1 ? "first " : "second "; + var argument = arguments == null ? string.Empty : arguments == 1 ? "first " : "second "; switch (argumentExpression) { @@ -2088,7 +2170,7 @@ private Expression[] ParseArguments() var argList = new List(); while (true) { - var argumentExpression = ParseConditionalOperator(); + var argumentExpression = ParseOutKeyword(); _expressionHelper.WrapConstantExpression(ref argumentExpression); @@ -2102,6 +2184,11 @@ private Expression[] ParseArguments() _textParser.NextToken(); } + //if (argList.OfType().Count() > 1) + //{ + // throw ParseError(_textParser.CurrentToken.Pos, Res.OutVariableSingleRequired); + //} + return argList.ToArray(); } diff --git a/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs b/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs index 3a926a2e3..d5d667b97 100644 --- a/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs +++ b/src/System.Linq.Dynamic.Core/Parser/SupportedMethods/MethodFinder.cs @@ -170,7 +170,7 @@ public int FindIndexer(Type type, Expression[] args, out MethodBase? method) return 0; } - bool IsApplicable(MethodData method, Expression[] args) + private bool IsApplicable(MethodData method, Expression[] args) { bool isParamArray = method.Parameters.Length > 0 && method.Parameters.Last().IsDefined(typeof(ParamArrayAttribute), false); @@ -221,18 +221,30 @@ bool IsApplicable(MethodData method, Expression[] args) } else { - ParameterInfo pi = method.Parameters[i]; - if (pi.IsOut) + var methodParameter = method.Parameters[i]; + if (methodParameter.IsOut && args[i] is ParameterExpression parameterExpression) { +#if NET35 return false; - } +#else + if (!parameterExpression.IsByRef) + { + return false; + } - var promotedExpression = _parsingConfig.ExpressionPromoter.Promote(args[i], pi.ParameterType, false, method.MethodBase.DeclaringType != typeof(IEnumerableSignatures)); - if (promotedExpression == null) + promotedArgs[i] = Expression.Parameter(methodParameter.ParameterType, methodParameter.Name); +#endif + } + else { - return false; + var promotedExpression = _parsingConfig.ExpressionPromoter.Promote(args[i], methodParameter.ParameterType, false, method.MethodBase.DeclaringType != typeof(IEnumerableSignatures)); + if (promotedExpression == null) + { + return false; + } + + promotedArgs[i] = promotedExpression; } - promotedArgs[i] = promotedExpression; } } diff --git a/src/System.Linq.Dynamic.Core/Res.cs b/src/System.Linq.Dynamic.Core/Res.cs index 2402f710b..0b897a856 100644 --- a/src/System.Linq.Dynamic.Core/Res.cs +++ b/src/System.Linq.Dynamic.Core/Res.cs @@ -1,81 +1,81 @@ -namespace System.Linq.Dynamic.Core +namespace System.Linq.Dynamic.Core; + +internal static class Res { - internal static class Res - { - public const string AmbiguousConstructorInvocation = "Ambiguous invocation of '{0}' constructor"; - public const string AmbiguousIndexerInvocation = "Ambiguous invocation of indexer in type '{0}'"; - public const string AmbiguousMethodInvocation = "Ambiguous invocation of method '{0}' in type '{1}'"; - public const string ArgsIncompatibleWithLambda = "Argument list incompatible with lambda expression"; - public const string BinraryCharExpected = "Binary character expected"; - public const string BothTypesConvertToOther = "Both of the types '{0}' and '{1}' convert to the other"; - public const string CannotConvertValue = "A value of type '{0}' cannot be converted to type '{1}'"; - public const string CannotIndexMultiDimArray = "Indexing of multi-dimensional arrays is not supported"; - public const string CloseBracketExpected = "']' expected"; - public const string CloseBracketOrCommaExpected = "']' or ',' expected"; - public const string CloseParenOrCommaExpected = "')' or ',' expected"; - public const string CloseParenOrOperatorExpected = "')' or operator expected"; - public const string ColonExpected = "':' expected"; - public const string DigitExpected = "Digit expected"; - public const string DotExpected = "'.' expected"; - public const string DotOrOpenParenExpected = "'.' or '(' expected"; - public const string DotOrOpenParenOrStringLiteralExpected = "'.' or '(' or string literal expected"; - public const string DynamicExpandoObjectIsNotSupported = "Dynamic / ExpandoObject is not supported in .NET 3.5, UAP and .NETStandard 1.3"; - public const string DuplicateIdentifier = "The identifier '{0}' was defined more than once"; - public const string EnumTypeNotFound = "Enum type '{0}' not found"; - public const string EnumValueExpected = "Enum value expected"; - public const string EnumValueNotDefined = "Enum value '{0}' is not defined in enum type '{1}'"; - public const string ExpressionExpected = "Expression expected"; - public const string ExpressionTypeMismatch = "Expression of type '{0}' expected"; - 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 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"; - public const string IdentifierImplementingInterfaceExpected = "Identifier implementing interface '{0}' expected"; - public const string IifRequiresThreeArgs = "The 'iif' function requires three arguments"; - public const string IncompatibleOperand = "Operator '{0}' incompatible with operand type '{1}'"; - public const string IncompatibleOperands = "Operator '{0}' incompatible with operand types '{1}' and '{2}'"; - public const string IncompatibleTypes = "Types '{0}' and '{1}' are incompatible"; - public const string InvalidBinaryIntegerLiteral = "Invalid binary integer literal '{0}'"; - public const string InvalidCharacter = "Syntax error '{0}'"; - public const string InvalidCharacterLiteral = "Character literal must contain exactly one character"; - public const string InvalidIndex = "Array index must be an integer expression"; - public const string InvalidIntegerLiteral = "Invalid integer literal '{0}'"; - public const string InvalidIntegerQualifier = "Invalid integer literal qualifier '{0}'"; - public const string InvalidRealLiteral = "Invalid real literal '{0}'"; - public const string InvalidStringQuoteCharacter = "An escaped string should start with a double (\") or a single (') quote."; - public const string InvalidStringLength = "String '{0}' should have at least {1} characters."; - public const string IsNullRequiresTwoArgs = "The 'isnull' function requires two arguments"; - public const string MethodIsVoid = "Method '{0}' in type '{1}' does not return a value"; - public const string MethodsAreInaccessible = "Methods on type '{0}' are not accessible"; - public const string MinusCannotBeAppliedToUnsignedInteger = "'-' cannot be applied to unsigned integers."; - public const string MissingAsClause = "Expression is missing an 'as' clause"; - public const string NeitherTypeConvertsToOther = "Neither of the types '{0}' and '{1}' converts to the other"; - public const string NoApplicableAggregate = "No applicable aggregate method '{0}({1})' exists"; - public const string NoApplicableIndexer = "No applicable indexer exists in type '{0}'"; - public const string NoApplicableMethod = "No applicable method '{0}' exists in type '{1}'"; - public const string NoItInScope = "No 'it' is in scope"; - public const string NoMatchingConstructor = "No matching constructor in type '{0}'"; - public const string NoParentInScope = "No 'parent' is in scope"; - public const string NoRootInScope = "No 'root' is in scope"; - public const string NullPropagationRequiresCorrectArgs = "The 'np' (null-propagation) function requires 1 or 2 arguments"; - public const string NullPropagationRequiresValidExpression = "The 'np' (null-propagation) function requires the first argument to be a MemberExpression, ParameterExpression or MethodCallExpression"; - public const string OpenBracketExpected = "'[' expected"; - public const string OpenCurlyParenExpected = "'{' expected"; - public const string OpenParenExpected = "'(' expected"; - public const string OpenParenOrIdentifierExpected = "'(' or Identifier expected"; - public const string ParseExceptionFormat = "{0} (at index {1})"; - public const string SyntaxError = "Syntax error"; - public const string TokenExpected = "{0} expected"; - public const string TypeHasNoNullableForm = "Type '{0}' has no nullable form"; - public const string TypeNotFound = "Type '{0}' not found"; - public const string UnexpectedUnclosedString = "Unexpected end of string with unclosed string at position {0} near '{1}'."; - public const string UnexpectedUnrecognizedEscapeSequence = "Unexpected unrecognized escape sequence at position {0} near '{1}'."; - public const string UnknownIdentifier = "Unknown identifier '{0}'"; - public const string UnknownPropertyOrField = "No property or field '{0}' exists in type '{1}'"; - public const string UnterminatedStringLiteral = "Unterminated string literal"; - } -} + public const string AmbiguousConstructorInvocation = "Ambiguous invocation of '{0}' constructor"; + public const string AmbiguousIndexerInvocation = "Ambiguous invocation of indexer in type '{0}'"; + public const string AmbiguousMethodInvocation = "Ambiguous invocation of method '{0}' in type '{1}'"; + public const string ArgsIncompatibleWithLambda = "Argument list incompatible with lambda expression"; + public const string BinaryCharExpected = "Binary character expected"; + public const string BothTypesConvertToOther = "Both of the types '{0}' and '{1}' convert to the other"; + public const string CannotConvertValue = "A value of type '{0}' cannot be converted to type '{1}'"; + public const string CannotIndexMultiDimArray = "Indexing of multi-dimensional arrays is not supported"; + public const string CloseBracketExpected = "']' expected"; + public const string CloseBracketOrCommaExpected = "']' or ',' expected"; + public const string CloseParenOrCommaExpected = "')' or ',' expected"; + public const string CloseParenOrOperatorExpected = "')' or operator expected"; + public const string ColonExpected = "':' expected"; + public const string DigitExpected = "Digit expected"; + public const string DotExpected = "'.' expected"; + public const string DotOrOpenParenExpected = "'.' or '(' expected"; + public const string DotOrOpenParenOrStringLiteralExpected = "'.' or '(' or string literal expected"; + public const string DynamicExpandoObjectIsNotSupported = "Dynamic / ExpandoObject is not supported in .NET 3.5, UAP and .NETStandard 1.3"; + public const string DuplicateIdentifier = "The identifier '{0}' was defined more than once"; + public const string EnumTypeNotFound = "Enum type '{0}' not found"; + public const string EnumValueExpected = "Enum value expected"; + public const string EnumValueNotDefined = "Enum value '{0}' is not defined in enum type '{1}'"; + public const string ExpressionExpected = "Expression expected"; + public const string ExpressionTypeMismatch = "Expression of type '{0}' expected"; + 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 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"; + public const string IdentifierImplementingInterfaceExpected = "Identifier implementing interface '{0}' expected"; + public const string IifRequiresThreeArgs = "The 'iif' function requires three arguments"; + public const string IncompatibleOperand = "Operator '{0}' incompatible with operand type '{1}'"; + public const string IncompatibleOperands = "Operator '{0}' incompatible with operand types '{1}' and '{2}'"; + public const string IncompatibleTypes = "Types '{0}' and '{1}' are incompatible"; + public const string InvalidBinaryIntegerLiteral = "Invalid binary integer literal '{0}'"; + public const string InvalidCharacter = "Syntax error '{0}'"; + public const string InvalidCharacterLiteral = "Character literal must contain exactly one character"; + public const string InvalidIndex = "Array index must be an integer expression"; + public const string InvalidIntegerLiteral = "Invalid integer literal '{0}'"; + public const string InvalidIntegerQualifier = "Invalid integer literal qualifier '{0}'"; + public const string InvalidRealLiteral = "Invalid real literal '{0}'"; + public const string InvalidStringQuoteCharacter = "An escaped string should start with a double (\") or a single (') quote."; + public const string InvalidStringLength = "String '{0}' should have at least {1} characters."; + public const string IsNullRequiresTwoArgs = "The 'isnull' function requires two arguments"; + public const string MethodIsVoid = "Method '{0}' in type '{1}' does not return a value"; + public const string MethodsAreInaccessible = "Methods on type '{0}' are not accessible"; + public const string MinusCannotBeAppliedToUnsignedInteger = "'-' cannot be applied to unsigned integers."; + public const string MissingAsClause = "Expression is missing an 'as' clause"; + public const string NeitherTypeConvertsToOther = "Neither of the types '{0}' and '{1}' converts to the other"; + public const string NoApplicableAggregate = "No applicable aggregate method '{0}({1})' exists"; + public const string NoApplicableIndexer = "No applicable indexer exists in type '{0}'"; + public const string NoApplicableMethod = "No applicable method '{0}' exists in type '{1}'"; + public const string NoItInScope = "No 'it' is in scope"; + public const string NoMatchingConstructor = "No matching constructor in type '{0}'"; + public const string NoParentInScope = "No 'parent' is in scope"; + public const string NoRootInScope = "No 'root' is in scope"; + public const string NullPropagationRequiresCorrectArgs = "The 'np' (null-propagation) function requires 1 or 2 arguments"; + public const string NullPropagationRequiresValidExpression = "The 'np' (null-propagation) function requires the first argument to be a MemberExpression, ParameterExpression or MethodCallExpression"; + public const string OpenBracketExpected = "'[' expected"; + public const string OpenCurlyParenExpected = "'{' expected"; + public const string OpenParenExpected = "'(' expected"; + public const string OpenParenOrIdentifierExpected = "'(' or Identifier expected"; + public const string OutKeywordRequiresDiscard = "When using an out variable, a discard '_' is required."; + public const string ParseExceptionFormat = "{0} (at index {1})"; + public const string SyntaxError = "Syntax error"; + public const string TokenExpected = "{0} expected"; + public const string TypeHasNoNullableForm = "Type '{0}' has no nullable form"; + public const string TypeNotFound = "Type '{0}' not found"; + public const string UnexpectedUnclosedString = "Unexpected end of string with unclosed string at position {0} near '{1}'."; + public const string UnexpectedUnrecognizedEscapeSequence = "Unexpected unrecognized escape sequence at position {0} near '{1}'."; + public const string UnknownIdentifier = "Unknown identifier '{0}'"; + public const string UnknownPropertyOrField = "No property or field '{0}' exists in type '{1}'"; + public const string UnterminatedStringLiteral = "Unterminated string literal"; +} \ 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 82284d7a3..7369a3ddf 100644 --- a/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs +++ b/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs @@ -495,7 +495,7 @@ private void ValidateBinaryChar() { if (!IsZeroOrOne(_ch)) { - throw ParseError(_textPos, Res.BinraryCharExpected); + throw ParseError(_textPos, Res.BinaryCharExpected); } } diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs index b858dc5c8..0c51ea52e 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.MethodCall.cs @@ -1,5 +1,6 @@ using System.Collections.Generic; using System.Linq.Dynamic.Core.CustomTypeProviders; +using System.Linq.Dynamic.Core.Exceptions; using System.Linq.Dynamic.Core.Tests.Helpers.Models; using FluentAssertions; using Moq; @@ -33,6 +34,69 @@ private static ParsingConfig CreateParsingConfigForStaticMethodCallTests() }; } + [Fact] + public void ExpressionTests_MethodCall_WithArgument_And_1_OutArgument() + { + // Arrange + var config = CreateParsingConfigForMethodCallTests(); + var users = User.GenerateSampleModels(5); + + // Act + string s = ""; + var expected = users.Select(u => u.TryParseWithArgument(u.UserName, out s)); + var result = users.AsQueryable().Select(config, "TryParseWithArgument(it.UserName, $out _)"); + + // Assert + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public void ExpressionTests_MethodCall_WithArgument_And_2_OutArguments() + { + // Arrange + var config = CreateParsingConfigForMethodCallTests(); + var users = User.GenerateSampleModels(5); + + // Act + string s = "?"; + int i = -1; + var expected = users.Select(u => u.TryParseWithArgumentAndTwoOut(u.UserName, out s, out i)); + var result = users.AsQueryable().Select(config, "TryParseWithArgumentAndTwoOut(UserName, out _, out _)"); + + // Assert + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public void ExpressionTests_MethodCall_WithoutArgument_And_OutArgument() + { + // Arrange + var config = CreateParsingConfigForMethodCallTests(); + var users = User.GenerateSampleModels(5); + + // Act + string s = ""; + var expected = users.Select(u => u.TryParseWithoutArgument(out s)); + var result = users.AsQueryable().Select(config, "TryParseWithoutArgument(out _)"); + + // Assert + result.Should().BeEquivalentTo(expected); + } + + [Fact] + public void ExpressionTests_MethodCall_WithArgument_And_NoDiscardForOutArgument_ThrowsException() + { + // Arrange + var config = CreateParsingConfigForMethodCallTests(); + var users = User.GenerateSampleModels(5); + + // Act + Action action = () => users.AsQueryable().Select(config, "TryParseWithArgument(it.UserName, $out x)"); + + // Assert + action.Should().Throw().WithMessage("When using an out variable, a discard '_' is required."); + } + [Fact] public void ExpressionTests_MethodCall_NoParams() { diff --git a/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs b/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs index eec7384cb..801f8099b 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Helpers/Models/User.cs @@ -1,70 +1,92 @@ using System.Collections.Generic; -namespace System.Linq.Dynamic.Core.Tests.Helpers.Models +namespace System.Linq.Dynamic.Core.Tests.Helpers.Models; + +public class User { - public class User - { - public Guid Id { get; set; } + public Guid Id { get; set; } - public SnowflakeId SnowflakeId { get; set; } + public SnowflakeId SnowflakeId { get; set; } - public string UserName { get; set; } + public string UserName { get; set; } - public int? NullableInt { get; set; } + public int? NullableInt { get; set; } - public int Income { get; set; } + public int Income { get; set; } - public UserProfile Profile { get; set; } + public UserProfile Profile { get; set; } - public UserState State { get; set; } + public UserState State { get; set; } - public List Roles { get; set; } + public List Roles { get; set; } - public bool TestMethod1() - { - return true; - } + public bool TestMethod1() + { + return true; + } + + public bool TestMethod2(User other) + { + return true; + } + + public bool TestMethod3(User other) + { + return Id == other.Id; + } + + public bool TryParseWithoutArgument(out string xxx) + { + return TryParseWithArgument(UserName, out xxx); + } - public bool TestMethod2(User other) + public bool TryParseWithArgument(string s, out string xxx) + { + if (s.EndsWith("1") || s.EndsWith("2")) { + xxx = UserName; return true; } - public bool TestMethod3(User other) - { - return Id == other.Id; - } + xxx = ""; + return false; + } + + public bool TryParseWithArgumentAndTwoOut(string s, out string xxx, out int x) + { + x = 0; + return TryParseWithArgument(s, out xxx) && int.TryParse(s, out x); + } + + public static IList GenerateSampleModels(int total, bool allowNullableProfiles = false) + { + var list = new List(); - public static IList GenerateSampleModels(int total, bool allowNullableProfiles = false) + for (int i = 0; i < total; i++) { - var list = new List(); + var user = new User + { + Id = Guid.NewGuid(), + SnowflakeId = new SnowflakeId(((ulong)long.MaxValue + (ulong)i + 2UL)), + UserName = "User" + i, + Income = 1 + (i % 15) * 100 + }; - for (int i = 0; i < total; i++) + if (!allowNullableProfiles || (i % 8) != 5) { - var user = new User + user.Profile = new UserProfile { - Id = Guid.NewGuid(), - SnowflakeId = new SnowflakeId(((ulong)long.MaxValue + (ulong)i + 2UL)), - UserName = "User" + i, - Income = 1 + (i % 15) * 100 + FirstName = "FirstName" + i, + LastName = "LastName" + i, + Age = (i % 50) + 18 }; - - if (!allowNullableProfiles || (i % 8) != 5) - { - user.Profile = new UserProfile - { - FirstName = "FirstName" + i, - LastName = "LastName" + i, - Age = (i % 50) + 18 - }; - } - - user.Roles = new List(Role.StandardRoles); - - list.Add(user); } - return list.ToArray(); + user.Roles = new List(Role.StandardRoles); + + list.Add(user); } + + return list.ToArray(); } -} +} \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/MethodFinderTest.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/MethodFinderTest.cs index 16564d945..a1afadd49 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/MethodFinderTest.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/MethodFinderTest.cs @@ -3,21 +3,20 @@ using Xunit; using static System.Linq.Expressions.Expression; -namespace System.Linq.Dynamic.Core.Tests.Parser +namespace System.Linq.Dynamic.Core.Tests.Parser; + +public class MethodFinderTest { - public class MethodFinderTest + [Fact] + public void MethodsOfDynamicLinqAndSystemLinqShouldBeEqual() { - [Fact] - public void MethodsOfDynamicLinqAndSystemLinqShouldBeEqual() - { - Expression> expr = x => x.ToString(); + Expression> expr = x => x.ToString(); - var selector = "ToString()"; - var prm = Parameter(typeof(int?)); - var parser = new ExpressionParser(new[] { prm }, selector, new object[] { }, ParsingConfig.Default); - var expr1 = parser.Parse(null); + var selector = "ToString()"; + var prm = Parameter(typeof(int?)); + var parser = new ExpressionParser(new[] { prm }, selector, new object[] { }, ParsingConfig.Default); + var expr1 = parser.Parse(null); - Assert.Equal(((MethodCallExpression)expr.Body).Method.DeclaringType, ((MethodCallExpression)expr1).Method.DeclaringType); - } + Assert.Equal(((MethodCallExpression)expr.Body).Method.DeclaringType, ((MethodCallExpression)expr1).Method.DeclaringType); } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj b/test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj index 1f82ea88a..3a27dfd4a 100644 --- a/test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj +++ b/test/System.Linq.Dynamic.Core.Tests/System.Linq.Dynamic.Core.Tests.csproj @@ -6,6 +6,7 @@ full True latest + enable ../../src/System.Linq.Dynamic.Core/System.Linq.Dynamic.Core.snk {912FBF24-3CAE-4A50-B5EA-E525B9FAEC80}