diff --git a/src-blazor/BlazorAppServer/BlazorAppServer.csproj b/src-blazor/BlazorAppServer/BlazorAppServer.csproj index 298131d6..02873fea 100644 --- a/src-blazor/BlazorAppServer/BlazorAppServer.csproj +++ b/src-blazor/BlazorAppServer/BlazorAppServer.csproj @@ -17,9 +17,7 @@ - - diff --git a/src/System.Linq.Dynamic.Core/Compatibility/TypeExtensions.cs b/src/System.Linq.Dynamic.Core/Compatibility/TypeExtensions.cs index 5cc01f02..b7ce2fbc 100644 --- a/src/System.Linq.Dynamic.Core/Compatibility/TypeExtensions.cs +++ b/src/System.Linq.Dynamic.Core/Compatibility/TypeExtensions.cs @@ -1,4 +1,4 @@ -#if NETSTANDARD1_3 +#if NETSTANDARD1_3 || UAP10_0 using System.Linq; namespace System.Reflection; diff --git a/src/System.Linq.Dynamic.Core/DynamicClassFactory.cs b/src/System.Linq.Dynamic.Core/DynamicClassFactory.cs index 0dc61889..75264a4e 100644 --- a/src/System.Linq.Dynamic.Core/DynamicClassFactory.cs +++ b/src/System.Linq.Dynamic.Core/DynamicClassFactory.cs @@ -1,8 +1,9 @@ -#if !(UAP10_0) +#if !UAP10_0 using System.Collections; using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; +using System.Linq.Dynamic.Core.Parser; using System.Linq.Dynamic.Core.Validation; using System.Reflection; using System.Reflection.Emit; @@ -27,12 +28,6 @@ public static class DynamicClassFactory private static readonly ConstructorInfo ObjectCtor = typeof(object).GetConstructor(Type.EmptyTypes)!; -#if UAP10_0 || NETSTANDARD - private static readonly MethodInfo ObjectToString = typeof(object).GetMethod(nameof(ToString), BindingFlags.Instance | BindingFlags.Public)!; -#else - private static readonly MethodInfo ObjectToString = typeof(object).GetMethod(nameof(ToString), BindingFlags.Instance | BindingFlags.Public, null, Type.EmptyTypes, null)!; -#endif - private static readonly ConstructorInfo StringBuilderCtor = typeof(StringBuilder).GetConstructor(Type.EmptyTypes)!; #if UAP10_0 || NETSTANDARD private static readonly MethodInfo StringBuilderAppendString = typeof(StringBuilder).GetMethod(nameof(StringBuilder.Append), [typeof(string)])!; @@ -419,7 +414,7 @@ private static Type EmitType(IList properties, bool createParam ilgeneratorToString.Emit(OpCodes.Callvirt, StringBuilderAppendString); ilgeneratorToString.Emit(OpCodes.Pop); ilgeneratorToString.Emit(OpCodes.Ldloc_0); - ilgeneratorToString.Emit(OpCodes.Callvirt, ObjectToString); + ilgeneratorToString.Emit(OpCodes.Callvirt, PredefinedMethodsHelper.ObjectToString); ilgeneratorToString.Emit(OpCodes.Ret); EmitEqualityOperators(typeBuilder, equals); diff --git a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs index eb8e52f5..b298848f 100644 --- a/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs +++ b/src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs @@ -1840,9 +1840,9 @@ private Expression ParseMemberAccess(Type? type, Expression? expression, string? case 1: var method = (MethodInfo)methodBase!; - if (!PredefinedTypesHelper.IsPredefinedType(_parsingConfig, method.DeclaringType!)) + if (!PredefinedTypesHelper.IsPredefinedType(_parsingConfig, method.DeclaringType!) && !PredefinedMethodsHelper.IsPredefinedMethod(_parsingConfig, method)) { - throw ParseError(errorPos, Res.MethodsAreInaccessible, TypeHelper.GetTypeName(method.DeclaringType!)); + throw ParseError(errorPos, Res.MethodIsInaccessible, id, TypeHelper.GetTypeName(method.DeclaringType!)); } MethodInfo methodToCall; diff --git a/src/System.Linq.Dynamic.Core/Parser/PredefinedMethodsHelper.cs b/src/System.Linq.Dynamic.Core/Parser/PredefinedMethodsHelper.cs new file mode 100644 index 00000000..a3b6d85e --- /dev/null +++ b/src/System.Linq.Dynamic.Core/Parser/PredefinedMethodsHelper.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Linq.Dynamic.Core.Validation; +using System.Reflection; + +namespace System.Linq.Dynamic.Core.Parser; + +internal static class PredefinedMethodsHelper +{ + internal static readonly MethodInfo ObjectToString = typeof(object).GetMethod(nameof(ToString), BindingFlags.Instance | BindingFlags.Public, null, Type.EmptyTypes, null)!; + internal static readonly MethodInfo ObjectInstanceEquals = typeof(object).GetMethod(nameof(Equals), BindingFlags.Instance | BindingFlags.Public, null, [typeof(object)], null)!; + internal static readonly MethodInfo ObjectStaticEquals = typeof(object).GetMethod(nameof(Equals), BindingFlags.Static | BindingFlags.Public, null, [typeof(object), typeof(object)], null)!; + internal static readonly MethodInfo ObjectStaticReferenceEquals = typeof(object).GetMethod(nameof(ReferenceEquals), BindingFlags.Static | BindingFlags.Public, null, [typeof(object), typeof(object)], null)!; + + + private static readonly HashSet ObjectToStringAndObjectEquals = + [ + ObjectToString, + ObjectInstanceEquals, + ObjectStaticEquals, + ObjectStaticReferenceEquals + ]; + + public static bool IsPredefinedMethod(ParsingConfig config, MemberInfo member) + { + Check.NotNull(config); + Check.NotNull(member); + + return config.AllowEqualsAndToStringMethodsOnObject && ObjectToStringAndObjectEquals.Contains(member); + } +} \ 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 66bb53fd..f13ee4ef 100644 --- a/src/System.Linq.Dynamic.Core/ParsingConfig.cs +++ b/src/System.Linq.Dynamic.Core/ParsingConfig.cs @@ -312,4 +312,11 @@ public IQueryableAnalyzer QueryableAnalyzer /// Default value is false. /// public bool RestrictOrderByToPropertyOrField { get; set; } + + /// + /// When set to true, the parser will allow the use of the Equals(object obj), Equals(object objA, object objB), ReferenceEquals(object objA, object objB) and ToString() methods on the type. + /// + /// Default value is false. + /// + public bool AllowEqualsAndToStringMethodsOnObject { get; set; } } \ No newline at end of file diff --git a/src/System.Linq.Dynamic.Core/Res.cs b/src/System.Linq.Dynamic.Core/Res.cs index 2bbb2d7f..d2831c24 100644 --- a/src/System.Linq.Dynamic.Core/Res.cs +++ b/src/System.Linq.Dynamic.Core/Res.cs @@ -50,7 +50,7 @@ internal static class Res 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 MethodIsInaccessible = "Method '{0}' on type '{1}' is 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"; diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs index c020cb94..100603aa 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs @@ -281,11 +281,15 @@ public void DynamicClassArray_Issue593_Fails() isValid.Should().BeFalse(); // This should actually be true, but fails. For solution see Issue593_Solution1 and Issue593_Solution2. } - // [SkipIfGitHubActions] - [Fact(Skip = "867")] + [SkipIfGitHubActions] public void DynamicClassArray_Issue593_Solution1() { // Arrange + var config = new ParsingConfig + { + AllowEqualsAndToStringMethodsOnObject = true + }; + var field = new { Name = "firstName", @@ -308,7 +312,7 @@ public void DynamicClassArray_Issue593_Solution1() var query = dynamicClasses.AsQueryable(); // Act - var isValid = query.Any("firstName.ToString() eq \"firstValue\""); + var isValid = query.Any(config, "firstName.ToString() eq \"firstValue\""); // Assert isValid.Should().BeTrue(); diff --git a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs index f56283c0..a65012b0 100644 --- a/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/DynamicExpressionParserTests.cs @@ -1058,13 +1058,23 @@ public void DynamicExpressionParser_ParseLambda_StringLiteral_QuotationMark() Assert.Equal(expectedRightValue, rightValue); } - [Fact(Skip = "867")] + [Fact] public void DynamicExpressionParser_ParseLambda_TupleToStringMethodCall_ReturnsStringLambdaExpression() { + // Arrange + var config = new ParsingConfig + { + AllowEqualsAndToStringMethodsOnObject = true + }; + + // Act var expression = DynamicExpressionParser.ParseLambda( + config, typeof(Tuple), typeof(string), "it.ToString()"); + + // Assert Assert.Equal(typeof(string), expression.ReturnType); } @@ -1147,7 +1157,7 @@ public void DynamicExpressionParser_ParseLambda_CustomMethod_WhenClassDoesNotHav Action action = () => DynamicExpressionParser.ParseLambda(typeof(CustomClassWithMethod), null, expression); // Assert - action.Should().Throw().WithMessage("Methods on type 'CustomClassWithMethod' are not accessible"); + action.Should().Throw().WithMessage("Method 'GetAge' on type 'CustomClassWithMethod' is not accessible."); } // [Fact] diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs index 5770152b..9369d143 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/ExpressionParserTests.cs @@ -346,7 +346,8 @@ public void Parse_NullableShouldReturnNullable(string expression, object resultT [Theory] [InlineData("it.MainCompany.Name != null", "(company.MainCompany.Name != null)")] [InlineData("@MainCompany.Companies.Count() > 0", "(company.MainCompany.Companies.Count() > 0)")] - // [InlineData("Company.Equals(null, null)", "Equals(null, null)")] issue 867 + [InlineData("Company.Equals(null, null)", "Equals(null, null)")] + [InlineData("Equals(null)", "company.Equals(null)")] [InlineData("MainCompany.Name", "company.MainCompany.Name")] [InlineData("Name", "company.Name")] [InlineData("company.Name", "company.Name")] @@ -357,7 +358,8 @@ public void Parse_When_PrioritizePropertyOrFieldOverTheType_IsTrue(string expres var config = new ParsingConfig { IsCaseSensitive = true, - CustomTypeProvider = _dynamicTypeProviderMock.Object + CustomTypeProvider = _dynamicTypeProviderMock.Object, + AllowEqualsAndToStringMethodsOnObject = true }; ParameterExpression[] parameters = { ParameterExpressionHelper.CreateParameterExpression(typeof(Company), "company") }; var sut = new ExpressionParser(parameters, expression, null, config); diff --git a/test/System.Linq.Dynamic.Core.Tests/Parser/MethodFinderTest.cs b/test/System.Linq.Dynamic.Core.Tests/Parser/MethodFinderTest.cs index c4b318e1..eb9edd73 100644 --- a/test/System.Linq.Dynamic.Core.Tests/Parser/MethodFinderTest.cs +++ b/test/System.Linq.Dynamic.Core.Tests/Parser/MethodFinderTest.cs @@ -7,16 +7,93 @@ namespace System.Linq.Dynamic.Core.Tests.Parser; public class MethodFinderTest { - [Fact(Skip = "867")] - public void MethodsOfDynamicLinqAndSystemLinqShouldBeEqual() + [Fact] + public void Method_ToString_OnDynamicLinq_And_SystemLinq_ShouldBeEqual() { + // Arrange + var config = new ParsingConfig + { + AllowEqualsAndToStringMethodsOnObject = true + }; + Expression> expr = x => x.ToString(); - + var selector = "ToString()"; var prm = Parameter(typeof(int?)); - var parser = new ExpressionParser([prm], selector, [], ParsingConfig.Default); - var expr1 = parser.Parse(null); - - Assert.Equal(((MethodCallExpression)expr.Body).Method.DeclaringType, ((MethodCallExpression)expr1).Method.DeclaringType); + var parser = new ExpressionParser([prm], selector, [], config); + + // Act + var expression = parser.Parse(null); + + // Assert + Assert.Equal(((MethodCallExpression)expr.Body).Method.DeclaringType, ((MethodCallExpression)expression).Method.DeclaringType); + } + + [Fact] + public void Method_Equals1_OnDynamicLinq_And_SystemLinq_ShouldBeEqual() + { + // Arrange + var config = new ParsingConfig + { + AllowEqualsAndToStringMethodsOnObject = true + }; + + Expression> expr = x => x.Equals("a"); + + var selector = "Equals(\"a\")"; + var prm = Parameter(typeof(int?)); + var parser = new ExpressionParser([prm], selector, [], config); + + // Act + var expression = parser.Parse(null); + + // Assert + Assert.Equal(((MethodCallExpression)expr.Body).Method.DeclaringType, ((MethodCallExpression)expression).Method.DeclaringType); + } + + [Fact] + public void Method_Equals2_OnDynamicLinq_And_SystemLinq_ShouldBeEqual() + { + // Arrange + var config = new ParsingConfig + { + AllowEqualsAndToStringMethodsOnObject = true + }; + + // ReSharper disable once RedundantNameQualifier + Expression> expr = x => object.Equals("a", "b"); + + var selector = "object.Equals(\"a\", \"b\")"; + var prm = Parameter(typeof(int?)); + var parser = new ExpressionParser([prm], selector, [], config); + + // Act + var expression = parser.Parse(null); + + // Assert + Assert.Equal(((MethodCallExpression)expr.Body).Method.DeclaringType, ((MethodCallExpression)expression).Method.DeclaringType); + } + + [Fact] + public void Method_ReferenceEquals_OnDynamicLinq_And_SystemLinq_ShouldBeEqual() + { + // Arrange + var config = new ParsingConfig + { + AllowEqualsAndToStringMethodsOnObject = true + }; + + // ReSharper disable once RedundantNameQualifier + Expression> expr = x => object.ReferenceEquals("a", "b"); + + var selector = "object.ReferenceEquals(\"a\", \"b\")"; + var prm = Parameter(typeof(int?)); + var parser = new ExpressionParser([prm], selector, [], config); + + // Act + var expression = parser.Parse(null); + + // Assert + Assert.Equal(((MethodCallExpression)expr.Body).Method.DeclaringType, ((MethodCallExpression)expression).Method.DeclaringType); } } \ No newline at end of file diff --git a/test/System.Linq.Dynamic.Core.Tests/SecurityTests.cs b/test/System.Linq.Dynamic.Core.Tests/SecurityTests.cs index 787124e3..ac95f023 100644 --- a/test/System.Linq.Dynamic.Core.Tests/SecurityTests.cs +++ b/test/System.Linq.Dynamic.Core.Tests/SecurityTests.cs @@ -31,7 +31,7 @@ public void MethodsShouldOnlyBeCallableOnPredefinedTypes_Test1() Action action = () => baseQuery.OrderBy(predicate); // Assert - action.Should().Throw().WithMessage("Methods on type 'Object' are not accessible"); + action.Should().Throw().WithMessage("Method 'GetType' on type 'Object' is not accessible."); } [Fact] @@ -48,19 +48,19 @@ public void MethodsShouldOnlyBeCallableOnPredefinedTypes_Test2() ); // Assert - action.Should().Throw().WithMessage($"Methods on type 'Object' are not accessible"); + action.Should().Throw().WithMessage("Method 'GetType' on type 'Object' is not accessible."); } [Theory] - [InlineData(typeof(FileStream), "Close()", "Stream")] - [InlineData(typeof(Assembly), "GetName().Name.ToString()", "Assembly")] - public void DynamicExpressionParser_ParseLambda_IllegalMethodCall_ThrowsException(Type itType, string expression, string type) + [InlineData(typeof(FileStream), "Close()", "Method 'Close' on type 'Stream' is not accessible.")] + [InlineData(typeof(Assembly), "GetName().Name.ToString()", "Method 'GetName' on type 'Assembly' is not accessible.")] + public void DynamicExpressionParser_ParseLambda_IllegalMethodCall_ThrowsException(Type itType, string expression, string errorMessage) { // Act Action action = () => DynamicExpressionParser.ParseLambda(itType, null, expression); // Assert - action.Should().Throw().WithMessage($"Methods on type '{type}' are not accessible"); + action.Should().Throw().WithMessage(errorMessage); } [Theory] @@ -79,7 +79,7 @@ public void UsingSystemReflectionAssembly_ThrowsException(string selector) Action action = () => queryable.Select(selector); // Assert - action.Should().Throw().WithMessage("Methods on type 'Object' are not accessible"); + action.Should().Throw().WithMessage("Method 'GetType' on type 'Object' is not accessible."); } [Theory]