使用 List<T>.Contains 方法为 LINQ 构建表达式树

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

问题

我正在为我们的 Web 应用程序中的多个报告重构一些 LINQ 查询,并且我正在尝试将一些重复的查询谓词移动到它们自己的 IQueryable 扩展方法中,以便我们可以将它们重新用于这些报告和将来的报告。正如您可能推断的那样,我已经重构了组的谓词,但是代码的谓词给我带来了问题。这是我目前拥有的一种报告方法的示例:

DAL 方法:

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 分机:

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;
}

谓词:

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

我遇到的问题是 Queryable.Where 不接受 Expression<Func<T, List<string>, bool> 的类型。我能想到的动态创建此谓词的唯一方法是使用两个参数,这是真正难倒我的部分。

我不明白的是以下方法有效。我可以传递我尝试动态创建的确切 lambda 表达式,它可以正确地过滤我的数据。

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

        }

    }

问题

  1. 我可以实现自己的 Queryable.Where 重载吗?如果是这样,它是否可行?
  2. 如果重载不可行,有没有办法在不使用两个参数的情况下动态构造谓词 p => codes.Contains(p.Code)
  3. 有更简单的方法吗?我觉得我错过了什么。
  1. 您可以创建自己的扩展方法,将其命名为 Where,接受一个 IQueryable<T>,return 一个 IQueryable<T>,否则就创建它模拟 LINQ 方法的形式。它不会成为 LINQ 方法,但它看起来像一个。我不鼓励您编写这样的方法,因为它可能会使其他人感到困惑;即使您想创建一个新的扩展方法,也请使用 LINQ 中未使用的名称以避免混淆。简而言之,做你现在正在做的事情,创建新的扩展而不实际命名它们 Where。如果你真的想命名一个 Where 尽管没有什么能阻止你。

  2. 当然,只需使用 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);
    }
    

    如果你真的不能让你的实体实现接口(提示:你几乎肯定可以),那么代码看起来与你拥有的代码相同,但使用你传入的列表作为常量而不是一个新参数:

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

    我强烈建议使用前一种方法;此方法失去了静态类型安全性,并且更复杂,因此更难维护。

    另外请注意,您在代码中的注释:// j => codes.Contains(j.Code) 不准确。 lambda actually 看起来像:(j, codes) => codes.Contains(j.Code); 实际上明显不同。

  3. 参见#2 的前半部分。