1

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?

si2030
  • 3,895
  • 8
  • 38
  • 87
  • 1
    Just want to make sure you know what expression you actually need to construct - https://stackoverflow.com/questions/444798/case-insensitive-containsstring before digging into the question... – Alexei Levenkov Sep 07 '19 at 03:23
  • 1
    Yeah, `IndexOf` returns a number (i.e. the index), not a boolean. I'm not sure when you expect this to return true, and when to return false, but I think you might be trying to reimplement `Contains` by another name – Andrew Williamson Sep 07 '19 at 03:25
  • Im actually not that worried about whether its contains Index of etc.. Just need this to be case insensitive for the search.. appreciate the response though. – si2030 Sep 07 '19 at 05:27
  • Just looked at that question Alexei and yes its case insensitive Im after.. just not sure how to implement this with the current code and dynamic lambda.. Think I might need to add another Expression parameter.. – si2030 Sep 07 '19 at 05:30

3 Answers3

3

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);
        }
    }
}
user7217806
  • 2,014
  • 2
  • 10
  • 12
  • The only issue with using the custom compare approach is if this is meant to be converted to SQL. – Nkosi Sep 08 '19 at 05:26
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))
    );
Andrew Williamson
  • 8,299
  • 3
  • 34
  • 62
0

i did it using ToLower expression first and then the result of this expression was instance parameter in my contains expression exam:

    ParameterExpression arg = Expression.Parameter(entityType, "x");
    MemberExpression property = Expression.Property(arg, field);

    var lowerStringExpression = Expression.Call(property, "ToLower", null);            
    var expression = Expression.Call(lowerStringExpression, "Contains", null, Expression.Constant(value.ToLower(), typeof(string)));

    return Expression.Lambda<Func<TSource, bool>>(expression, new ParameterExpression[] { arg });
Harold Meza
  • 67
  • 2
  • 1