Skip to content

Dynamic Filter Builder

In Part 1, Chapter 4, we introduced the Specification<T>.All identity element and the dynamic filter pattern. In this chapter, we evolve that pattern into an independent builder class. The builder composes optional filter conditions through a fluent API, cleanly encapsulating the filter logic for complex search screens.

  1. Pattern using All as the initial value - Understanding the identity property of Specification<T>.All
  2. Conditional &= chaining - Progressive composition after null/empty checks
  3. Filter Builder separation - Extracting filter composition logic into an independent class

Specification<T>.All is the identity element of the & operation. Since All & X = X, if no filters are applied, All is returned as-is, functioning as a full query.

var spec = Specification<Product>.All; // Initial value
if (!string.IsNullOrWhiteSpace(request.Name))
spec &= new ProductNameContainsSpec(request.Name);
if (!string.IsNullOrWhiteSpace(request.Category))
spec &= new ProductCategorySpec(request.Category);
return spec; // Returns All if no filters

Separating filter composition logic into a static method keeps Usecase code clean and allows filter logic to be tested independently.

public static class ProductFilterBuilder
{
public static Specification<Product> Build(SearchProductsRequest request)
{
var spec = Specification<Product>.All;
// Conditional &= chaining
return spec;
}
}
DynamicFilter/
├── Product.cs # Product record
├── SampleProducts.cs # Sample data
├── SearchProductsRequest.cs # Search request DTO
├── ProductFilterBuilder.cs # Dynamic filter builder
├── Specifications/
│ ├── ProductNameContainsSpec.cs # Name contains search
│ ├── ProductCategorySpec.cs # Category filter
│ ├── ProductPriceRangeSpec.cs # Price range
│ └── ProductInStockSpec.cs # In stock
└── Program.cs # Demo execution
Request StateBuild() Return ValueIsAllBehavior
No filtersAlltrueFull query
1 filterThat SpecfalseSingle filter
N filtersAnd compositionfalseComposite filter

null Fallback vs All Initial Value Comparison

Section titled “null Fallback vs All Initial Value Comparison”
Itemnull FallbackAll Initial Value
null checkRequired every timeNot needed
Composition syntaxspec = spec is not null ? spec & x : xspec &= x
Empty filter handlingSeparate branch neededAutomatic (returns All)

A: All’s IsSatisfiedBy() always returns true, so the overhead is negligible. Additionally, the & operator performs identity optimization (All & X = X), preventing unnecessary And wrapping.

Q2: Should the Filter Builder be an instance class instead of a static method?

Section titled “Q2: Should the Filter Builder be an instance class instead of a static method?”

A: If external dependencies (e.g., current user info, configuration values) are needed, it can be an instance class. However, for pure filter composition, a static method is sufficient.

Q3: Can |= (OR chaining) be used with the same pattern?

Section titled “Q3: Can |= (OR chaining) be used with the same pattern?”

A: All is the identity element for the & operation, but not for the | operation (All | X = All — OR-ing with a condition that satisfies everything always results in everything). A separate initial value strategy is needed for OR composition.


We’ve cleanly separated filter composition logic with a builder. But how can we ensure these Specifications express the correct business rules? The next chapter covers systematic testing strategies for Specifications.

Chapter 3: Testing Strategies