From 8bc70807dc3b9b9f6d650cc4725b3f7771b5b18a Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 25 Dec 2024 10:23:19 +0100 Subject: [PATCH 1/5] Refactor KeywordsHelper, TypeFinder and update comments on ParsingConfig --- .../DynamicQueryableExtensions.cs | 15 ++- .../Exceptions/ParseException.cs | 123 ++++++++++-------- .../Parser/ExpressionParser.cs | 27 ++-- .../Parser/IKeywordsHelper.cs | 4 +- .../Parser/ITypeFinder.cs | 12 +- .../Parser/KeywordsHelper.cs | 50 +++---- .../Parser/TypeFinder.cs | 119 +++++++++-------- src/System.Linq.Dynamic.Core/ParsingConfig.cs | 8 +- .../Tokenizer/TextParser.cs | 4 +- .../EntitiesTests.cs | 14 +- .../Helpers/Entities/BlogContext.cs | 2 +- .../Helpers/Entities/Post.cs | 2 + .../Parser/TypeFinderTests.cs | 111 ++++++++-------- .../QueryableTests.Where.cs | 12 +- 14 files changed, 264 insertions(+), 239 deletions(-) diff --git a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs index 9768b1eca..917dc7456 100644 --- a/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs +++ b/src/System.Linq.Dynamic.Core/DynamicQueryableExtensions.cs @@ -9,6 +9,7 @@ using JetBrains.Annotations; using System.Linq.Dynamic.Core.Parser; using System.Linq.Dynamic.Core.Util; + #if !(SILVERLIGHT) using System.Diagnostics; #endif @@ -314,7 +315,7 @@ public static IQueryable Cast(this IQueryable source, Type type) Check.NotNull(source); Check.NotNull(type); - var optimized = OptimizeExpression(Expression.Call(null, _cast.MakeGenericMethod(new[] { type }), new[] { source.Expression })); + var optimized = OptimizeExpression(Expression.Call(null, _cast.MakeGenericMethod(type), source.Expression)); return source.Provider.CreateQuery(optimized); } @@ -330,10 +331,13 @@ public static IQueryable Cast(this IQueryable source, ParsingConfig config, stri { Check.NotNull(source); Check.NotNull(config); - Check.NotEmpty(typeName, nameof(typeName)); + Check.NotEmpty(typeName); var finder = new TypeFinder(config, new KeywordsHelper(config)); - Type type = finder.FindTypeByName(typeName, null, true)!; + if (!finder.TryFindTypeByName(typeName, null, true, out var type)) + { + throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.TypeNotFound, typeName)); + } return Cast(source, type); } @@ -1445,7 +1449,10 @@ public static IQueryable OfType(this IQueryable source, ParsingConfig config, st Check.NotEmpty(typeName); var finder = new TypeFinder(config, new KeywordsHelper(config)); - Type type = finder.FindTypeByName(typeName, null, true)!; + if (!finder.TryFindTypeByName(typeName, null, true, out var type)) + { + throw new ParseException(string.Format(CultureInfo.CurrentCulture, Res.TypeNotFound, typeName)); + } return OfType(source, type); } diff --git a/src/System.Linq.Dynamic.Core/Exceptions/ParseException.cs b/src/System.Linq.Dynamic.Core/Exceptions/ParseException.cs index fba8b3d1e..42cca6700 100644 --- a/src/System.Linq.Dynamic.Core/Exceptions/ParseException.cs +++ b/src/System.Linq.Dynamic.Core/Exceptions/ParseException.cs @@ -4,74 +4,85 @@ using System.Runtime.Serialization; #endif -namespace System.Linq.Dynamic.Core.Exceptions +namespace System.Linq.Dynamic.Core.Exceptions; + +/// +/// Represents errors that occur while parsing dynamic linq string expressions. +/// +#if !(SILVERLIGHT || UAP10_0 || NETSTANDARD || PORTABLE || WPSL || NETSTANDARD2_0) +[Serializable] +#endif +public sealed class ParseException : Exception { + private const int UnknownPosition = -1; + /// - /// Represents errors that occur while parsing dynamic linq string expressions. + /// The location in the parsed string that produced the . + /// If the value is -1, the position is unknown. /// -#if !(SILVERLIGHT || UAP10_0 || NETSTANDARD || PORTABLE || WPSL || NETSTANDARD2_0) - [Serializable] -#endif - public sealed class ParseException : Exception + public int Position { get; } + + /// + /// Initializes a new instance of the class with a specified error message and position. + /// + /// The message that describes the error. + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public ParseException(string message, Exception? innerException = null) : this(message, UnknownPosition, innerException) { - /// - /// The location in the parsed string that produced the . - /// - public int Position { get; } + } - /// - /// Initializes a new instance of the class with a specified error message and position. - /// - /// The message that describes the error. - /// The location in the parsed string that produced the - /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. - public ParseException(string message, int position, Exception? innerException = null) : base(message, innerException) + /// + /// Initializes a new instance of the class with a specified error message and position. + /// + /// The message that describes the error. + /// The location in the parsed string that produced the + /// The exception that is the cause of the current exception, or a null reference if no inner exception is specified. + public ParseException(string message, int position, Exception? innerException = null) : base(message, innerException) + { + Position = position; + } + + /// + /// Creates and returns a string representation of the current exception. + /// + /// A string representation of the current exception. + public override string ToString() + { + var text = string.Format(CultureInfo.CurrentCulture, Res.ParseExceptionFormat, Message, Position); + + if (InnerException != null) { - Position = position; + text = $"{text} ---> {InnerException}{Environment.NewLine} --- End of inner exception stack trace ---"; } - /// - /// Creates and returns a string representation of the current exception. - /// - /// A string representation of the current exception. - public override string ToString() + if (StackTrace != null) { - var text = string.Format(CultureInfo.CurrentCulture, Res.ParseExceptionFormat, Message, Position); - - if (InnerException != null) - { - text = $"{text} ---> {InnerException}{Environment.NewLine} --- End of inner exception stack trace ---"; - } - - if (StackTrace != null) - { - text = $"{text}{Environment.NewLine}{StackTrace}"; - } - - return text; + text = $"{text}{Environment.NewLine}{StackTrace}"; } + return text; + } + #if !(SILVERLIGHT || UAP10_0 || NETSTANDARD || PORTABLE || WPSL || NETSTANDARD2_0) - private ParseException(SerializationInfo info, StreamingContext context) : base(info, context) - { - Position = (int)info.GetValue("position", typeof(int)); - } + private ParseException(SerializationInfo info, StreamingContext context) : base(info, context) + { + Position = (int)info.GetValue("position", typeof(int))!; + } - /// - /// When overridden in a derived class, sets the with information about the exception. - /// - /// The that holds the serialized object data about the exception being thrown. - /// The that contains contextual information about the source or destination. - /// - /// - /// - /// - public override void GetObjectData(SerializationInfo info, StreamingContext context) - { - base.GetObjectData(info, context); + /// + /// When overridden in a derived class, sets the with information about the exception. + /// + /// The that holds the serialized object data about the exception being thrown. + /// The that contains contextual information about the source or destination. + /// + /// + /// + /// + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); - info.AddValue("position", Position); - } -#endif + info.AddValue("position", Position); } -} +#endif +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 635876159..528dc5365 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -906,8 +906,7 @@ private AnyOf ParseStringLiteral(bool forceParseAsString) if (_parsingConfig.SupportCastingToFullyQualifiedTypeAsString && !forceParseAsString && parsedStringValue.Length > 2 && parsedStringValue.Contains('.')) { // Try to resolve this string as a type - var type = _typeFinder.FindTypeByName(parsedStringValue, null, false); - if (type is { }) + if (_typeFinder.TryFindTypeByName(parsedStringValue, null, false, out var type)) { return type; } @@ -970,7 +969,7 @@ private Expression ParseIdentifier() { _textParser.ValidateToken(TokenId.Identifier); - var isValidKeyWord = _keywordsHelper.TryGetValue(_textParser.CurrentToken.Text, out var keywordOrType); + var isValid = _keywordsHelper.TryGetValue(_textParser.CurrentToken.Text, out var keywordOrType); var shouldPrioritizeType = true; if (_parsingConfig.PrioritizePropertyOrFieldOverTheType && keywordOrType.IsThird) @@ -983,7 +982,7 @@ private Expression ParseIdentifier() } } - if (isValidKeyWord && shouldPrioritizeType) + if (isValid && shouldPrioritizeType) { var keywordOrFunctionAllowed = !_usedForOrderBy || _usedForOrderBy && !_parsingConfig.RestrictOrderByToPropertyOrField; if (!keywordOrFunctionAllowed) @@ -1397,8 +1396,7 @@ private Expression ParseNew() _textParser.NextToken(); } - newType = _typeFinder.FindTypeByName(newTypeName, new[] { _it, _parent, _root }, false); - if (newType == null) + if (!_typeFinder.TryFindTypeByName(newTypeName, [_it, _parent, _root], false, out newType)) { throw ParseError(_textParser.CurrentToken.Pos, Res.TypeNotFound, newTypeName); } @@ -1543,6 +1541,7 @@ private Expression CreateNewExpression(List properties, List x.Name != "Item").ToArray(); } + var propertyTypes = propertyInfos.Select(p => p.PropertyType).ToArray(); var ctor = type.GetConstructor(propertyTypes); if (ctor != null) @@ -1550,7 +1549,7 @@ private Expression CreateNewExpression(List properties, List constructorParameters + var bindParametersSequentially = !properties.All(p => constructorParameters .Any(cp => cp.Name == p.Name && (cp.ParameterType == p.Type || p.Type == Nullable.GetUnderlyingType(cp.ParameterType)))); var expressionsPromoted = new List(); @@ -1564,9 +1563,10 @@ private Expression CreateNewExpression(List properties, List new { p, index }) .First(p => p.p.Name == cParameterName && (p.p.Type == propertyType || p.p.Type == Nullable.GetUnderlyingType(propertyType))); + // Promote from Type to Nullable Type if needed expressionsPromoted.Add(_parsingConfig.ExpressionPromoter.Promote(expressions[propertyAndIndex.index], propertyType, true, true)); } @@ -2027,8 +2027,7 @@ private Expression ParseAsEnumOrNestedClass(string id) } var typeAsString = string.Concat(parts.Take(parts.Count - 2).ToArray()); - var type = _typeFinder.FindTypeByName(typeAsString, null, true); - if (type == null) + if (!_typeFinder.TryFindTypeByName(typeAsString, null, true, out var type)) { throw ParseError(_textParser.CurrentToken.Pos, Res.TypeNotFound, typeAsString); } @@ -2233,20 +2232,20 @@ private Type ResolveTypeFromExpressionValue(string functionName, ConstantExpress private Type ResolveTypeStringFromArgument(string typeName) { - bool typeIsNullable = false; + var typeIsNullable = false; + if (typeName.EndsWith("?")) { typeName = typeName.TrimEnd('?'); typeIsNullable = true; } - var resultType = _typeFinder.FindTypeByName(typeName, new[] { _it, _parent, _root }, true); - if (resultType == null) + if (!_typeFinder.TryFindTypeByName(typeName, [_it, _parent, _root], true, out var type)) { throw ParseError(_textParser.CurrentToken.Pos, Res.TypeNotFound, typeName); } - return typeIsNullable ? TypeHelper.ToNullableType(resultType) : resultType; + return typeIsNullable ? TypeHelper.ToNullableType(type) : type; } private Expression[] ParseArgumentList() diff --git a/src/System.Linq.Dynamic.Core/Parser/IKeywordsHelper.cs b/src/System.Linq.Dynamic.Core/Parser/IKeywordsHelper.cs index 6fbd28bd8..c0d5b5ede 100644 --- a/src/System.Linq.Dynamic.Core/Parser/IKeywordsHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/IKeywordsHelper.cs @@ -3,7 +3,7 @@ namespace System.Linq.Dynamic.Core.Parser; -interface IKeywordsHelper +internal interface IKeywordsHelper { - bool TryGetValue(string name, out AnyOf keywordOrType); + bool TryGetValue(string text, out AnyOf value); } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/ITypeFinder.cs b/src/System.Linq.Dynamic.Core/Parser/ITypeFinder.cs index ece62c526..1d477547d 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ITypeFinder.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ITypeFinder.cs @@ -1,9 +1,9 @@ -using System.Linq.Expressions; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Expressions; -namespace System.Linq.Dynamic.Core.Parser +namespace System.Linq.Dynamic.Core.Parser; + +internal interface ITypeFinder { - interface ITypeFinder - { - Type? FindTypeByName(string name, ParameterExpression?[]? expressions, bool forceUseCustomTypeProvider); - } + bool TryFindTypeByName(string name, ParameterExpression?[]? expressions, bool forceUseCustomTypeProvider, [NotNullWhen(true)] out Type? type); } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs b/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs index dc55bc356..380e33849 100644 --- a/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs +++ b/src/System.Linq.Dynamic.Core/Parser/KeywordsHelper.cs @@ -15,20 +15,20 @@ internal class KeywordsHelper : IKeywordsHelper public const string SYMBOL_PARENT = "^"; public const string SYMBOL_ROOT = "~"; + public const string FUNCTION_AS = "as"; + public const string FUNCTION_CAST = "cast"; public const string FUNCTION_IIF = "iif"; + public const string FUNCTION_IS = "is"; public const string FUNCTION_ISNULL = "isnull"; public const string FUNCTION_NEW = "new"; public const string FUNCTION_NULLPROPAGATION = "np"; - public const string FUNCTION_IS = "is"; - public const string FUNCTION_AS = "as"; - public const string FUNCTION_CAST = "cast"; private readonly ParsingConfig _config; - // Keywords compare case depends on the value from ParsingConfig.IsCaseSensitive - private readonly Dictionary> _keywordMapping; + // Keywords, symbols and functions compare case depends on the value from ParsingConfig.IsCaseSensitive + private readonly Dictionary> _mappings; - // PreDefined Types are not IgnoreCase + // Pre-defined Types are not IgnoreCase private static readonly Dictionary PreDefinedTypeMapping = new(); // Custom DefinedTypes are not IgnoreCase @@ -47,7 +47,7 @@ public KeywordsHelper(ParsingConfig config) { _config = Check.NotNull(config); - _keywordMapping = new(config.IsCaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase) + _mappings = new(config.IsCaseSensitive ? StringComparer.Ordinal : StringComparer.OrdinalIgnoreCase) { { "true", Expression.Constant(true) }, { "false", Expression.Constant(false) }, @@ -68,9 +68,9 @@ public KeywordsHelper(ParsingConfig config) if (config.AreContextKeywordsEnabled) { - _keywordMapping.Add(KEYWORD_IT, KEYWORD_IT); - _keywordMapping.Add(KEYWORD_PARENT, KEYWORD_PARENT); - _keywordMapping.Add(KEYWORD_ROOT, KEYWORD_ROOT); + _mappings.Add(KEYWORD_IT, KEYWORD_IT); + _mappings.Add(KEYWORD_PARENT, KEYWORD_PARENT); + _mappings.Add(KEYWORD_ROOT, KEYWORD_ROOT); } // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract @@ -84,45 +84,45 @@ public KeywordsHelper(ParsingConfig config) } } - public bool TryGetValue(string name, out AnyOf keywordOrType) + public bool TryGetValue(string text, out AnyOf value) { - // 1. Try to get as keyword - if (_keywordMapping.TryGetValue(name, out var keyWord)) + // 1. Try to get as constant-expression, keyword, symbol or functions + if (_mappings.TryGetValue(text, out var expressionOrKeywordOrSymbolOrFunction)) { - keywordOrType = keyWord; + value = expressionOrKeywordOrSymbolOrFunction; return true; } - // 2. Try to get as predefined shorttype ("bool", "char", ...) - if (PredefinedTypesHelper.PredefinedTypesShorthands.TryGetValue(name, out var predefinedShortHandType)) + // 2. Try to get as predefined short-type ("bool", "char", ...) + if (PredefinedTypesHelper.PredefinedTypesShorthands.TryGetValue(text, out var predefinedShortHandType)) { - keywordOrType = predefinedShortHandType; + value = predefinedShortHandType; return true; } // 3. Try to get as predefined type ("Boolean", "System.Boolean", ..., "DateTime", "System.DateTime", ...) - if (PreDefinedTypeMapping.TryGetValue(name, out var predefinedType)) + if (PreDefinedTypeMapping.TryGetValue(text, out var predefinedType)) { - keywordOrType = predefinedType; + value = predefinedType; return true; } // 4. Try to get as an enum from the system namespace - if (_config.SupportEnumerationsFromSystemNamespace && EnumerationsFromMscorlib.PredefinedEnumerationTypes.TryGetValue(name, out var predefinedEnumType)) + if (_config.SupportEnumerationsFromSystemNamespace && EnumerationsFromMscorlib.PredefinedEnumerationTypes.TryGetValue(text, out var predefinedEnumType)) { - keywordOrType = predefinedEnumType; + value = predefinedEnumType; return true; } // 5. Try to get as custom type - if (_customTypeMapping.TryGetValue(name, out var customType)) + if (_customTypeMapping.TryGetValue(text, out var customType)) { - keywordOrType = customType; + value = customType; return true; } - // 6. Not found, return false - keywordOrType = default; + // Not found, return false + value = default; return false; } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/TypeFinder.cs b/src/System.Linq.Dynamic.Core/Parser/TypeFinder.cs index 0ee9f6008..368cbe662 100644 --- a/src/System.Linq.Dynamic.Core/Parser/TypeFinder.cs +++ b/src/System.Linq.Dynamic.Core/Parser/TypeFinder.cs @@ -1,91 +1,90 @@ -using System.Linq.Dynamic.Core.Validation; +using System.Diagnostics.CodeAnalysis; +using System.Linq.Dynamic.Core.Validation; using System.Linq.Expressions; -namespace System.Linq.Dynamic.Core.Parser +namespace System.Linq.Dynamic.Core.Parser; + +internal class TypeFinder : ITypeFinder { - internal class TypeFinder : ITypeFinder + private readonly IKeywordsHelper _keywordsHelper; + private readonly ParsingConfig _parsingConfig; + + public TypeFinder(ParsingConfig parsingConfig, IKeywordsHelper keywordsHelper) { - private readonly IKeywordsHelper _keywordsHelper; - private readonly ParsingConfig _parsingConfig; + _parsingConfig = Check.NotNull(parsingConfig); + _keywordsHelper = Check.NotNull(keywordsHelper); + } - public TypeFinder(ParsingConfig parsingConfig, IKeywordsHelper keywordsHelper) - { - Check.NotNull(parsingConfig); - Check.NotNull(keywordsHelper); + public bool TryFindTypeByName(string name, ParameterExpression?[]? expressions, bool forceUseCustomTypeProvider, [NotNullWhen(true)] out Type? type) + { + Check.NotEmpty(name); - _keywordsHelper = keywordsHelper; - _parsingConfig = parsingConfig; + if (_keywordsHelper.TryGetValue(name, out var keywordOrType) && keywordOrType.IsThird) + { + type = keywordOrType.Third; + return true; } - public Type? FindTypeByName(string name, ParameterExpression?[]? expressions, bool forceUseCustomTypeProvider) + if (expressions != null && TryResolveTypeUsingExpressions(name, expressions, out type)) { - Check.NotEmpty(name); + return true; + } + + return TryResolveTypeByUsingCustomTypeProvider(name, forceUseCustomTypeProvider, out type); + } - _keywordsHelper.TryGetValue(name, out var keywordOrType); + private bool TryResolveTypeByUsingCustomTypeProvider(string name, bool forceUseCustomTypeProvider, [NotNullWhen(true)] out Type? resolvedType) + { + resolvedType = default; - if (keywordOrType.IsThird) + if ((forceUseCustomTypeProvider || _parsingConfig.AllowNewToEvaluateAnyType) && _parsingConfig.CustomTypeProvider != null) + { + resolvedType = _parsingConfig.CustomTypeProvider.ResolveType(name); + if (resolvedType != null) { - return keywordOrType.Third; + return true; } - if (expressions != null && TryResolveTypeUsingExpressions(name, expressions, out var resolvedType)) + // In case the type is not found based on fullname, try to get the type on simple-name if allowed + if (_parsingConfig.ResolveTypesBySimpleName) { - return resolvedType; + resolvedType = _parsingConfig.CustomTypeProvider.ResolveTypeBySimpleName(name); + return resolvedType != null; } - - return ResolveTypeByUsingCustomTypeProvider(name, forceUseCustomTypeProvider); } - private Type? ResolveTypeByUsingCustomTypeProvider(string name, bool forceUseCustomTypeProvider) + return false; + } + + private bool TryResolveTypeUsingExpressions(string name, ParameterExpression?[] expressions, [NotNullWhen(true)] out Type? result) + { + foreach (var expression in expressions.OfType()) { - if ((forceUseCustomTypeProvider || _parsingConfig.AllowNewToEvaluateAnyType) && _parsingConfig.CustomTypeProvider != null) + if (name == expression.Type.Name) { - var resolvedType = _parsingConfig.CustomTypeProvider.ResolveType(name); - if (resolvedType != null) - { - return resolvedType; - } - - // In case the type is not found based on fullname, try to get the type on simplename if allowed - if (_parsingConfig.ResolveTypesBySimpleName) - { - return _parsingConfig.CustomTypeProvider.ResolveTypeBySimpleName(name); - } + result = expression.Type; + return true; } - return null; - } - - private bool TryResolveTypeUsingExpressions(string name, ParameterExpression?[] expressions, out Type? result) - { - foreach (var expression in expressions.Where(e => e != null)) + if (name == $"{expression.Type.Namespace}.{expression.Type.Name}") { - if (name == expression!.Type.Name) - { - result = expression.Type; - return true; - } + result = expression.Type; + return true; + } - if (name == $"{expression.Type.Namespace}.{expression.Type.Name}") + if (_parsingConfig is { ResolveTypesBySimpleName: true, CustomTypeProvider: not null }) + { + var possibleFullName = $"{expression.Type.Namespace}.{name}"; + var resolvedType = _parsingConfig.CustomTypeProvider.ResolveType(possibleFullName); + if (resolvedType != null) { - result = expression.Type; + result = resolvedType; return true; } - - if (_parsingConfig.ResolveTypesBySimpleName && _parsingConfig.CustomTypeProvider != null) - { - string possibleFullName = $"{expression.Type.Namespace}.{name}"; - var resolvedType = _parsingConfig.CustomTypeProvider.ResolveType(possibleFullName); - if (resolvedType != null) - { - result = resolvedType; - return true; - } - } } - - result = null; - return false; } + + result = null; + return false; } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/ParsingConfig.cs b/src/System.Linq.Dynamic.Core/ParsingConfig.cs index 43fc199a7..922e4dd25 100644 --- a/src/System.Linq.Dynamic.Core/ParsingConfig.cs +++ b/src/System.Linq.Dynamic.Core/ParsingConfig.cs @@ -39,7 +39,13 @@ public class ParsingConfig }; /// - /// Gets or sets if parameter, method, and properties resolution should be case-sensitive or not. + /// Defines if the resolution should be case-sensitive for: + /// - fields and properties + /// - (extension) methods + /// - constant expressions ("null", "true", "false") + /// - keywords ("it", "parent", "root") + /// - symbols ("$", "^", "~") + /// - functions ("as", "cast", "iif", "is", "isnull", "new", "np") /// /// Default value is false. /// diff --git a/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs b/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs index 158b0768e..c040a6346 100644 --- a/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs +++ b/src/System.Linq.Dynamic.Core/Tokenizer/TextParser.cs @@ -7,10 +7,10 @@ namespace System.Linq.Dynamic.Core.Tokenizer; /// /// TextParser which can be used to parse a text into tokens. /// -public class TextParser +internal class TextParser { private const char DefaultNumberDecimalSeparator = '.'; - private static readonly char[] EscapeCharacters = { '\\', 'a', 'b', 'f', 'n', 'r', 't', 'v' }; + private static readonly char[] EscapeCharacters = ['\\', 'a', 'b', 'f', 'n', 'r', 't', 'v']; private readonly char _numberDecimalSeparator; private readonly string _text; diff --git a/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.cs b/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.cs index f29fde4e7..7ea211e45 100644 --- a/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/EntitiesTests.cs @@ -27,7 +27,7 @@ public EntitiesTests(EntitiesTestsDatabaseFixture fixture) { builder.UseSqlServer(fixture.ConnectionString); } - + _context = new BlogContext(builder.Options); _context.Database.EnsureCreated(); #else @@ -42,14 +42,14 @@ private void InternalPopulateTestData() { return; } - + for (int i = 0; i < 25; i++) { var blog = new Blog { - X = i.ToString(), - Name = "Blog" + (i + 1), - BlogId = 1000 + i, + X = i.ToString(), + Name = "Blog" + (i + 1), + BlogId = 1000 + i, Created = DateTime.Now.AddDays(-Rnd.Next(0, 100)) }; @@ -57,13 +57,15 @@ private void InternalPopulateTestData() for (int j = 0; j < 10; j++) { + var postDate = DateTime.Today.AddDays(-Rnd.Next(0, 100)).AddSeconds(Rnd.Next(0, 30000)); var post = new Post { PostId = 10000 + i * 10 + j, Blog = blog, Title = $"Blog {i + 1} - Post {j + 1}", Content = "My Content", - PostDate = DateTime.Today.AddDays(-Rnd.Next(0, 100)).AddSeconds(Rnd.Next(0, 30000)), + PostDate = postDate, + CloseDate = Rnd.Next(0, 10) < 5 ? postDate.AddDays(1) : null, NumberOfReads = Rnd.Next(0, 5000) }; diff --git a/test/System.Linq.Dynamic.Core.Tests/Helpers/Entities/BlogContext.cs b/test/System.Linq.Dynamic.Core.Tests/Helpers/Entities/BlogContext.cs index 809a7350a..0d4cfb85f 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Helpers/Entities/BlogContext.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Helpers/Entities/BlogContext.cs @@ -21,7 +21,7 @@ public BlogContext(DbContextOptions options) public void EnableLogging() { var serviceProvider = this.GetInfrastructure(); - var loggerFactory = serviceProvider.GetService(); + var loggerFactory = serviceProvider.GetRequiredService(); loggerFactory.AddProvider(new DbLoggerProvider()); } diff --git a/test/System.Linq.Dynamic.Core.Tests/Helpers/Entities/Post.cs b/test/System.Linq.Dynamic.Core.Tests/Helpers/Entities/Post.cs index 1406b4e9c..00d2e9a6f 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Helpers/Entities/Post.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Helpers/Entities/Post.cs @@ -20,4 +20,6 @@ public class Post public int NumberOfReads { get; set; } public DateTime PostDate { get; set; } + + public DateTime? CloseDate { get; set; } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/TypeFinderTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/TypeFinderTests.cs index 904e1f9ac..a4175e0a0 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/TypeFinderTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/TypeFinderTests.cs @@ -1,77 +1,76 @@ -using Moq; -using NFluent; -using System.Linq.Dynamic.Core.CustomTypeProviders; +using System.Linq.Dynamic.Core.CustomTypeProviders; using System.Linq.Dynamic.Core.Parser; using System.Linq.Dynamic.Core.Tests.Entities; using System.Linq.Expressions; +using FluentAssertions; +using Moq; using Xunit; -namespace System.Linq.Dynamic.Core.Tests.Parser +namespace System.Linq.Dynamic.Core.Tests.Parser; + +public class TypeFinderTests { - public class TypeFinderTests - { - private readonly ParsingConfig _parsingConfig = new ParsingConfig(); - private readonly Mock _keywordsHelperMock; - private readonly Mock _dynamicTypeProviderMock; + private readonly ParsingConfig _parsingConfig; - private readonly TypeFinder _sut; + private readonly TypeFinder _sut; - public TypeFinderTests() - { - _dynamicTypeProviderMock = new Mock(); - _dynamicTypeProviderMock.Setup(dt => dt.ResolveType(typeof(BaseEmployee).FullName)).Returns(typeof(BaseEmployee)); - _dynamicTypeProviderMock.Setup(dt => dt.ResolveType(typeof(Boss).FullName)).Returns(typeof(Boss)); - _dynamicTypeProviderMock.Setup(dt => dt.ResolveType(typeof(Worker).FullName)).Returns(typeof(Worker)); - _dynamicTypeProviderMock.Setup(dt => dt.ResolveTypeBySimpleName("Boss")).Returns(typeof(Boss)); + public TypeFinderTests() + { + var dynamicTypeProviderMock = new Mock(); + dynamicTypeProviderMock.Setup(dt => dt.ResolveType(typeof(BaseEmployee).FullName!)).Returns(typeof(BaseEmployee)); + dynamicTypeProviderMock.Setup(dt => dt.ResolveType(typeof(Boss).FullName!)).Returns(typeof(Boss)); + dynamicTypeProviderMock.Setup(dt => dt.ResolveType(typeof(Worker).FullName!)).Returns(typeof(Worker)); + dynamicTypeProviderMock.Setup(dt => dt.ResolveTypeBySimpleName("Boss")).Returns(typeof(Boss)); - _parsingConfig = new ParsingConfig - { - CustomTypeProvider = _dynamicTypeProviderMock.Object - }; + _parsingConfig = new ParsingConfig + { + CustomTypeProvider = dynamicTypeProviderMock.Object + }; - _keywordsHelperMock = new Mock(); + var keywordsHelperMock = new Mock(); - _sut = new TypeFinder(_parsingConfig, _keywordsHelperMock.Object); - } + _sut = new TypeFinder(_parsingConfig, keywordsHelperMock.Object); + } - [Fact] - public void TypeFinder_FindTypeByName_With_SimpleTypeName_forceUseCustomTypeProvider_equals_false() - { - // Assign - _parsingConfig.ResolveTypesBySimpleName = true; + [Fact] + public void TypeFinder_TryFindTypeByName_With_SimpleTypeName_forceUseCustomTypeProvider_Equals_false() + { + // Assign + _parsingConfig.ResolveTypesBySimpleName = true; - // Act - Type result = _sut.FindTypeByName("Boss", null, forceUseCustomTypeProvider: false); + // Act + var result = _sut.TryFindTypeByName("Boss", null, forceUseCustomTypeProvider: false, out var type); - // Assert - Check.That(result).IsNull(); - } + // Assert + result.Should().BeFalse(); + } - [Fact] - public void TypeFinder_FindTypeByName_With_SimpleTypeName_forceUseCustomTypeProvider_equals_true() - { - // Assign - _parsingConfig.ResolveTypesBySimpleName = true; + [Fact] + public void TypeFinder_TryFindTypeByName_With_SimpleTypeName_forceUseCustomTypeProvider_Equals_true() + { + // Assign + _parsingConfig.ResolveTypesBySimpleName = true; - // Act - Type result = _sut.FindTypeByName("Boss", null, forceUseCustomTypeProvider: true); + // Act + var result = _sut.TryFindTypeByName("Boss", null, forceUseCustomTypeProvider: true, out var type); - // Assert - Check.That(result).Equals(typeof(Boss)); - } + // Assert + result.Should().BeTrue(); + type.Should().Be(); + } - [Fact] - public void TypeFinder_FindTypeByName_With_SimpleTypeName_basedon_it() - { - // Assign - _parsingConfig.ResolveTypesBySimpleName = true; - var expressions = new[] { Expression.Parameter(typeof(BaseEmployee)) }; + [Fact] + public void TypeFinder_TryFindTypeByName_With_SimpleTypeName_BasedOn_it() + { + // Assign + _parsingConfig.ResolveTypesBySimpleName = true; + var expressions = new[] { Expression.Parameter(typeof(BaseEmployee)) }; - // Act - Type result = _sut.FindTypeByName("Boss", expressions, forceUseCustomTypeProvider: false); + // Act + var result = _sut.TryFindTypeByName("Boss", expressions, forceUseCustomTypeProvider: false, out var type); - // Assert - Check.That(result).Equals(typeof(Boss)); - } + // Assert + result.Should().BeTrue(); + type.Should().Be(); } -} +} \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs index 4d029103c..1622a54ea 100644 --- a/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs +++ b/test/System.Linq.Dynamic.Core.Tests/QueryableTests.Where.cs @@ -82,9 +82,9 @@ public void Where_Dynamic_DateTime_NotEquals_Null() IQueryable queryable = new[] { new Post() }.AsQueryable(); // Act - var expected = queryable.Where(p => p.PostDate != null).ToArray(); - var result1 = queryable.Where("PostDate != null").ToArray(); - var result2 = queryable.Where("null != PostDate").ToArray(); + var expected = queryable.Where(p => p.CloseDate != null).ToArray(); + var result1 = queryable.Where("CloseDate != null").ToArray(); + var result2 = queryable.Where("null != CloseDate").ToArray(); // Assert Assert.Equal(expected, result1); @@ -98,9 +98,9 @@ public void Where_Dynamic_DateTime_Equals_Null() IQueryable queryable = new[] { new Post() }.AsQueryable(); // Act - var expected = queryable.Where(p => p.PostDate == null).ToArray(); - var result1 = queryable.Where("PostDate == null").ToArray(); - var result2 = queryable.Where("null == PostDate").ToArray(); + var expected = queryable.Where(p => p.CloseDate == null).ToArray(); + var result1 = queryable.Where("CloseDate == null").ToArray(); + var result2 = queryable.Where("null == CloseDate").ToArray(); // Assert Assert.Equal(expected, result1); From c6321e6511bd10a85a0f23aa60bd42f677126775 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 25 Dec 2024 11:31:10 +0100 Subject: [PATCH 2/5] . --- src/System.Linq.Dynamic.Core/ParsingConfig.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/System.Linq.Dynamic.Core/ParsingConfig.cs b/src/System.Linq.Dynamic.Core/ParsingConfig.cs index 922e4dd25..c57e58543 100644 --- a/src/System.Linq.Dynamic.Core/ParsingConfig.cs +++ b/src/System.Linq.Dynamic.Core/ParsingConfig.cs @@ -44,7 +44,6 @@ public class ParsingConfig /// - (extension) methods /// - constant expressions ("null", "true", "false") /// - keywords ("it", "parent", "root") - /// - symbols ("$", "^", "~") /// - functions ("as", "cast", "iif", "is", "isnull", "new", "np") /// /// Default value is false. From 92a30c5e0d2ab6184563313713c958813b87969e Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Wed, 25 Dec 2024 13:30:04 +0100 Subject: [PATCH 3/5] operator aliases --- src/System.Linq.Dynamic.Core/ParsingConfig.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/System.Linq.Dynamic.Core/ParsingConfig.cs b/src/System.Linq.Dynamic.Core/ParsingConfig.cs index c57e58543..b9a35b240 100644 --- a/src/System.Linq.Dynamic.Core/ParsingConfig.cs +++ b/src/System.Linq.Dynamic.Core/ParsingConfig.cs @@ -45,6 +45,7 @@ public class ParsingConfig /// - constant expressions ("null", "true", "false") /// - keywords ("it", "parent", "root") /// - functions ("as", "cast", "iif", "is", "isnull", "new", "np") + /// - operator aliases ("eq", "equal", "ne", "notequal", "neq", "lt", "LessThan", "le", "LessThanEqual", "gt", "GreaterThan", "ge", "GreaterThanEqual", "and", "AndAlso", "or", "OrElse", "not", "mod") /// /// Default value is false. /// From 922e8e900d5c9c50a934407272774dd3a9be744b Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Fri, 27 Dec 2024 10:11:13 +0100 Subject: [PATCH 4/5] . --- src/System.Linq.Dynamic.Core/ParsingConfig.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/System.Linq.Dynamic.Core/ParsingConfig.cs b/src/System.Linq.Dynamic.Core/ParsingConfig.cs index b9a35b240..70acd35cc 100644 --- a/src/System.Linq.Dynamic.Core/ParsingConfig.cs +++ b/src/System.Linq.Dynamic.Core/ParsingConfig.cs @@ -292,5 +292,5 @@ public IQueryableAnalyzer QueryableAnalyzer /// /// Default value is false. /// - public bool RestrictOrderByToPropertyOrField { get; set; } = false; + public bool RestrictOrderByToPropertyOrField { get; set; } } \ No newline at end of file From 782bbb5826d11ae3dda323e5c81de53c2c171242 Mon Sep 17 00:00:00 2001 From: Stef Heyenrath Date: Sun, 19 Jan 2025 08:20:46 +0100 Subject: [PATCH 5/5] . --- .../Extensions/ListExtensions.cs | 14 ++ .../Parser/ExpressionParser.cs | 26 +-- .../Parser/ExpressionPromoter.cs | 187 +++++++++--------- .../Parser/IExpressionPromoter.cs | 29 ++- .../Parser/ExpressionPromoterTests.cs | 83 ++++---- 5 files changed, 176 insertions(+), 163 deletions(-) create mode 100644 src/System.Linq.Dynamic.Core/Extensions/ListExtensions.cs diff --git a/src/System.Linq.Dynamic.Core/Extensions/ListExtensions.cs b/src/System.Linq.Dynamic.Core/Extensions/ListExtensions.cs new file mode 100644 index 000000000..8885fc4e5 --- /dev/null +++ b/src/System.Linq.Dynamic.Core/Extensions/ListExtensions.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; + +namespace System.Linq.Dynamic.Core.Extensions; + +internal static class ListExtensions +{ + internal static void AddIfNotNull(this IList list, T? value) + { + if (value != null) + { + list.Add(value); + } + } +} \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index 528dc5365..1dddb75e0 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -1494,7 +1494,10 @@ private Expression CreateArrayInitializerExpression(List expressions if (newType != null) { - return Expression.NewArrayInit(newType, expressions.Select(expression => _parsingConfig.ExpressionPromoter.Promote(expression, newType, true, true))); + var promotedExpressions = expressions + .Select(expression => _parsingConfig.ExpressionPromoter.Promote(expression, newType, true, true)) + .OfType(); + return Expression.NewArrayInit(newType, promotedExpressions); } return Expression.NewArrayInit(expressions.All(expression => expression.Type == expressions[0].Type) ? expressions[0].Type : typeof(object), expressions); @@ -1551,24 +1554,24 @@ private Expression CreateNewExpression(List properties, List constructorParameters .Any(cp => cp.Name == p.Name && (cp.ParameterType == p.Type || p.Type == Nullable.GetUnderlyingType(cp.ParameterType)))); - var expressionsPromoted = new List(); + var expressionsPromoted = new List(); // Loop all expressions and promote if needed for (int i = 0; i < constructorParameters.Length; i++) { if (bindParametersSequentially) { - expressionsPromoted.Add(_parsingConfig.ExpressionPromoter.Promote(expressions[i], propertyTypes[i], true, true)); + expressionsPromoted.AddIfNotNull(_parsingConfig.ExpressionPromoter.Promote(expressions[i], propertyTypes[i], true, true)); } else { - Type propertyType = constructorParameters[i].ParameterType; + var propertyType = constructorParameters[i].ParameterType; var cParameterName = constructorParameters[i].Name; var propertyAndIndex = properties.Select((p, index) => new { p, index }) .First(p => p.p.Name == cParameterName && (p.p.Type == propertyType || p.p.Type == Nullable.GetUnderlyingType(propertyType))); - + // Promote from Type to Nullable Type if needed - expressionsPromoted.Add(_parsingConfig.ExpressionPromoter.Promote(expressions[propertyAndIndex.index], propertyType, true, true)); + expressionsPromoted.AddIfNotNull(_parsingConfig.ExpressionPromoter.Promote(expressions[propertyAndIndex.index], propertyType, true, true)); } } @@ -1584,6 +1587,7 @@ private Expression CreateNewExpression(List properties, List _parsingConfig.ExpressionPromoter.Promote(expressions[i], t.ParameterType, true, true)) + .OfType() .ToArray(); return Expression.New(exactConstructor, expressionsPromoted); @@ -1661,14 +1665,14 @@ private Expression ParseTypeAccess(Type type, bool getNext) } // This is a shorthand for explicitly converting a string to something - bool shorthand = _textParser.CurrentToken.Id == TokenId.StringLiteral; - if (_textParser.CurrentToken.Id == TokenId.OpenParen || shorthand) + var isShorthand = _textParser.CurrentToken.Id == TokenId.StringLiteral; + if (_textParser.CurrentToken.Id == TokenId.OpenParen || isShorthand) { Expression[] args; - if (shorthand) + if (isShorthand) { var expressionOrType = ParseStringLiteral(true); - args = new[] { expressionOrType.First }; + args = [expressionOrType.First]; } else { @@ -1685,7 +1689,7 @@ private Expression ParseTypeAccess(Type type, bool getNext) // ReSharper disable once ConditionIsAlwaysTrueOrFalseAccordingToNullableAPIContract if (args.Length == 1 && (args[0] == null || args[0] is ConstantExpression) && TryGenerateConversion(args[0], type, out var generatedExpression)) { - return generatedExpression!; + return generatedExpression; } // If only 1 argument, and if the type is a ValueType and argType is also a ValueType, just Convert diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs index edcd48b57..43f7acbff 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionPromoter.cs @@ -1,136 +1,135 @@ using System.Linq.Expressions; using System.Reflection; -namespace System.Linq.Dynamic.Core.Parser +namespace System.Linq.Dynamic.Core.Parser; + +/// +/// ExpressionPromoter +/// +public class ExpressionPromoter : IExpressionPromoter { + private readonly NumberParser _numberParser; + private readonly ConstantExpressionHelper _constantExpressionHelper; + /// - /// ExpressionPromoter + /// Initializes a new instance of the class. /// - public class ExpressionPromoter : IExpressionPromoter + /// The ParsingConfig. + public ExpressionPromoter(ParsingConfig config) { - private readonly NumberParser _numberParser; - private readonly ConstantExpressionHelper _constantExpressionHelper; + _numberParser = new NumberParser(config); + _constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(config); + } - /// - /// Initializes a new instance of the class. - /// - /// The ParsingConfig. - public ExpressionPromoter(ParsingConfig config) + /// + public virtual Expression? Promote(Expression sourceExpression, Type type, bool exact, bool convertExpression) + { + Type returnType; + if (sourceExpression is LambdaExpression lambdaExpression) { - _numberParser = new NumberParser(config); - _constantExpressionHelper = ConstantExpressionHelperFactory.GetInstance(config); + returnType = lambdaExpression.GetReturnType(); } - - /// - public virtual Expression? Promote(Expression expr, Type type, bool exact, bool convertExpr) + else { - Type returnType; - if (expr is LambdaExpression lambdaExpression) - { - returnType = lambdaExpression.GetReturnType(); - } - else - { - returnType = expr.Type; - } + returnType = sourceExpression.Type; + } - if (returnType == type || type.IsGenericParameter) - { - return expr; - } + if (returnType == type || type.IsGenericParameter) + { + return sourceExpression; + } - if (expr is ConstantExpression ce) + if (sourceExpression is ConstantExpression ce) + { + if (Constants.IsNull(ce)) { - if (Constants.IsNull(ce)) + if (!type.GetTypeInfo().IsValueType || TypeHelper.IsNullableType(type)) { - if (!type.GetTypeInfo().IsValueType || TypeHelper.IsNullableType(type)) - { - return Expression.Constant(null, type); - } + return Expression.Constant(null, type); } - else + } + else + { + if (_constantExpressionHelper.TryGetText(ce, out var text)) { - if (_constantExpressionHelper.TryGetText(ce, out var text)) - { - Type target = TypeHelper.GetNonNullableType(type); - object? value = null; + Type target = TypeHelper.GetNonNullableType(type); + object? value = null; #if !(UAP10_0 || NETSTANDARD) - switch (Type.GetTypeCode(ce.Type)) - { - case TypeCode.Int32: - case TypeCode.UInt32: - case TypeCode.Int64: - case TypeCode.UInt64: - value = _numberParser.ParseNumber(text, target); - - // Make sure an enum value stays an enum value - if (target.IsEnum) - { - value = Enum.ToObject(target, value!); - } - break; - - case TypeCode.Double: - if (target == typeof(decimal) || target == typeof(double)) - { - value = _numberParser.ParseNumber(text, target); - } - break; + switch (Type.GetTypeCode(ce.Type)) + { + case TypeCode.Int32: + case TypeCode.UInt32: + case TypeCode.Int64: + case TypeCode.UInt64: + value = _numberParser.ParseNumber(text, target); - case TypeCode.String: - TypeHelper.TryParseEnum(text, target, out value); - break; - } -#else - if (ce.Type == typeof(int) || ce.Type == typeof(uint) || ce.Type == typeof(long) || ce.Type == typeof(ulong)) - { - // If target is an enum value, just use the Value from the ConstantExpression - if (target.GetTypeInfo().IsEnum) - { - value = Enum.ToObject(target, ce.Value); - } - else + // Make sure an enum value stays an enum value + if (target.IsEnum) { - value = _numberParser.ParseNumber(text!, target); + value = Enum.ToObject(target, value!); } - } - else if (ce.Type == typeof(double)) - { + break; + + case TypeCode.Double: if (target == typeof(decimal) || target == typeof(double)) { value = _numberParser.ParseNumber(text, target); } + break; + + case TypeCode.String: + TypeHelper.TryParseEnum(text, target, out value); + break; + } +#else + if (ce.Type == typeof(int) || ce.Type == typeof(uint) || ce.Type == typeof(long) || ce.Type == typeof(ulong)) + { + // If target is an enum value, just use the Value from the ConstantExpression + if (target.GetTypeInfo().IsEnum) + { + value = Enum.ToObject(target, ce.Value); } - else if (ce.Type == typeof(string) && TypeHelper.TryParseEnum(text, target, out value)) + else { - // Empty if + value = _numberParser.ParseNumber(text!, target); } -#endif - if (value != null) + } + else if (ce.Type == typeof(double)) + { + if (target == typeof(decimal) || target == typeof(double)) { - return Expression.Constant(value, type); + value = _numberParser.ParseNumber(text, target); } } + else if (ce.Type == typeof(string) && TypeHelper.TryParseEnum(text, target, out value)) + { + // Empty if + } +#endif + if (value != null) + { + return Expression.Constant(value, type); + } } } + } - if (TypeHelper.IsCompatibleWith(returnType, type)) + if (TypeHelper.IsCompatibleWith(returnType, type)) + { + if (type == typeof(decimal) && TypeHelper.IsEnumType(sourceExpression.Type)) { - if (type == typeof(decimal) && TypeHelper.IsEnumType(expr.Type)) - { - return Expression.Convert(Expression.Convert(expr, Enum.GetUnderlyingType(expr.Type)), type); - } - - if (type.GetTypeInfo().IsValueType || exact || expr.Type.GetTypeInfo().IsValueType && convertExpr) - { - return Expression.Convert(expr, type); - } + return Expression.Convert(Expression.Convert(sourceExpression, Enum.GetUnderlyingType(sourceExpression.Type)), type); + } - return expr; + if (type.GetTypeInfo().IsValueType || exact || sourceExpression.Type.GetTypeInfo().IsValueType && convertExpression) + { + return Expression.Convert(sourceExpression, type); } - return null; + return sourceExpression; } + + return null; } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Parser/IExpressionPromoter.cs b/src/System.Linq.Dynamic.Core/Parser/IExpressionPromoter.cs index 29d8ba7fd..cf100c98c 100644 --- a/src/System.Linq.Dynamic.Core/Parser/IExpressionPromoter.cs +++ b/src/System.Linq.Dynamic.Core/Parser/IExpressionPromoter.cs @@ -1,22 +1,19 @@ using System.Linq.Expressions; -namespace System.Linq.Dynamic.Core.Parser +namespace System.Linq.Dynamic.Core.Parser; + +/// +/// Expression promoter is used to promote object or value types to their destination type when an automatic promotion is available such as: int to int?. +/// +public interface IExpressionPromoter { /// - /// Expression promoter is used to promote object or value types - /// to their destination type when an automatic promotion is available - /// such as: int to int? + /// Promote an expression. /// - public interface IExpressionPromoter - { - /// - /// Promote an expression - /// - /// Source expression - /// Destination data type to promote - /// If the match must be exact - /// Convert expression - /// The promoted or null. - Expression? Promote(Expression expr, Type type, bool exact, bool convertExpr); - } + /// Source expression + /// Destination data type to promote + /// If the match must be exact + /// Convert expression + /// The promoted or null. + Expression? Promote(Expression sourceExpression, Type type, bool exact, bool convertExpression); } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionPromoterTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionPromoterTests.cs index 6178deeb2..f500a5b87 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionPromoterTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionPromoterTests.cs @@ -5,53 +5,52 @@ using System.Linq.Expressions; using Xunit; -namespace System.Linq.Dynamic.Core.Tests.Parser +namespace System.Linq.Dynamic.Core.Tests.Parser; + +public class ExpressionPromoterTests { - public class ExpressionPromoterTests + public class SampleDto { - public class SampleDto - { - public Guid data { get; set; } - } + public Guid Data { get; set; } + } - private readonly Mock _expressionPromoterMock; - private readonly Mock _dynamicLinkCustomTypeProviderMock; + private readonly Mock _expressionPromoterMock; + private readonly Mock _dynamicLinkCustomTypeProviderMock; - public ExpressionPromoterTests() - { - _dynamicLinkCustomTypeProviderMock = new Mock(); - _dynamicLinkCustomTypeProviderMock.Setup(d => d.GetCustomTypes()).Returns(new HashSet()); - _dynamicLinkCustomTypeProviderMock.Setup(d => d.ResolveType(It.IsAny())).Returns(typeof(SampleDto)); + public ExpressionPromoterTests() + { + _dynamicLinkCustomTypeProviderMock = new Mock(); + _dynamicLinkCustomTypeProviderMock.Setup(d => d.GetCustomTypes()).Returns(new HashSet()); + _dynamicLinkCustomTypeProviderMock.Setup(d => d.ResolveType(It.IsAny())).Returns(typeof(SampleDto)); - _expressionPromoterMock = new Mock(); - _expressionPromoterMock.Setup(e => e.Promote(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Expression.Constant(Guid.NewGuid())); - } + _expressionPromoterMock = new Mock(); + _expressionPromoterMock.Setup(e => e.Promote(It.IsAny(), It.IsAny(), It.IsAny(), It.IsAny())).Returns(Expression.Constant(Guid.NewGuid())); + } - [Fact] - public void DynamicExpressionParser_ParseLambda_WithCustomExpressionPromoter() + [Fact] + public void DynamicExpressionParser_ParseLambda_WithCustomExpressionPromoter() + { + // Assign + var parsingConfig = new ParsingConfig() { - // Assign - var parsingConfig = new ParsingConfig() - { - AllowNewToEvaluateAnyType = true, - CustomTypeProvider = _dynamicLinkCustomTypeProviderMock.Object, - ExpressionPromoter = _expressionPromoterMock.Object - }; - - // Act - string query = $"new {typeof(SampleDto).FullName}(@0 as data)"; - LambdaExpression expression = DynamicExpressionParser.ParseLambda(parsingConfig, null, query, new object[] { Guid.NewGuid().ToString() }); - Delegate del = expression.Compile(); - SampleDto result = (SampleDto)del.DynamicInvoke(); - - // Assert - Assert.NotNull(result); - - // Verify - _dynamicLinkCustomTypeProviderMock.Verify(d => d.GetCustomTypes(), Times.Once); - _dynamicLinkCustomTypeProviderMock.Verify(d => d.ResolveType($"{typeof(SampleDto).FullName}"), Times.Once); - - _expressionPromoterMock.Verify(e => e.Promote(It.IsAny(), typeof(Guid), true, true), Times.Once); - } + AllowNewToEvaluateAnyType = true, + CustomTypeProvider = _dynamicLinkCustomTypeProviderMock.Object, + ExpressionPromoter = _expressionPromoterMock.Object + }; + + // Act + string query = $"new {typeof(SampleDto).FullName}(@0 as Data)"; + LambdaExpression expression = DynamicExpressionParser.ParseLambda(parsingConfig, null, query, Guid.NewGuid().ToString()); + Delegate del = expression.Compile(); + SampleDto result = (SampleDto)del.DynamicInvoke(); + + // Assert + Assert.NotNull(result); + + // Verify + _dynamicLinkCustomTypeProviderMock.Verify(d => d.GetCustomTypes(), Times.Once); + _dynamicLinkCustomTypeProviderMock.Verify(d => d.ResolveType($"{typeof(SampleDto).FullName}"), Times.Once); + + _expressionPromoterMock.Verify(e => e.Promote(It.IsAny(), typeof(Guid), true, true), Times.Once); } -} +} \ No newline at end of file