0

Piggybacking off of a very similar question...

I need to generate an Expression from a ViewModel to pass as a search predicate for IQueryable.Where. I need to be able to include/exclude query parameters based on what is provided by the user. Example:

public class StoresFilter
{
    public int[] Ids { get; set; }

    [StringLength(150)]
    public string Name { get; set; }

    [StringLength(5)]
    public string Abbreviation { get; set; }

    [Display(Name = "Show all")]
    public bool ShowAll { get; set; } = true;

    public Expression<Func<Store, bool>> ToExpression()
    {
        List<Expression<Func<Store, bool>>> expressions = new List<Expression<Func<Store, bool>>>();

        if (Ids != null && Ids.Length > 0)
        {
            expressions.Add(x => Ids.Contains(x.Id));
        }
        if (Name.HasValue())
        {
            expressions.Add(x => x.Name.Contains(Name));
        }
        if (Abbreviation.HasValue())
        {
            expressions.Add(x => x.Abbreviation.Contains(Abbreviation));
        }
        if (!ShowAll)
        {
            expressions.Add(x => x.Enabled == true);
        }
        if (expressions.Count == 0)
        {
            return x => true;
        }

        // how to combine list of expressions into composite expression???
        return compositeExpression;
    }
}

Is there a simple way to build a composite expression from a list of expressions? Or do I need to go through the process of manually building out the expression using ParameterExpression, Expression.AndAlso, ExpressionVisitor, etc?

Mrinal Kamboj
  • 11,300
  • 5
  • 40
  • 74
Sam
  • 9,933
  • 12
  • 68
  • 104
  • As far as I understand you are looking for a way to generate the `Expression>` at run time, which can be supplied to a `IQueryable` where clause, it can be easily made a generic for any type `T`. Otherwise need to be coded for every type separately for type specific logic – Mrinal Kamboj May 15 '18 at 08:40
  • Update the code for using the Sample model `Store` that you have provided, adding a code for all use cases you have mentioned – Mrinal Kamboj May 16 '18 at 09:47

2 Answers2

7

You should not build and combine Expressions, but instead of this you should do it through IQuerable<Store> via .Where chain. Moreover, source.Expression will contain desired expression:

public IQueryable<Store> ApplyFilter(IQueryable<Store> source)
{
    if (Ids != null && Ids.Length > 0)  
        source = source.Where(x => Ids.Contains(x.Id)); 

    if (Name.HasValue())    
        source = source.Where(x => x.Name.Contains(Name));  

    if (Abbreviation.HasValue())    
        source = source.Where(x => x.Abbreviation.Contains(Abbreviation));  

    if (!ShowAll)   
        source = source.Where(x => x.Enabled == true);      

    //or return source.Expression as you wanted
    return source;
}

Usage:

var filter = new StoresFilter { Name = "Market" };
var filteredStores = filter.ApplyFilter(context.Stores).ToList();
Slava Utesinov
  • 13,410
  • 2
  • 19
  • 26
  • 1
    This is treating `IQueryable` like `IEnumerable` and replacing `Expression>` with `Func`, which would work when all data is loaded in the memory, but in reality `IQueryable` shall do the remote data processing, that's why Expression instead of plain Func. This [link](https://stackoverflow.com/questions/793571/why-would-you-use-expressionfunct-rather-than-funct?rq=1) explains well. For all data in memory IEnumerable serves the purpose – Mrinal Kamboj May 16 '18 at 09:41
  • @MrinalKamboj, can you concrete specify row, where it happen( _"treating"_)? Only `filteredStores` variable is `IEnumerable`, just because I called `.ToLIst()` method. – Slava Utesinov May 16 '18 at 10:22
  • `IQueryable` implements `IEnumerable`, it is internally calling a MethodCallExpression , Check here https://referencesource.microsoft.com/#System.Core/System/Linq/IQueryable.cs,965b3e30b9ed851f, for Linq methods like `Select`, `Where`, in this case it will be direct translation to Func, nothing else is possible, since data is in memory and there's no Expression tree implementation it cannot be serialized to a remote data source to parse / execute to fetch data. This is correct in this context, but has limited application from Queryable context – Mrinal Kamboj May 16 '18 at 11:15
  • @MrinalKamboj, why you decide, that data is already in memory? `context.Stores` is not in memory. Read question again: "to pass as a search predicate for IQueryable.Where", not IEnumerable.Where. Data only materialized after ToLIst() call. Where is conversion(impl/expl) to `Func` occurs, specify point. – Slava Utesinov May 16 '18 at 12:05
  • @ SlavaUtesinov what kind of remote context is not clear from the question itself, creating Lambda based Expression is not always feasible / supported, especially if type metadata is not accessible. On other hand creating expressions through ExpressionTree classes would work in most of the cases. In my understanding there would be remote context where solution provided by you may not work as expected and would need explicit ExpressionTrees translation – Mrinal Kamboj May 16 '18 at 12:37
  • @MrinalKamboj, kind of remote context doesn't matter. Real importance is that `source` implements `IQueryable`(read the question), so it always can build `Expression`s internally by itself via corresponding extention methods. At case of database, `Expression`s will be traslated to T-SQL, at case of `List`, it will be compiled to `Func`, we should not care about it, it is work of `IQueryProvider`. Your intention is correct, but it is too complicated and redundant. – Slava Utesinov May 16 '18 at 13:05
  • By Remote Context I meant the implementation of the `IQueryable` for remote data processing, as many system are now supporting C# drivers, In Apache Ignite (Grid Gain), I have seen implementation of `ICacheQueryable`, where they prefer to take Expression class object, as its easy to parse Metadata. In fact some of the `IQueryable` methods contains `NotImplementedException`. In my understanding Expression class usage may be complex but provides much more control for data processing – Mrinal Kamboj May 17 '18 at 05:34
  • @MrinalKamboj, It is endless dispute. Let OP decide. – Slava Utesinov May 17 '18 at 05:41
  • No dispute from my end, your solution is certainly simpler, provided the IQueryable implementer is able to parse and create Expression internally – Mrinal Kamboj May 17 '18 at 06:01
2
void Main()
{
    var store = new Store
    {
      Id = 1,
      Abbreviation = "ABC",
      Enabled = true,
      Name = "DEF"
    };

   var filter =  new Filter<Store>
   {
    Ids = new HashSet<int>(new [] {1,2,3,4}),
    Abbreviation = "GFABC",
    Enabled = true,
    Name = "SDEFGH",
    ShowAll = false
   }

   var expression = filter.ToExpression(store);

   var parameterType = Expression.Parameter(typeof(Store), "obj");

   // Generate Func from the Expression Tree
   Func<Store,bool> func = Expression.Lambda<Func<Store,bool>>(expression,parameterType).Compile();
}

public class Store
{
    public int Id {get; set;}

    public string Name {get; set;}

    public string Abbreviation { get; set; }

    public bool Enabled { get; set; }   
}

public class Filter<T> where T : Store
{
    public HashSet<int> Ids { get; set; }

    public string Name { get; set; }

    public string Abbreviation { get; set; }

    public bool Enabled {get; set;}

    public bool ShowAll { get; set; } = true;

    public Expression ToExpression(T data)
    {
        var parameterType = Expression.Parameter(typeof(T), "obj");

        var expressionList = new List<Expression>();

        if (Ids != null && Ids.Count > 0)
        {
            MemberExpression idExpressionColumn = Expression.Property(parameterType, "Id");

            ConstantExpression idConstantExpression = Expression.Constant(data.Id, typeof(int));

            MethodInfo filtersMethodInfo = typeof(HashsetExtensions).GetMethod("Contains", new[] { typeof(HashSet<int>), typeof(int) });

            var methodCallExpression = Expression.Call(null, filtersMethodInfo, idExpressionColumn, idConstantExpression);

            expressionList.Add(methodCallExpression);
        }
        if (!string.IsNullOrEmpty(Name))
        {
            MemberExpression idExpressionColumn = Expression.Property(parameterType, "Name");

            ConstantExpression idConstantExpression = Expression.Constant(data.Name, typeof(string));

            MethodInfo filtersMethodInfo = typeof(StringExtensions).GetMethod("Contains", new[] { typeof(string), typeof(string) });

            var methodCallExpression = Expression.Call(null, filtersMethodInfo, idExpressionColumn, idConstantExpression);

            expressionList.Add(methodCallExpression);
        }
        if (!string.IsNullOrEmpty(Abbreviation))
        {
            MemberExpression idExpressionColumn = Expression.Property(parameterType, "Abbreviation");

            ConstantExpression idConstantExpression = Expression.Constant(data.Abbreviation, typeof(string));

            MethodInfo filtersMethodInfo = typeof(StringExtensions).GetMethod("Contains", new[] { typeof(string), typeof(string) });

            var methodCallExpression = Expression.Call(null, filtersMethodInfo, idExpressionColumn, idConstantExpression);

            expressionList.Add(methodCallExpression);
        }
        if (!ShowAll)
        {
            MemberExpression idExpressionColumn = Expression.Property(parameterType, "Enabled");

            var binaryExpression = Expression.Equal(idExpressionColumn, Expression.Constant(true, typeof(bool)));

            expressionList.Add(binaryExpression);
        }

        if (expressionList.Count == 0)
        {
            expressionList.Add(BinaryExpression.Constant(true));
        }

        // Aggregate List<Expression> data into single Expression

        var returnExpression = expressionList.Skip(1).Aggregate(expressionList.First(), (expr1,expr2) => Expression.And(expr1,expr2));      

        return returnExpression;

        // Generate Func<T,bool> - Expression.Lambda<Func<T,bool>>(returnExpression,parameterType).Compile();
    }

}

public static class StringExtensions
{
    public static bool Contains(this string source, string subString)
    {
        return source?.IndexOf(subString, StringComparison.OrdinalIgnoreCase) >= 0;
    }
}

public static class HashsetExtensions
{
    public static bool Contains(this HashSet<string> source, string subString)
    {
        return source.Contains(subString,StringComparer.OrdinalIgnoreCase);
    }
}

How it works ?

  • Only in simple equality cases you can use BinaryExpression like Expression.Equal, Expression.GreaterThan, which is shown for the property like "ShowAll"
  • For other cases like string / Array / List Contains, you need extension method, which can take two types and provide the result. A separate Contains for string to make it case neutral. Also for collection Hashset has a better choice, it has O(1) time complexity, unlike O(N) for an array
  • We use MethodCallExpression to call the extension methods
  • Finally we aggreagte all the expressions, which can be compiled to create Func<T,bool>
  • In case you need something like x => true, then BinaryExpression.Constant(true) is sufficient
  • I have provided a Sample implementation using the Store class that you have defined
Mrinal Kamboj
  • 11,300
  • 5
  • 40
  • 74
  • Downvoter mostly you don't understand the Expression trees, what I have mentioned is the correct way of custom processing Expression tree to generate `Function` – Mrinal Kamboj May 15 '18 at 17:28