2014-09-09 3 views
0

Я пытаюсь написать функцию, которая фильтрует источник данных IQueryable с использованием селектора ключей и коллекции либо в SQL, либо в памяти, если коллекция больше определенного порогового значения.Использование выражения <Func <TSource, TKey >> с 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<Func<TSource, TKey>>, но не знаете, как используй это. Любая помощь оценивается.

ответ

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

Большое спасибо @Servy за подробное объяснение и код. – Magnus

Смежные вопросы