Build expression tree for LINQ using List<T>.Contains method

12,114
  1. You can create your own extension method, name it Where, accept an IQueryable<T>, return an IQueryable<T>, and otherwise make it emulate the form of LINQ methods. It wouldn't be a LINQ method, but it would look like one. I would discourage you from writing such a method simply because it would likely confuse others; even if you want to make a new extension method, use a name not used in LINQ to avoid confusion. In short, do what you're doing now, create new extensions without actually naming them Where. If you really wanted to name one Where though nothing's stopping you.

  2. Sure, just use a lambda:

    public static Expression<Func<T, bool>> FilterByCode<T>(List<string> codes)
        where T : ICoded //some interface with a `Code` field
    {
        return p => codes.Contains(p.Code);
    }
    

    If you really cannot have your entities implement an interface (hint: you almost certainly can), then the code would look identical to the code that you have, but using the list that you pass in as a constant rather than a new parameter:

    public static Expression<Func<T, bool>> FilterByCode<T>(List<string> codes)
    {
        var methodInfo = typeof(List<string>).GetMethod("Contains", 
            new Type[] { typeof(string) });
    
        var list = Expression.Constant(codes);
    
        var param = Expression.Parameter(typeof(T), "j");
        var value = Expression.Property(param, "Code");
        var body = Expression.Call(list, methodInfo, value);
    
        // j => codes.Contains(j.Code)
        return Expression.Lambda<Func<T, bool>>(body, param);
    }
    

    I would strongly encourage use of the former method; this method loses static type safety, and is more complex and as such harder to maintain.

    Another note, the comment you have in your code: // j => codes.Contains(j.Code) isn't accurate. What that lambda actually looks like is: (j, codes) => codes.Contains(j.Code); which is actually noticeably different.

  3. See the first half of #2.

Share:
12,114

Related videos on Youtube

Garrett Bates
Author by

Garrett Bates

Updated on June 04, 2022

Comments

  • Garrett Bates
    Garrett Bates almost 2 years

    Problem

    I'm working on refactoring some LINQ queries for several reports in our web application, and I'm attempting to move some duplicate query predicates into their own IQueryable exension methods so we can reuse them for these reports, and reports in the future. As you can probably infer, I've already refactored the predicate for groups, but the predicate for codes is giving me problems. This is an example of one of the report methods I have so far:

    DAL method:

    public List<Entities.QueryView> GetQueryView(Filter filter)
    {
        using (var context = CreateObjectContext())
        {
            return (from o in context.QueryViews
                        where (!filter.FromDate.HasValue || o.RepairDate >= EntityFunctions.TruncateTime(filter.FromDate))
                        && (!filter.ToDate.HasValue || o.RepairDate <= EntityFunctions.TruncateTime(filter.ToDate))
                        select o)
                    .WithCode(filter)
                    .InGroup(filter)
                    .ToList();
        }
    }
    

    IQueryable Extension:

    public static IQueryable<T> WithCode<T>(this IQueryable<T> query, Filter filter)
    {
        List<string> codes = DAL.GetCodesByCategory(filter.CodeCategories);
    
        if (codes.Count > 0)
            return query.Where(Predicates.FilterByCode<T>(codes));
    
        return query;
    }
    

    Predicate:

    public static Expression<Func<T, List<string>, bool>> FilterByCode<T>(List<string> codes)
    {
        // Method info for List<string>.Contains(code).
        var methodInfo = typeof(List<string>).GetMethod("Contains", new Type[] { typeof(string) });
    
        // List of codes to call .Contains() against.
        var instance = Expression.Variable(typeof(List<string>), "codes");
    
        var param = Expression.Parameter(typeof(T), "j");
        var left = Expression.Property(param, "Code");
        var expr = Expression.Call(instance, methodInfo, Expression.Property(param, "Code"));
    
        // j => codes.Contains(j.Code)
        return Expression.Lambda<Func<T, List<string>, bool>>(expr, new ParameterExpression[] { param, instance });
    }
    

    The problem I'm having is that Queryable.Where doesn't accept a type of Expression<Func<T, List<string>, bool>. The only way I can think of creating this predicate dynamically is to use two parameters, which is the part that is really stumping me.

    What I'm not comprehending is the following method works. I can pass the exact lambda expression I am trying to create dynamically, and it correctly filters my data.

    public List<Entities.QueryView> GetQueryView(Filter filter)
    {
        // Get the codes here.
        List<string> codes = DAL.GetCodesByCategory(filter.CodeCategories);
    
        using (var context = CreateObjectContext())
        {
            return (from o in context.QueryViews
                        where (!filter.FromDate.HasValue || o.RepairDate >= EntityFunctions.TruncateTime(filter.FromDate))
                        && (!filter.ToDate.HasValue || o.RepairDate <= EntityFunctions.TruncateTime(filter.ToDate))
                        select o)
                    .Where(p => codes.Contains(p.Code)) // This works fine.
                    //.WithCode(filter)
                    .InGroup(filter)
                    .ToList();
    
            }
    
        }
    

    Questions

    1. Can I implement my own Queryable.Where overload? If so, is it even feasible?
    2. If an overload isn't feasible, is there a way to dynamically construct the predicate p => codes.Contains(p.Code) without using two parameters?
    3. Is there an easier way to do this? I feel like I'm missing something.
  • Garrett Bates
    Garrett Bates almost 9 years
    Excellent! This was very helpful. I created an interface for the QueryView entity, and used it as a type constraint for my extension method and everything works. Thank you for your help.