Sam
Sam

Reputation: 10113

How to convent viewmodel to Expression<Func<T,bool>>?

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?

Upvotes: 0

Views: 302

Answers (2)

Slava Utesinov
Slava Utesinov

Reputation: 13498

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();

Upvotes: 7

Mrinal Kamboj
Mrinal Kamboj

Reputation: 11478

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

Upvotes: 2

Related Questions