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

0

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?

c#
dynamic-linq
asked on Stack Overflow Sep 7, 2019 by si2030

2 Answers

2

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);
        }
    }
}
answered on Stack Overflow Sep 7, 2019 by user7217806
1

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))
    );
answered on Stack Overflow Sep 7, 2019 by Andrew Williamson

User contributions licensed under CC BY-SA 3.0