Filter Transformation Extensions
FilterExtensions in TcfOss.Filtering.Contracts provides four helpers for working
with contract filter trees before mapping them to LINQ requests:
| Method | Purpose | Result |
|---|---|---|
Map |
Replace or remove nodes recursively | The transformed tree, or null |
Find |
Locate the first node matching a predicate | The matching node, or null |
TryExtract<T> |
Extract one value and remove its corresponding node | The remaining tree, plus an out T? value |
Merge |
Combine two filters with a logical operator | The combined tree, or null |
Import the contract namespace for all examples on this page:
using TcfOss.Filtering.Contracts;
These helpers operate on contract DTOs, not TcfOss.Filtering.Linq filter models.
They do not validate field names, parse values, or execute queries. Continue to use
request mapping before applying a client request to a query.
Map: Transform or Remove Nodes
Filter? Map(this Filter? filter, Func<Filter, Filter?> transform)
Return a replacement filter from the callback to change a node, the node itself to
keep it, or null to remove it. For example, remove a UI-only condition:
Filter filter = new CompositeFilter(LogicalOperators.And,
[
new SimpleFilter("Name", FilterOperators.Contains, "smith"),
new SimpleFilter("PreviewOnly", FilterOperators.EqualTo, "true"),
]);
Filter? cleaned = filter.Map(node =>
node is SimpleFilter { Field: "PreviewOnly" } ? null : node);
// cleaned is the Name filter, not a one-child CompositeFilter.
Callbacks can also replace a node with a different filter:
Filter? rewritten = filter.Map(node =>
node is SimpleFilter { Field: "PreviewOnly" }
? new SimpleFilter("Status", FilterOperators.EqualTo, "Published")
: node);
Traversal and Simplification
Map visits children in array order before visiting their parent:
- A
nullinput returnsnullwithout invoking the callback. - For a
CompositeFilter, it maps the children and removesnullresults. With no children remaining it returnsnull; with one child it returns that child directly. In both cases, the callback is not called on the composite. With two or more children, the callback receives a copy of the composite with the transformed children. - For a
QuantifiedFilter, it mapsSubFilterfirst. If that becomesnull, the whole quantified filter is removed without invoking its callback. Otherwise, the callback receives a copy with the transformed subfilter. - Other filter types are passed directly to the callback.
SetFilterandRangeFilterare leaves; their values are not separate filter nodes.
Replacement trees returned by the callback are not traversed again or simplified
afterward. Even an identity mapping (node => node) can collapse existing empty
or one-child composites.
Find: Locate a Node
Filter? Find(this Filter? filter, Func<Filter, bool> predicate)
Find checks the current node first, then searches composite children in array
order or a quantified filter's subfilter. It stops at the first match and does
not transform or simplify the tree. A null input or no match returns null.
Filter filter = new CompositeFilter(LogicalOperators.And,
[
new SimpleFilter("Name", FilterOperators.Contains, "smith"),
new QuantifiedFilter
{
Field = "Tags",
Operator = QuantifiedOperators.Any,
SubFilter = new SimpleFilter("Value", FilterOperators.EqualTo, "featured"),
},
]);
Filter? found = filter.Find(node =>
node is SimpleFilter { Field: "Value" });
// found is the Value filter inside the Tags quantified filter.
Unlike Map, Find searches parent-before-child. A predicate matching the root
composite returns that composite immediately rather than searching its children.
TryExtract: Remove One Node and Return a Value
Filter? TryExtract<T>(
this Filter? filter,
Func<Filter, T?> extractor,
out T? extracted) where T : class
The extractor returns a non-null reference value when a node should be extracted,
or null when it should remain. The value can be the matching filter itself or
another reference type such as a string or an application-specific object.
Filter filter = new CompositeFilter(LogicalOperators.And,
[
new SimpleFilter("SearchTerm", FilterOperators.Contains, "smith"),
new SimpleFilter("IsActive", FilterOperators.EqualTo, "true"),
]);
Filter? remaining = filter.TryExtract<SimpleFilter>(
node => node is SimpleFilter { Field: "SearchTerm" } simple ? simple : null,
out SimpleFilter? searchTerm);
// searchTerm is the SearchTerm filter; handle it separately.
// remaining is the IsActive filter after its composite is collapsed.
TryExtract uses Map, so it follows the same child-before-parent traversal and
simplification rules. Only the first non-null extracted value is retained; its
node is removed, and the extractor is not called again after that match. A parent
visited afterward sees the already-transformed children. Collapsed or removed
composites are never offered to the extractor.
If no value is extracted, extracted is null, but the returned tree can still
be simplified by Map. For a null input, both outputs are null.
Despite its name, the method returns the remaining filter, not a success boolean;
check extracted for a match. T must be a reference type, not a value type such as int.
Merge: Combine Filters
Filter? Merge(
this Filter? filter,
Filter? other,
string op = LogicalOperators.And)
By default, Merge combines two filters with AND. Supply LogicalOperators.Or
to combine alternatives instead:
Filter name = new SimpleFilter("Name", FilterOperators.Contains, "smith");
Filter status = new SimpleFilter("Status", FilterOperators.EqualTo, "Published");
Filter? both = name.Merge(status);
Filter? either = name.Merge(status, LogicalOperators.Or);
- If either argument is
null, the other is returned unchanged. Twonullarguments returnnull. - When an argument is a top-level composite using the requested operator, its children are included directly in the result. If both are matching composites, their child arrays are concatenated, left before right.
- Composites using a different operator stay nested to preserve their grouping.
- Flattening is only at the arguments' top level, not recursive. There is no
deduplication or general logical simplification, and
opis not validated. Use the logical operator constants.
For example, merging (A OR B) with C using the default operator produces
(A OR B) AND C, not A AND B AND C.