如何通过通用字段动态过滤 IQueryable

How to filter IQueryable dynamically by generic field

我正在尝试创建一个 IDictionary> class,我可以在其中通过函数确定键。我已经成功地用这样的数组构建了 class(为示例起见简化了函数):

private List<TSource> Filter(TKey key, Func<TSource, TKey> keySelector)
{
    IQueryable<TSource> source = GetSource(); // It's there
    IQueryable<TSource> filtered = source.Where(
        x => keySelector.Invoke(x).Equals(key)
        );
    return filtered.ToList();
}

但这只适用于数组等,不适用于 linq-to-sql。我知道这可以用表达式来完成,但这主要超出了我的范围。我用谷歌搜索并提出了以下功能:

private List<TSource> Filter(TKey key, Expression<Func<TSource, TKey>> keySelector)
{
    IQueryable<TSource> source = GetSource();
    Func<TSource, bool> compiledKeyFilter = GetFilter(keySelector);
    IEnumerable<TSource> filtered = source.Where(compiledKeyFilter);
    return filtered.ToList();
}
private Func<TSource, bool> GetFilter(Expression<Func<TSource, TKey>> expr)
{
    if (this.filter == null)
    {
        var invokedExpr = Expression.Invoke(expr, expr.Parameters.Cast<Expression>());
        var lamda = Expression.Lambda<Func<TSource, bool>>(
            Expression.Call(expr.Body, typeof(TKey).GetMethod("Equals", new[] { typeof(TKey) }), invokedExpr),
            expr.Parameters
            );
        this.filter = lamda.Compile();
    }

    return this.filter;
}

当前 returns 源中的所有行。表达式应该是可重用的,而不仅仅是一次性执行。我从 SO (Dynamically generated lookup key for IQueryable) 中找到了以下内容,我所做的非常相似。它有效,但我无法将其与编译方法结合使用:

private Expression<Func<TSource, bool>> MakeFilterExpression(TKey key)
{
    var param = Expression.Parameter(typeof(TSource));
    return Expression.Lambda<Func<TSource, bool>>(
            Expression.Equal(
                Expression.Invoke(keySelector, param),
                Expression.Constant(key)
            ),
        param
        );
}

所以我想想出一个 class 我可以像这样使用:

// Inside MyCache there would be something close to this:
class MyCache
{
    private Func<TSource, bool> filter;
    public MyCache(Expression<Func<TSource, TKey>> func)
    {
        this.filter = MakeFilter(func);
    }
    private Func<TSource, bool> MakeFilter(Expression<Func<TSource, TKey>> func)
    {
        // magic
    }
    public List<TSource> GetByKey(TKey key)
    {
        return GetSource().Where(this.filter(key)).ToList();
    }
}

// This is my class where I give my func to determine the key in ctor.
var cache = new MyCache<MySource>(x => x.myField);
var list1 = cache.GetByKey(3); // Now I have list to iterate.
var list2 = cache.GetByKey(4); // Here's another list.

甚至可以将其编译成可重用的函数吗?帮忙?!

如果我对你的问题的理解正确,那么你正在尝试提出一个 Filter 方法的版本,该方法可以在给定 Expression<Func<TSource, TKey>> 的情况下与 LINQ-to-SQL 查询一起使用键选择器。

您可以使用 LINQKit library 来帮助您。它允许您从一个表达式调用另一个表达式,然后 "expand" 结果表达式。

以下是如何在 LINQKit 的帮助下编写 Filter 方法:

private static List<TSource> Filter<TSource,TKey>(
    TKey key,
    Expression<Func<TSource, TKey>> keySelector)
{
    IQueryable<TSource> source = GetSource<TSource>(); // It's there

    Expression<Func<TSource, bool>> predicate = x => keySelector.Invoke(x).Equals(key);

    //LINQ-to-SQL cannot translate `Invoke` into SQL. However, after invoking this line
    //LINQKit would flatten the expression so that it contains something like
    //x => x.Age.Equals(5) (for a keySelector of value x => x.Age and key of value 5)
    predicate = predicate.Expand();

    IQueryable<TSource> filtered = source.Where(predicate);

    return filtered.ToList();
}