【问题标题】:Using Expression<Func<TSource, TKey>> with IQueryable将 Expression<Func<TSource, TKey>> 与 IQueryable 一起使用
【发布时间】:2014-11-03 01:58:37
【问题描述】:

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

这就是我现在拥有的。

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 服务器的情况。我明白了:

方法 'System.Object DynamicInvoke(System.Object[])' 不支持 翻译成 SQL

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

【问题讨论】:

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


    【解决方案1】:

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

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

    【讨论】:

    • 非常感谢@Servy 的详尽解释和代码。
    猜你喜欢
    • 2012-08-14
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多