Skip to content

Commit

Permalink
Add option to ParsingConfig to allow the Equals and ToString methods …
Browse files Browse the repository at this point in the history
…on object (#875)

* Add option to ParsingConfig to allow the Equals and ToString methods on object

* .

* ---

* ReferenceEquals

* .
  • Loading branch information
StefH authored Jan 24, 2025
1 parent fca802e commit 1934a14
Show file tree
Hide file tree
Showing 12 changed files with 158 additions and 35 deletions.
2 changes: 0 additions & 2 deletions src-blazor/BlazorAppServer/BlazorAppServer.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,7 @@
<PackageReference Include="Microsoft.Extensions.Configuration" Version="6.0.1" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="6.0.0" />
<PackageReference Include="System.ComponentModel.Annotations" Version="5.0.0" />
<PackageReference Include="Oqtane.Shared" Version="3.1.4" />
<PackageReference Include="System.Configuration.ConfigurationManager" Version="6.0.0" />
<!--<PackageReference Include="System.Linq.Dynamic.Core" Version="1.2.19" />-->
</ItemGroup>

<ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#if NETSTANDARD1_3
#if NETSTANDARD1_3 || UAP10_0
using System.Linq;

namespace System.Reflection;
Expand Down
11 changes: 3 additions & 8 deletions src/System.Linq.Dynamic.Core/DynamicClassFactory.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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)])!;
Expand Down Expand Up @@ -419,7 +414,7 @@ private static Type EmitType(IList<DynamicProperty> 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);
Expand Down
4 changes: 2 additions & 2 deletions src/System.Linq.Dynamic.Core/Parser/ExpressionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
30 changes: 30 additions & 0 deletions src/System.Linq.Dynamic.Core/Parser/PredefinedMethodsHelper.cs
Original file line number Diff line number Diff line change
@@ -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<MemberInfo> 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);
}
}
7 changes: 7 additions & 0 deletions src/System.Linq.Dynamic.Core/ParsingConfig.cs
Original file line number Diff line number Diff line change
Expand Up @@ -312,4 +312,11 @@ public IQueryableAnalyzer QueryableAnalyzer
/// Default value is <c>false</c>.
/// </summary>
public bool RestrictOrderByToPropertyOrField { get; set; }

/// <summary>
/// When set to <c>true</c>, 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 <see cref="object"/> type.
///
/// Default value is <c>false</c>.
/// </summary>
public bool AllowEqualsAndToStringMethodsOnObject { get; set; }
}
2 changes: 1 addition & 1 deletion src/System.Linq.Dynamic.Core/Res.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down
10 changes: 7 additions & 3 deletions test/System.Linq.Dynamic.Core.Tests/DynamicClassTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<int>),
typeof(string),
"it.ToString()");

// Assert
Assert.Equal(typeof(string), expression.ReturnType);
}

Expand Down Expand Up @@ -1147,7 +1157,7 @@ public void DynamicExpressionParser_ParseLambda_CustomMethod_WhenClassDoesNotHav
Action action = () => DynamicExpressionParser.ParseLambda(typeof(CustomClassWithMethod), null, expression);

// Assert
action.Should().Throw<ParseException>().WithMessage("Methods on type 'CustomClassWithMethod' are not accessible");
action.Should().Throw<ParseException>().WithMessage("Method 'GetAge' on type 'CustomClassWithMethod' is not accessible.");
}

// [Fact]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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")]
Expand All @@ -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);
Expand Down
91 changes: 84 additions & 7 deletions test/System.Linq.Dynamic.Core.Tests/Parser/MethodFinderTest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Func<int?, string?>> 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<Func<int?, bool>> 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<Func<int?, bool>> 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<Func<int?, bool>> 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);
}
}
14 changes: 7 additions & 7 deletions test/System.Linq.Dynamic.Core.Tests/SecurityTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ public void MethodsShouldOnlyBeCallableOnPredefinedTypes_Test1()
Action action = () => baseQuery.OrderBy(predicate);

// Assert
action.Should().Throw<ParseException>().WithMessage("Methods on type 'Object' are not accessible");
action.Should().Throw<ParseException>().WithMessage("Method 'GetType' on type 'Object' is not accessible.");
}

[Fact]
Expand All @@ -48,19 +48,19 @@ public void MethodsShouldOnlyBeCallableOnPredefinedTypes_Test2()
);

// Assert
action.Should().Throw<ParseException>().WithMessage($"Methods on type 'Object' are not accessible");
action.Should().Throw<ParseException>().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<ParseException>().WithMessage($"Methods on type '{type}' are not accessible");
action.Should().Throw<ParseException>().WithMessage(errorMessage);
}

[Theory]
Expand All @@ -79,7 +79,7 @@ public void UsingSystemReflectionAssembly_ThrowsException(string selector)
Action action = () => queryable.Select(selector);

// Assert
action.Should().Throw<ParseException>().WithMessage("Methods on type 'Object' are not accessible");
action.Should().Throw<ParseException>().WithMessage("Method 'GetType' on type 'Object' is not accessible.");
}

[Theory]
Expand Down

0 comments on commit 1934a14

Please sign in to comment.