C# Dynamic lambda problem with implementing "IndexOf" ignore case

c# dynamic-linq

Question

I have been following pashov.net and in particular his approach to filtering by building dynamic Linq expressions.

I implemented it and it worked but is the string search is case sensitive. Currently it does not have an IndexOf StringComparison.OrdinalIgnoreCase option so I had a go at adding one in.

I get an error once it reached that part of the code where it tries to run the lambda call on it... return Expression.Lambda<Func<T, bool>>(exp, param);

It was having trouble converting from int32 to bool.

    System.ArgumentException
  HResult=0x80070057
  Message=Expression of type 'System.Int32' cannot be used for return type 'System.Boolean'
  Source=System.Linq.Expressions
  StackTrace:
   at System.Linq.Expressions.Expression.ValidateLambdaArgs(Type delegateType, Expression& body, ReadOnlyCollection`1 parameters, String paramName)
   at System.Linq.Expressions.Expression.Lambda[TDelegate](Expression body, String name, Boolean tailCall, IEnumerable`1 parameters)
   at System.Linq.Expressions.Expression.Lambda[TDelegate](Expression body, Boolean tailCall, IEnumerable`1 parameters)
   at System.Linq.Expressions.Expression.Lambda[TDelegate](Expression body, ParameterExpression[] parameters)
   at JobsLedger.API.ControllerServices.Shared.OrderAndFIlterHelpers.DynamicFilteringHelper.ConstructAndExpressionTree[T](List`1 filters) in C:\AURELIA\1.0 - JobsLedgerSPA -ASPNET CORE 3.0\JobsLedger.API\ControllerServices\Shared\OrderAndFIlterHelpers\DynamicFilteringHelper.cs:line 48
   at JobsLedger.API.ControllerServices.Shared.ODataFilterAndSort.ParginatedFilteredSorted[T](IQueryable`1 source, String query) in C:\AURELIA\1.0 - JobsLedgerSPA -ASPNET CORE 3.0\JobsLedger.API\ControllerServices\Shared\OrderAndFIlterHelpers\ODataFilterAndSort.cs:line 41
   at JobsLedger.API.ControllerServices.API.App.ClientServices.ClientServices.GetPaginatedClients(String query) in C:\AURELIA\1.0 - JobsLedgerSPA -ASPNET CORE 3.0\JobsLedger.API\ControllerServices\API\App\ClientServices\ClientServices.cs:line 65
   at JobsLedger.API.Controllers.API.App.ClientController.Index(String query) in C:\AURELIA\1.0 - JobsLedgerSPA -ASPNET CORE 3.0\JobsLedger.API\Controllers\API\App\ClientController.cs:line 28
   at Microsoft.Extensions.Internal.ObjectMethodExecutor.Execute(Object target, Object[] parameters)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ActionMethodExecutor.SyncActionResultExecutor.Execute(IActionResultTypeMapper mapper, ObjectMethodExecutor executor, Object controller, Object[] arguments)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeActionMethodAsync()
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
   at Microsoft.AspNetCore.Mvc.Infrastructure.ControllerActionInvoker.InvokeNextActionFilterAsync()

I did some research and it was suggested that it be converted Using .Convert but that didn't work as there is no conversion from int32 to bool.

here is the code.

        public static Expression<Func<T, bool>> ConstructAndExpressionTree<T>(List<ExpressionFilter> filters) {
            if (filters.Count == 0)
                return null;

            ParameterExpression param = Expression.Parameter(typeof(T), "t");
            Expression exp = null;

            if (filters.Count == 1) {
                exp = ExpressionRetriever.GetExpression<T>(param, filters[0]);
            }
            else {
                exp = ExpressionRetriever.GetExpression<T>(param, filters[0]);
                for (int i = 1; i < filters.Count; i++) {
                    exp = Expression.And(exp, ExpressionRetriever.GetExpression<T>(param, filters[i]));
                }
            }

            return Expression.Lambda<Func<T, bool>>(exp, param); //.. FAILS HERE
        }

    public static class ExpressionRetriever {
        private static MethodInfo containsMethod = typeof(string).GetMethod("Contains");
        private static MethodInfo startsWithMethod = typeof(string).GetMethod("StartsWith", new Type[] { typeof(string) });
        private static MethodInfo endsWithMethod = typeof(string).GetMethod("EndsWith", new Type[] { typeof(string) });

        public static Expression GetExpression<T>(ParameterExpression param, ExpressionFilter filter) {
            MemberExpression member = Expression.Property(param, filter.PropertyName);
            ConstantExpression constant = Expression.Constant(filter.Value);
            switch (filter.Comparison) {
                case Comparison.Equal:
                    return Expression.Equal(member, constant);
                case Comparison.GreaterThan:
                    return Expression.GreaterThan(member, constant);
                case Comparison.GreaterThanOrEqual:
                    return Expression.GreaterThanOrEqual(member, constant);
                case Comparison.LessThan:
                    return Expression.LessThan(member, constant);
                case Comparison.LessThanOrEqual:
                    return Expression.LessThanOrEqual(member, constant);
                case Comparison.NotEqual:
                    return Expression.NotEqual(member, constant);
                case Comparison.Contains:
                    return Expression.Call(member, containsMethod, constant);
                case Comparison.StartsWith:
                    return Expression.Call(member, startsWithMethod, constant); 
                case Comparison.IndexOf:
                    var test = Expression.Call(member, "IndexOf", null, Expression.Constant(filter.Value, typeof(string)), Expression.Constant(StringComparison.InvariantCultureIgnoreCase));           
                    return test;
                    //return Expression.Convert(test, typeof(bool));
                case Comparison.EndsWith:
                    return Expression.Call(member, endsWithMethod, constant);
                default:
                    return null;
            }
        }
    }

It would be nice if the I could put some code in that didnt require creating a Call as I have done. Is there a way to implement this as a method etc as per the other string options or is the way I tried the only way considering I need an extra parameter..

i.e How can I implement the IndexOf option and return a bool?

1
0
9/7/2019 2:18:42 AM

Accepted Answer

In .NET Core there is an overload of Contains which allows specifying comparison rules. There you could end up with something like:

public static class ExpressionRetriever
{
    private static MethodInfo containsMethod = typeof(string).GetMethod("Contains", new Type[] { typeof(string), typeof(StringComparison)});
    ...

    public static Expression GetExpression<T>(ParameterExpression param, ExpressionFilter filter)
    {
        ...
        ConstantExpression comparisonType = Expression.Constant(StringComparison.OrdinalIgnoreCase);
        switch (filter.Comparison)
        {
            ...
            case Comparison.Contains:
                return Expression.Call(member, containsMethod, constant), comparisonType);
        }
    }
}

In any other case, you could create a custom compare method which you use in your expression, like:

public class StringExtensions
{
    public static bool ContainsIgnoringCase(string str, string substring)
    {
        return str.IndexOf(substring, StringComparison.OrdinalIgnoreCase) >= 0;
    }
}

...

public static class ExpressionRetriever
{
    ...
    private static MethodInfo containsIgnoringCaseMethod = typeof(StringExtensions).GetMethod("ContainsIgnoringCase", new Type[] { typeof(string), typeof(string)});

    public static Expression GetExpression<T>(ParameterExpression param, ExpressionFilter filter)
    {
        ...
        switch (filter.Comparison)
        {
            ...
            case Comparison.Contains:
                return Expression.Call(null, containsIgnoringCaseMethod, member, constant);
        }
    }
}
2
9/7/2019 8:45:56 AM

Popular Answer

From the documentation for IndexOf, the method returns:

The index position of the value parameter if that string is found, or -1 if it is not.

So in order to check if Foo.PropertyName contains Value, you need to create the following expression:

Foo.PropertyName.IndexOf(Value, StringComparison.InvariantCultureIgnoreCase) != -1

This means wrapping all of what you have so far in Expression.NotEqual(left, right):

case Comparison.IndexOf:
    return Expression.NotEqual(
        Expression.Call(
            member,
            "IndexOf",
            null,
            Expression.Constant(filter.Value, typeof(string)),
            Expression.Constant(StringComparison.InvariantCultureIgnoreCase, typeof(StringComparison))
        ),
        Expression.Constant(-1, typeof(int))
    );


Related Questions





Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow
Licensed under: CC-BY-SA with attribution
Not affiliated with Stack Overflow