Способ создания переиспользуемых Linq фильтров (построителей предикатов для условия Where), которые можно применять для разных типов объектов. Поля объектов для фильтрации указываются с помощью MemberExpression.

Способ подходит для Entity Framework, включая Async операции.

Основная идея. Что такое переиспользуемый фильтр?

Например есть приказы:

class Order { 
	public DateTime Start { get; set; }
	public DateTime? End { get; set; }
}

Пусть нужно найти все приказы которые будут действовать в ближайшие 7 дней.

С помощью переиспользуемого построителя фильтра (если бы он был реализован) найти приказы можно так:

var ordersFiltred = orders
	.WhereOverlap(
		// с помощью MemberExpressions
		// указываем по каким полям производить поиск
		fromField: oo => oo.Start,
		toField: oo => oo.End,

		// указываем период поиска
		from: DateTime.Now,
		to: DateTime.Now.AddDays(7))
	.ToList();

Этот же WhereOverlap можно переиспользовать и применить к другому типу. Например, для поиска командировок:

class Trip { 
	public DateTime? From { get; set; }
	public DateTime? To { get; set; }
}
var tripsFiltred = trips
	.WhereOverlap(
		// с помощью MemberExpressions
		// указываем по каким полям производить поиск
		fromField: oo => oo.From,
		toField: oo => oo.To,

		from: DateTime.Now,
		to: DateTime.Now.AddDays(7))
	.ToList();

Приказы и командировки - это разные типы объектов, у них нет общего интерфейса, поля для поиска называются по-разному. И все таки для обоих типов (и приказов и командировок) применяется один переиспользуемый фильтр WhereOverlap.

Ниже описано как можно делать такие переиспользуемые построители предикатов.

Как сделать переиспользуемый фильтр

Выше было описано применение WhereOverlap, было бы логично показать его реализацию. Но для того, чтобы сделать WhereOverlap, нужна реализация операторов “И”, “ИЛИ”. Поэтому начнем с более простого примера.

Пусть есть выплаты и премии:

class Payout { 
	public decimal Total { get; set; }
	public bool UnderControl { get; set; }
}

class Premium {
	public decimal Sum { get; set; }
	public bool RequiresConfirmation { get; set; }
}

Сделаем переиспользуемый фильтр для поиска платежей больше определенной суммы:

class UnderControlPayFilter {
	readonly decimal Limit;
	public UnderControlPayFilter(decimal limit) {
		Limit = limit;
	}

	public Expression<Func<TEnt, bool>> Create<TEnt>(
		Expression<Func<TEnt, decimal>> sumField) {

		// GreaterOrEqual - нужно реализовать
		// GreaterOrEqual - это extension, который принимает
		//  - указание на поле (Expression sumField)
		//  - и значение с которым нужно сравнивать (Limit)
		return sumField.GreaterOrEqual(Limit);
	}
}

Пример использования UnderControlPayFilter фильтра:

// фильтр поиска платежей требующих дополнительного контроля
//
// конкретный предел (здесь 1000) можно вынести в настройки,
// а UnderControlPayFilter зарегистрировать в IoC-контейнере.
// Тогда можно централизовано (через найстройки приложения)
// управлять максимальным пределом
var underControlPayFilter = new UnderControlPayFilter(1000);


//
// Применение переиспользуемого фильтра для выплат

var payoutPredicate =
	underControlPayFilter.Create<Payout>(pp => pp.Total);

// здесь, для упрощения, payouts - это массив,
// в реальном приложении это может быть Entity Framework DbSet 
var payouts = new[] {
	new Payout{ Total = 100 },
	new Payout{ Total = 50, UnderControl = true },
	new Payout{ Total = 25.5m },
	new Payout{ Total = 1050.67m }
}
.AsQueryable()
.Where(payoutPredicate)
.ToList();


//
// Применение переиспользуемого фильтра для премий

var premiumPredicate =
	underControlPayFilter.Create<Premium>(pp => pp.Sum);

// здесь, для упрощения, premiums - это массив,
// в реальном приложении это может быть Entity Framework DbSet 
var premiums = new[] {
	new Premium{ Sum = 2000 },
	new Premium{ Sum = 50.08m },
	new Premium{ Sum = 25.5m, RequiresConfirmation = true },
	new Premium{ Sum = 1070.07m }
}
.AsQueryable()
.Where(premiumPredicate)
.ToList();

Все готово, осталось только реализовать GreaterOrEqual extension:

public static class MemberExpressionExtensions {
    public static Expression<Func<TEnt, bool>> GreaterOrEqual<TEnt, TProp>(
        this Expression<Func<TEnt, TProp>> field, TProp val)
            => Expression.Lambda<Func<TEnt, bool>>(
                Expression.GreaterThanOrEqual(field.Body, Expression.Constant(val, typeof(TProp))), 
                field.Parameters);
}

По аналогии можно реализовать extension-ы LessOrEqual, Equal, HasNoVal и другие.

Более сложные переиспользуемые фильтры с операторами “И” и “ИЛИ”

Пускай в выборку должны попадать не только платежи больше определенного предела, но и те, которые специально отмечены, как требующие дополнительного контроля.

Дополним UnderControlPayFilter:

class UnderControlPayFilter {
	readonly decimal Limit;
	public UnderControlPayFilter(decimal limit) {
		Limit = limit;
	}

	public Expression<Func<TEnt, bool>> Create<TEnt>(
		Expression<Func<TEnt, decimal>> sumField,
		Expression<Func<TEnt, bool>> controlMarkField) {

			// PredicateBuilder нужно реализовать (см. ниже)
			return PredicateBuilder.Or(
				sumField.GreaterOrEqual(Limit),
				controlMarkField.Equal(true));
	}
}

Пример использования:

// для выплат

var payoutPredicate =
	underControlPayFilter.Create<Payout>(
		sumField: pp => pp.Total,
		controlMarkField: pp => pp.UnderControl);

// для премий

var premiumPredicate = 
	underControlPayFilter.Create<Premium>(
		sumField: pp => pp.Sum,
		controlMarkField: pp => pp.RequiresConfirmation);

PredicateBuilder это “A universal PredicateBuilder” сделанный Pete Montgomery.

Заключение

Чтобы делать свои переиспользуемые фильтры, нужен только PredicateBuilder и MemberExpressionExtensions. Просто скопируйте их в свой проект. Переиспользуемые фильтры можно оформить как extension (как WhereOverlap), как статический хелпер или класс (как UnderControlPayFilter).

Я сделал парочку переиспользуемых фильтров - GitHub, NuGet (включает PredicateBuilder и MemberExpressionExtensions).

Самореклама

Делаю быстрый бесплатный редактор блок-схем и интеллект карт https://dgrm.net/