Creating reusable Linq filters (predicate builders for Where) that can be applied to different types
A way to create reusable Linq filters (predicate builders for Where clauses) that can be applied to different types of objects. The fields of objects to be filtered are specified using MemberExpression.
The method is suitable for Entity Framework, including Async operations.
Main idea. What is a reusable filter?
For example, there are orders:
class Order {
public DateTime Start { get; set; }
public DateTime? End { get; set; }
}
You need to find all the orders that will be valid in the next 7 days.
Using a reusable filter builder (if it were implemented), you can find orders like this:
var ordersFiltred = orders
.WhereOverlap(
// using MemberExpressions
// set up which fields to search for
fromField: oo => oo.Start,
toField: oo => oo.End, // set up the search period
from: DateTime.Now,
to: DateTime.Now.AddDays(7))
.ToList();
The same WhereOverlap can be reused and applied to another type. For example, to search for business trips:
class Trip {
public DateTime? From { get; set; }
public DateTime? To { get; set; }
}var tripsFiltred = trips
.WhereOverlap(
// using MemberExpressions
// set up which fields to search for
fromField: oo => oo.From,
toField: oo => oo.To, // set up the search period
from: DateTime.Now,
to: DateTime.Now.AddDays(7))
.ToList();
Orders and business trips are different types of objects, they do not have a common interface, and the search fields are named differently. And yet, for types (both orders and business trips), there is one reusable WhereOverlap filter.
The following describes how you can make such reusable predicate builders.
How to make a reusable filter
The use of WhereOverlap was described above, it would be logical to show its implementation. But in order to make WhereOverlap, you need to implement the operators “AND”, “OR”. So let’s start with a simpler example.
For example there are payouts and premiums:
class Payout {
public decimal Total { get; set; }
public bool UnderControl { get; set; }
}class Premium {
public decimal Sum { get; set; }
public bool RequiresConfirmation { get; set; }
}
Let’s make a reusable filter for finding payments greater than a certain amount:
class UnderControlPayFilter {
readonly decimal Limit;
public UnderControlPayFilter(decimal limit) {
Limit = limit;
} public Expression<Func<TEnt, bool>> Create<TEnt>(
Expression<Func<TEnt, decimal>> sumField) { // GreaterOrEqual - need to be implemented
// GreaterOrEqual - is extension, which takes arguments
// - field designation(Expression sumField)
// - and the value to be compared with (Limit)
return sumField.GreaterOrEqual(Limit);
}
}
An example of using the UnderControlPayFilter filter:
// search filter for payments requiring additional control
//
// the specific limit (here 1000) can be taken out in the settings,
// and the UnderControlPayFilter can be registered
// in the IoC container.
// Then you can centrally (through the application settings)
// control the maximum limit
var underControlPayFilter = new UnderControlPayFilter(1000);//
// Applying a reusable filter for payoutsvar payoutPredicate =
underControlPayFilter.Create<Payout>(pp => pp.Total);// here, for simplicity, payouts is an array,
// in a real application this can be an 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();
//
// Applying a reusable filter for premiumsvar premiumPredicate =
underControlPayFilter.Create<Premium>(pp => pp.Sum);// here, for simplicity, payouts is an array,
// in a real application this can be an 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();
Everything is ready, it remains only to implement the 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); }
The same way, you can implement the extensions LessOrEqual, Equal, HasNoVal and others.
More complex reusable filters with AND and OR operators
For example you need to find payments greater than a certain amount and those specifically marked as requiring additional monitoring.
Lets modify 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 need to be implemented
return PredicateBuilder.Or(
sumField.GreaterOrEqual(Limit),
controlMarkField.Equal(true));
}
}
Usage example:
// for payouts
var payoutPredicate =
underControlPayFilter.Create<Payout>(
sumField: pp => pp.Total,
controlMarkField: pp => pp.UnderControl);// for premiums
var premiumPredicate =
underControlPayFilter.Create<Premium>(
sumField: pp => pp.Sum,
controlMarkField: pp => pp.RequiresConfirmation);
PredicateBuilder is “A universal PredicateBuilder” by Pete Montgomery.
Conclusion
To make your own reusable filters you only need PredicateBuilder and MemberExpressionExtensions. Just copy them to your project. Reusable filters can be styled as an extension (as WhereOverlap), as a static helper, or as a class (as UnderControlPayFilter).