This library allows you to construct filtering expressions at run-time on the fly using fluent API and minimize boilerplate code such as null/empty checking and case ignoring.
Linq.PredicateBuilder can be useful when you have to fetch data from database using query based on search filter parameters. In such cases you usually need to create a lot of boilerplate code to check parameters against nulls, empty strings and trim starting/trailing whitespaces before including filtering conditions in query.
For this sample we will use Person
entity class and Filter
class containing search parameters
public class Person
{
public int Id { get; set; }
public string FirstName { get; set; }
public string LastName { get; set; }
public DateOnly? DateOfBirth { get; set; }
public Gender Gender { get; set; }
public string Comment { get; set; }
public IEnumerable<Person> Relatives{ get; set; }
}
public class Filter
{
public string FirstName { get; set; }
public string LastName { get; set; }
public string Comment { get; set; }
public DateTime? DateOfBirth { get; set; }
public Gender? Gender { get; set; }
public List<int> Ids { get; set; }
public bool? HasRelatives { get; set; }
}
We have a source of Persons
IQueryable<Persons> Persons { get; }
and Filter
class instance with parameters values
var filter = new Filter
{
FirstName = null,
LastName = "Brown",
Gender = Gender.Male,
Comment = string.Empty,
Ids = new List<int>(),
HasRelatives = null
};
So we can build a query from several predicate segments combined together.
var query = Persons.Build(_ => _
.Equals(x => x.FirstName, filter.FirstName) // filter.FirstName is null -> this segment will be ignored
.And.Equals(x => x.LastName, filter.LastName)
.And.Equals(x => x.Gender, filter.Gender)
.And.Contains(x => x.Comment, filter.Comment) // filter.Comment is empty -> this segment will be ignored
.And.In(x => x.Id, filter.Ids) // filter.Ids is empty -> this segment will be ignored
.And.Conditional(filter.HasRelatives == true).Where(x => x.Relatives.Any()) // filter.HasRelatives is null -> this segment will be ignored
.And.Conditional(filter.HasRelatives == false).Where(x => !x.Relatives.Any())); // filter.HasRelatives is null -> this segment will be ignored
Some of builder segments will be ignored because corresponding search parameters should not be used in the query. This query is equal to the following code:
var lastName = filter.LastName.ToLower();
var query = Persons.Where(x => x.LastName.ToLower().Equals(lastName) && x.Gender.Equals(filter.Gender));
You can combine predicates using logical operators AND and OR.
var query = Persons.Build(_ => _
.Equals(x => x.LastName, filter.LastName)
.And.Equals(x => x.Gender, filter.Gender));
var query = Persons.Build(_ => _
.Contains(x => x.FirstName, filter.FirstName)
.Or.Contains(x => x.LastName, filter.LastName));
You can also use logical negation operator NOT.
var query = Persons.Build(_ => _
.Not.Equals(x => x.LastName, filter.LastName)
.And.Not.Contains(x => x.Comment, filter.Comment));
You can't use AND and OR operators side by side because the precedence of those operators is not provided.
To mix ANDs and ORs or change the precedence of operators you can use Brackets
method with a nested builder.
var query = Persons.Build(_ => _
.Contains(x => x.Comment, filter.Comment)
.And.Brackets(b => b.Equals(x => x.FirstName, filter.FirstName).Or.Equals(x => x.LastName, filter.LastName)));
As you can see in the samples above, builder chain is divided into atomic logical segments connected with operators. Let's see how you can control query building depending on search filter parameter values.
Most of predicate methods have two mandatory parameters: property selector, filter parameter and one optional builder options.
The default builder options equal to IgnoreCase | IgnoreDefaultInputs | Trim
, which means that:
- if parameter type is string, the starting and trailing whitespaces will be removed from its value
- string operation will be performed case insensitive
- if parameter value equals to
default
,string.Empty
or empty collection then current segment will be ignored (skipped).
Builder parameters can be changed per segment or for whole builder.
You can control whether to ignore segment or not using Conditional
method before the segment. If parameter of
Conditional()
evaluates to false
the segment will be ignored (skipped).
var query = Persons.Build(_ => _
.Equals(x => x.LastName, filter.LastName)
.And.Conditional(boolean_expression).Where(x => x.DateOfBirth < new DateOnly(1990, 1, 1))); // this segment is controlled by .Conditional(boolean_expression)
To build predicates for nested collection you have Any
method that has collection selector and nested builder
for the collection. Following code shows the use case
var query = Persons.Build(_ => _
.Equals(x => x.LastName, filter.LastName)
.Or.Any(x => x.Relatives, b => b.Equals(x => x.LastName, filter.LastName)));
Method | Description |
---|---|
Equals(selector_expression, input_value) |
builds a predicate based on Equals() |
Contains(selector_expression, input_value) |
builds a predicate based on String.Contains() |
In(selector_expression, collection_input_value) |
builds a predicate based on Enumerable.Contains() |
Any(collection_selector_expression, nested_builder) |
builds a predicate for nested collections |
Where(predicate) |
uses the predicate from its parameter |
Where(predicate, input_value) |
conditional Where() method |
The input value of conditional Where
method is being passed into the predicate as a parameter,
and being checked against conditions to ignore the segment if needed:
.Where((x, parameter) => x.DateOfBirth >= parameter, filter.DateOfBirth)
In the samples above we created queries using Build
extension method. To build a predicate expression use
static QueryableBuilderExtensions.BuildPredicate
method.
var predicate = QueryableBuilderExtensions.BuildPredicate<Person>(_ => _
.Equals(x => x.LastName, filter.LastName)
.Or.Any(x => x.Relatives, b => b.Equals(x => x.LastName, filter.LastName)));
var query = Persons.Where(predicate ?? (x => true));