Build expression tree for LINQ using List<T>.Contains method
You can create your own extension method, name it
Where
, accept anIQueryable<T>
, return anIQueryable<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 themWhere
. If you really wanted to name oneWhere
though nothing's stopping you.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.See the first half of #2.
Related videos on Youtube
Garrett Bates
Updated on June 04, 2022Comments
-
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 ownIQueryable
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 ofExpression<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
- Can I implement my own
Queryable.Where
overload? If so, is it even feasible? - If an overload isn't feasible, is there a way to dynamically construct the predicate
p => codes.Contains(p.Code)
without using two parameters? - Is there an easier way to do this? I feel like I'm missing something.
- Can I implement my own
-
Garrett Bates almost 9 yearsExcellent! 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.