diff --git a/src/System.Linq.Dynamic.Core/CustomTypeProviders/DefaultDynamicLinqCustomTypeProvider.cs b/src/System.Linq.Dynamic.Core/CustomTypeProviders/DefaultDynamicLinqCustomTypeProvider.cs index 78c8fea3..b312e729 100644 --- a/src/System.Linq.Dynamic.Core/CustomTypeProviders/DefaultDynamicLinqCustomTypeProvider.cs +++ b/src/System.Linq.Dynamic.Core/CustomTypeProviders/DefaultDynamicLinqCustomTypeProvider.cs @@ -31,7 +31,7 @@ public DefaultDynamicLinqCustomTypeProvider(bool cacheCustomTypes = true) _cacheCustomTypes = cacheCustomTypes; } - /// + /// public virtual HashSet GetCustomTypes() { if (_cacheCustomTypes) @@ -47,7 +47,7 @@ public virtual HashSet GetCustomTypes() return GetCustomTypesInternal(); } - /// + /// public Dictionary> GetExtensionMethods() { if (_cacheCustomTypes) @@ -63,7 +63,7 @@ public Dictionary> GetExtensionMethods() return GetExtensionMethodsInternal(); } - /// + /// public Type ResolveType(string typeName) { Check.NotEmpty(typeName, nameof(typeName)); @@ -72,7 +72,7 @@ public Type ResolveType(string typeName) return ResolveType(assemblies, typeName); } - /// + /// public Type ResolveTypeBySimpleName(string simpleTypeName) { Check.NotEmpty(simpleTypeName, nameof(simpleTypeName)); @@ -91,7 +91,7 @@ private Dictionary> GetExtensionMethodsInternal() { var types = GetCustomTypes(); - List> list= new List>(); + List> list = new List>(); foreach (var type in types) { diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index d66abc0b..d6d6751f 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -1045,6 +1045,42 @@ Expression ParseIdentifier() return expr; } + //// This could be enum like "MyEnum.Value1" + //if (_textParser.CurrentToken.Id == TokenId.Identifier) + //{ + // var parts = new List { _textParser.CurrentToken.Text }; + + // _textParser.NextToken(); + // _textParser.ValidateToken(TokenId.Dot, Res.DotExpected); + // while (_textParser.CurrentToken.Id == TokenId.Dot || _textParser.CurrentToken.Id == TokenId.Plus) + // { + // parts.Add(_textParser.CurrentToken.Text); + + // _textParser.NextToken(); + // _textParser.ValidateToken(TokenId.Identifier, Res.IdentifierExpected); + + // parts.Add(_textParser.CurrentToken.Text); + + // _textParser.NextToken(); + // } + + // var enumTypeAsString = string.Join("", parts.Take(parts.Count - 2).ToArray()); + // var enumType = _typeFinder.FindTypeByName(enumTypeAsString, null, true); + // if (enumType == null) + // { + // throw ParseError(_textParser.CurrentToken.Pos, Res.EnumTypeNotFound, enumTypeAsString); + // } + + // string enumValue = parts.Last(); + // var @enum = TypeHelper.ParseEnum(enumValue, enumType); + // if (@enum == null) + // { + // throw ParseError(_textParser.CurrentToken.Pos, Res.EnumValueNotDefined, enumValue, enumTypeAsString); + // } + + // return Expression.Constant(@enum); + //} + if (_it != null) { return ParseMemberAccess(null, _it); @@ -1682,10 +1718,9 @@ Expression ParseMemberAccess(Type type, Expression expression) } } - if (type.GetTypeInfo().IsEnum) + var @enum = TypeHelper.ParseEnum(id, type); + if (@enum != null) { - var @enum = Enum.Parse(type, id, true); - return Expression.Constant(@enum); } @@ -1722,33 +1757,85 @@ Expression ParseMemberAccess(Type type, Expression expression) return _expressionHelper.ConvertToExpandoObjectAndCreateDynamicExpression(expression, type, id); } #endif - + // Parse as Lambda if (_textParser.CurrentToken.Id == TokenId.Lambda && _it.Type == type) { - // This might be an internal variable for use within a lambda expression, so store it as such - _internals.Add(id, _it); - string _previousItName = ItName; + return ParseAsLambda(id); + } + + // This could be enum like "A.B.C.MyEnum.Value1" or "A.B.C+MyEnum.Value1" + if (_textParser.CurrentToken.Id == TokenId.Dot || _textParser.CurrentToken.Id == TokenId.Plus) + { + return ParseAsEnum(id); + } + + throw ParseError(errorPos, Res.UnknownPropertyOrField, id, TypeHelper.GetTypeName(type)); + } + + private Expression ParseAsLambda(string id) + { + // This might be an internal variable for use within a lambda expression, so store it as such + _internals.Add(id, _it); + string _previousItName = ItName; + + // Also store ItName (only once) + if (string.Equals(ItName, KeywordsHelper.KEYWORD_IT)) + { + ItName = id; + } + + // next + _textParser.NextToken(); + + LastLambdaItName = ItName; + var exp = ParseConditionalOperator(); - // Also store ItName (only once) - if (string.Equals(ItName, KeywordsHelper.KEYWORD_IT)) + // Restore previous context and clear internals + _internals.Remove(id); + ItName = _previousItName; + + return exp; + } + + private Expression ParseAsEnum(string id) + { + var parts = new List { id }; + + while (_textParser.CurrentToken.Id == TokenId.Dot || _textParser.CurrentToken.Id == TokenId.Plus) + { + if (_textParser.CurrentToken.Id == TokenId.Dot || _textParser.CurrentToken.Id == TokenId.Plus) { - ItName = id; + parts.Add(_textParser.CurrentToken.Text); + _textParser.NextToken(); } - // next - _textParser.NextToken(); + if (_textParser.CurrentToken.Id == TokenId.Identifier) + { + parts.Add(_textParser.CurrentToken.Text); + _textParser.NextToken(); + } + } - LastLambdaItName = ItName; - var exp = ParseConditionalOperator(); + var enumTypeAsString = string.Join("", parts.Take(parts.Count - 2).ToArray()); + var enumType = _typeFinder.FindTypeByName(enumTypeAsString, null, true); + if (enumType == null) + { + throw ParseError(_textParser.CurrentToken.Pos, Res.EnumTypeNotFound, enumTypeAsString); + } - // Restore previous context and clear internals - _internals.Remove(id); - ItName = _previousItName; + string enumValueAsString = parts.LastOrDefault(); + if (enumValueAsString == null) + { + throw ParseError(_textParser.CurrentToken.Pos, Res.EnumValueExpected); + } - return exp; + var enumValue = TypeHelper.ParseEnum(enumValueAsString, enumType); + if (enumValue == null) + { + throw ParseError(_textParser.CurrentToken.Pos, Res.EnumValueNotDefined, enumValueAsString, enumTypeAsString); } - throw ParseError(errorPos, Res.UnknownPropertyOrField, id, TypeHelper.GetTypeName(type)); + return Expression.Constant(enumValue); } Expression ParseEnumerable(Expression instance, Type elementType, string methodName, int errorPos, Type type) diff --git a/src/System.Linq.Dynamic.Core/Res.cs b/src/System.Linq.Dynamic.Core/Res.cs index 63a66b91..d6b0e377 100644 --- a/src/System.Linq.Dynamic.Core/Res.cs +++ b/src/System.Linq.Dynamic.Core/Res.cs @@ -20,6 +20,9 @@ internal static class Res 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'"; diff --git a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs index 9ffe41b0..ecbd1089 100644 --- a/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/ExpressionTests.cs @@ -1,16 +1,26 @@ -using FluentAssertions; -using Newtonsoft.Json.Linq; -using NFluent; -using System.Collections.Generic; +using System.Collections.Generic; using System.Dynamic; using System.Globalization; using System.Linq.Dynamic.Core.Exceptions; using System.Linq.Dynamic.Core.Tests.Helpers; using System.Linq.Dynamic.Core.Tests.Helpers.Models; +using FluentAssertions; +using Newtonsoft.Json.Linq; +using NFluent; using Xunit; namespace System.Linq.Dynamic.Core.Tests { + public enum TestEnumPublic : sbyte + { + Var1 = 0, + Var2 = 1, + Var3 = 2, + Var4 = 4, + Var5 = 8, + Var6 = 16 + } + public class ExpressionTests { public enum TestEnum2 : sbyte @@ -20,7 +30,7 @@ public enum TestEnum2 : sbyte Var3 = 2, Var4 = 4, Var5 = 8, - Var6 = 16, + Var6 = 16 } public class TestEnumClass @@ -31,6 +41,8 @@ public class TestEnumClass public TestEnum2? C { get; set; } + public TestEnumPublic E { get; set; } + public int Id { get; set; } } @@ -677,7 +689,7 @@ public void ExpressionTests_Enum() } [Fact] - public void ExpressionTests_Enum_Property() + public void ExpressionTests_Enum_Property_Equality_Using_Argument() { // Arrange var qry = new List { new TestEnumClass { B = TestEnum2.Var2 } }.AsQueryable(); @@ -697,6 +709,129 @@ public void ExpressionTests_Enum_Property() Check.That(resultEqualIntParamRight.Single()).Equals(TestEnum2.Var2); } + [Fact] + public void ExpressionTests_Enum_Property_Equality_With_Integer_Inline() + { + // Arrange + var qry = new List { new TestEnumClass { E = TestEnumPublic.Var2 } }.AsQueryable(); + + // Act + var resultEqualIntParamLeft = qry.Where("1 == it.E").ToDynamicArray(); + var resultEqualIntParamRight = qry.Where("it.E == 1").ToDynamicArray(); + + // Assert + Check.That(resultEqualIntParamLeft.Single()).Equals(TestEnumPublic.Var2); + Check.That(resultEqualIntParamRight.Single()).Equals(TestEnumPublic.Var2); + } + + [Fact] + public void ExpressionTests_Enum_Property_Equality_Using_PublicEnum_And_FullName_Inline() + { + // Arrange + var qry = new List { new TestEnumClass { E = TestEnumPublic.Var2 } }.AsQueryable(); + string enumType = typeof(TestEnumPublic).FullName; + + // Act + var resultEqualEnumParamLeft = qry.Where($"{enumType}.Var2 == it.E").ToDynamicArray(); + var resultEqualEnumParamRight = qry.Where($"it.E == {enumType}.Var2").ToDynamicArray(); + + // Assert + Check.That(resultEqualEnumParamLeft.Single()).Equals(TestEnumPublic.Var2); + Check.That(resultEqualEnumParamRight.Single()).Equals(TestEnumPublic.Var2); + } + + [Fact] + public void ExpressionTests_Enum_Property_Equality_Using_Enum_And_FullName_Inline() + { + // Arrange + var qry = new List { new TestEnumClass { B = TestEnum2.Var2 } }.AsQueryable(); + string enumType = typeof(TestEnum2).FullName; + + // Act + var resultEqualEnumParamLeft = qry.Where($"{enumType}.Var2 == it.B").ToDynamicArray(); + var resultEqualEnumParamRight = qry.Where($"it.B == {enumType}.Var2").ToDynamicArray(); + + // Assert + Check.That(resultEqualEnumParamLeft.Single()).Equals(TestEnum2.Var2); + Check.That(resultEqualEnumParamRight.Single()).Equals(TestEnum2.Var2); + } + + [Fact] + public void ExpressionTests_Enum_Property_Equality_Using_PublicEnum_Name_Inline() + { + // Arrange + var qry = new List { new TestEnumClass { E = TestEnumPublic.Var2 } }.AsQueryable(); + string enumType = typeof(TestEnumPublic).FullName; + + // Act + var resultEqualEnumParamLeft = qry.Where($"{enumType}.Var2 == it.E").ToDynamicArray(); + var resultEqualEnumParamRight = qry.Where($"it.E == {enumType}.Var2").ToDynamicArray(); + + // Assert + Check.That(resultEqualEnumParamLeft.Single()).Equals(TestEnumPublic.Var2); + Check.That(resultEqualEnumParamRight.Single()).Equals(TestEnumPublic.Var2); + } + + [Fact] + public void ExpressionTests_Enum_Property_Equality_Using_Enum_Name_Inline_Should_Throw_Exception() + { + // Arrange + var config = new ParsingConfig + { + ResolveTypesBySimpleName = true + }; + var qry = new List { new TestEnumClass { B = TestEnum2.Var2 } }.AsQueryable(); + string enumType = typeof(TestEnum2).Name; + + // Act + Action a = () => qry.Where(config, $"{enumType}.Var2 == it.B").ToDynamicArray(); + + // Assert + a.Should().Throw(); + } + + [Fact] + public void ExpressionTests_Enum_Property_Equality_Using_PublicEnum_Invalid_Inline_Should_Throw_ParseException() + { + // Arrange + var qry = new List { new TestEnumClass { E = TestEnumPublic.Var2 } }.AsQueryable(); + string enumType = "a.b.c"; + + // Act + Action a = () => qry.Where($"{enumType}.Var2 == it.E").ToDynamicArray(); + + // Assert + a.Should().Throw().WithMessage("Enum type 'b.c' not found"); + } + + [Fact] + public void ExpressionTests_Enum_Property_Equality_Using_PublicEnum_InvalidValue_Inline_Should_Throw_ParseException() + { + // Arrange + var qry = new List { new TestEnumClass { E = TestEnumPublic.Var2 } }.AsQueryable(); + string enumType = typeof(TestEnumPublic).FullName; + + // Act + Action a = () => qry.Where($"{enumType}.VarInvalid == it.E").ToDynamicArray(); + + // Assert + a.Should().Throw().WithMessage("Enum value 'VarInvalid' is not defined in enum type 'System.Linq.Dynamic.Core.Tests.TestEnumPublic'"); + } + + [Fact] + public void ExpressionTests_Enum_Property_Equality_Using_PublicEnum_MissingValue_Inline_Should_Throw_ParseException() + { + // Arrange + var qry = new List { new TestEnumClass { E = TestEnumPublic.Var2 } }.AsQueryable(); + string enumType = typeof(TestEnumPublic).FullName; + + // Act + Action a = () => qry.Where($"{enumType}. == it.E").ToDynamicArray(); + + // Assert + a.Should().Throw().WithMessage("Enum type 'System.Linq.Dynamic.Core.Tests.' not found"); + } + [Fact] public void ExpressionTests_Enum_NullableProperty() {