使用表达式<func <tsource,tkey =“”>&gt;使用IQueryable </func <tsource,>

时间:2014-09-09 15:12:38

标签: c# linq linq-to-sql linq-to-objects

我试图编写一个函数,使用键选择器过滤IQueryable数据源,如果集合大于特定阈值,则使用SQL或内存中的集合。

这就是我现在所拥有的。

public static IEnumerable<TSource> SafeFilter<TSource, TKey>(this IQueryable<TSource> source, Func<TSource, TKey> keySelector, HashSet<TKey> filterSet, int threshold = 500)
{    
     if (filterSet.Count > threshold)
         return source.AsEnumerable().Where(x => filterSet.Contains(keySelector(x))); //In memory
     return source.Where(x => filterSet.AsEnumerable().Contains(keySelector(x)));     //In SQL
}

它编译并适用于“内存”情况但不适用于Sql server情况。我明白了:

  

方法'System.Object DynamicInvoke(System.Object [])'不受支持   转换为SQL

我怀疑我需要将其更改为Expression<Func<TSource, TKey>>,但不确定如何使用它。任何帮助表示赞赏。

1 个答案:

答案 0 :(得分:2)

你在这里做的是在另一个中组成一个功能。对于委托,这很容易,因为您可以调用一个,然后将结果作为参数传递给另一个。构成表达式稍微复杂一些;您需要将该参数的所有使用实例替换为它正在编写的表达式。幸运的是,您可以将此逻辑提取到自己的方法中:

public static Expression<Func<TFirstParam, TResult>>
    Compose<TFirstParam, TIntermediate, TResult>(
    this Expression<Func<TFirstParam, TIntermediate>> first,
    Expression<Func<TIntermediate, TResult>> second)
{
    var param = Expression.Parameter(typeof(TFirstParam), "param");

    var newFirst = first.Body.Replace(first.Parameters[0], param);
    var newSecond = second.Body.Replace(second.Parameters[0], newFirst);

    return Expression.Lambda<Func<TFirstParam, TResult>>(newSecond, param);
}

这使用以下方法将一个表达式的所有实例替换为另一个:

public static Expression Replace(this Expression expression,
    Expression searchEx, Expression replaceEx)
{
    return new ReplaceVisitor(searchEx, replaceEx).Visit(expression);
}
internal class ReplaceVisitor : ExpressionVisitor
{
    private readonly Expression from, to;
    public ReplaceVisitor(Expression from, Expression to)
    {
        this.from = from;
        this.to = to;
    }
    public override Expression Visit(Expression node)
    {
        return node == from ? to : base.Visit(node);
    }
}

现在你可以写:

public static IEnumerable<TSource> SafeFilter<TSource, TKey>
    (this IQueryable<TSource> source,
    Expression<Func<TSource, TKey>> keySelector,
    HashSet<TKey> filterSet,
    int threshold = 500)
{
    if (filterSet.Count > threshold)
    {
        var selector = keySelector.Compile();
        return source.AsEnumerable()
            .Where(x => filterSet.Contains(selector(x))); //In memory
    }
    return source.Where(keySelector.Compose(
        key => filterSet.AsEnumerable().Contains(key)));     //In SQL
}

另外,如果您的过滤器设置足够大,除了将整个集合放入内存之外,还有另一个选项。您可以做的是将过滤器集合分成批次,从数据库中获取每个批次并组合结果。这解决了IN子句中最大项数的限制,同时仍然允许在数据库端完成工作。它可能会或可能不会更好,具体取决于数据的具体情况,但它是另一个需要考虑的选择:

public static IEnumerable<TSource> SafeFilter<TSource, TKey>
    (this IQueryable<TSource> source,
    Expression<Func<TSource, TKey>> keySelector,
    HashSet<TKey> filterSet,
    int batchSize = 500)
{
    return filterSet.Batch(batchSize)
            .SelectMany(batch => source.Where(keySelector.Compose(
                key => batch.Contains(key))));
}