То, что вы здесь делаете, составляет одну функцию в другом. Для делегатов это легко, так как вы можете вызвать один, а затем передать результат как параметр другому. Для составления выражений несколько более активно; вам нужно заменить все экземпляры использования этого параметра выражением, которое оно создает. К счастью, вы можете извлечь эту логику в свой собственный метод:
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 за подробное объяснение и код. – Magnus