Simplify complex entity queries with a unified filtering solution.
[TOC]
The Filtering feature provides a flexible and powerful way to filter, sort, and paginate data through API requests. It allows clients to construct complex queries using a JSON-based filter model that gets translated into domain specifications and FindOptions on the server side. The translated filter model can easily be handled by the bITdevKit repositories.
Filtering is a consumer of two lower-level domain features:
- Domain Specifications for reusable criteria and named specifications
- Domain Repositories for query execution, includes, paging, and ordering
Its JSON-based filter payloads and converter conventions are also closely related to the shared infrastructure documented in Common Serialization.
graph LR
R[Client Request]-->|filter|E[API Endpoint]-->|filter|Q[QueryHandler or Service]-->|filter|P[Repository]
P-->|query|D[(Database)]
P-.->|Result_IEnumerable_T|R
Modern applications require complex data querying capabilities where clients need to:
- Filter data based on multiple conditions
- Combine different filter types (equality, ranges, text search, etc.)
- Sort results by multiple fields
- Include related entities (eager loading)
- Paginate results for better performance
- Handle nested entity relationships
- Support dynamic query building
Traditional REST APIs often struggle with these requirements, leading to:
- Multiple specialized endpoints for different query scenarios
- Complex URL parameters that are hard to maintain
- Limited query capabilities
- Poor reusability across different entity types
The Filtering feature solves these challenges by providing:
- Unified Query Interface
- Single, consistent way to express complex queries
- Works across different entity types
- Supports both simple and complex filtering scenarios
- No need to create custom endpoints for each query scenario╬
- Type-Safe Implementation
- Strongly-typed models for both client and server (Swagger)
- Compile-time validation of filter structures
- Clear contract between frontend and backend (FilterModel)
- Flexible Architecture
- Extensible design for more custom filter types [TODO]
- Support for additional domain-specific specifications
- Easy integration with existing repositories (FindOptions)
- Performance Optimization
- Built-in pagination support
- Efficient query building (Expressions)
- Optimized database access through specifications
- Data Grids, Tables and Lists
- Dynamic column filtering
- Multi-column sorting
- Server-side pagination
- Search Interfaces
- Full-text search across multiple fields
- Combined filters (date ranges, categories, status)
- Related entity filtering
- Lookup lists
- Dynamic data loading for select components
- Type-ahead/autocomplete requests
sequenceDiagram
participant C as Client
participant A as API Controller
participant H as Query Handler
participant R as Repository
participant S as SpecificationBuilder
participant O as OrderOptionBuilder
participant I as IncludeOptionBuilder
participant D as Database
C->>+A: HTTP Request with FilterModel
A->>+H: Send Query(FilterModel)
H->>+R: FindAllAsync(FilterModel)
par Build FindOptions
R->>+S: Build
S-->>-R: Specifications
R->>+O: Build
O-->>-R: OrderOptions
R->>+I: Build
I-->>-R: IncludeOptions
end
R->>+D: Execute Query (FindOptions)
D-->>-R: Raw Results
R-->>-H: ResultPaged
H-->>-A: Response
A-->>-C: HTTP Response (ResultPaged)
The following sections detail the implementation and usage of the Filtering feature, providing comprehensive examples and best practices for common scenarios.
{
"page": 1,
"pageSize": 10,
"filters": [
{
"field": "name",
"operator": "eq|neq|isnull|isnotnull|isempty|isnotempty|gt|gte|lt|lte|contains|doesnotcontain|startswith|doesnotstartwith|endswith|doesnotendwith|any|all|none",
"value": "any",
"logic": "and|or",
"customType": "none|fulltextsearch|daterange|daterelative|timerange|timerelative|numericrange|isnull|isnotnull|enumvalues|textin|textnotin|numericin|numericnotin|namedspecification|compositespecification",
"customParameters": {
"key": "value"
},
"specificationName": "name",
"specificationArguments": [],
"compositeSpecification": {
"nodes": []
}
}
],
"orderings": [
{
"field": "name",
"direction": "asc|desc"
}
],
"includes": [
"name"
]
}[ApiController][Route("api/[controller]")]
public class UsersController : ControllerBase
{
[HttpGet]
public async Task<ActionResult<ResultPaged<User>>> GetAll(
[FromQueryFilter] FilterModel filter)
{
// or: var filter await this.HttpContext.FromQueryFilterAsync();
var response = await mediator.Send(new UserFindAllQuery(filter)); // handler calls repository.FindAllResultPagedAsync(filter)
return Ok(response); // should ideally return a ResultPaged<UserModel> (mapped)
}
[HttpPost("search")]
public async Task<ActionResult<ResultPaged<User>>> Search(
[FromBodyFilter] FilterModel filter)
{
// or: var filter await this.HttpContext.FromBodyFilterAsync();
var response = await mediator.Send(new UserSearchQuery(filter)); // handler calls repository.FindAllResultPagedAsync(filter)
return Ok(response); // should ideally return a ResultPaged<UserModel> (mapped)
}
}app.MapGet("/api/users/search", async Task<Results<Ok<ResultPaged<User>>, NotFound>>
(HttpContext context, IMediator mediator, CancellationToken cancellationToken) =>
{
var filter = await context.FromQueryFilterAsync();
var response = await mediator.Send(
new UserSearchQuery(filter), cancellationToken); // handler calls repository.FindAllResultPagedAsync(filter)
return TypedResults.Ok(response); // should ideally return a ResultPaged<UserModel> (mapped)
}).WithFilterSchema(); // adds openapi schema for the filter modelapp.MapPost("/api/users/search", async Task<Results<Ok<ResultPaged<User>>, NotFound>>
(HttpContext context, IMediator mediator, CancellationToken cancellationToken) =>
{
var filter = await context.FromQueryFilterAsync();
var response = await mediator.Send(
new UserSearchQuery(filter), cancellationToken); // handler calls repository.FindAllResultPagedAsync(filter)
return TypedResults.Ok(response); // should ideally return a ResultPaged<UserModel> (mapped)
}).WithFilterSchema(true); // adds openapi schema for the filter modelpublic class UserQueryHandler : IRequestHandler<UserFindAllQuery, ResultPaged<User>>
{
private readonly IGenericReadOnlyRepository<User> repository;
public UserQueryHandler(IGenericReadOnlyRepository<User> repository)
{
this.repository = repository;
}
public async Task<ResultPaged<User>> Handle(
UserFindAllQuery query,
CancellationToken cancellationToken)
{
return await repository.FindAllResultAsync(
query.Filter,
cancellationToken: cancellationToken);
}
}Simple filter as URL parameters:
GET /api/core/cities?filter={"page":1,"pageSize":10,"filters":[{"field":"Name","operator":"eq","value":"Berlin"}]} HTTP/1.1
Accept: application/jsonURL-encoded for more complex filters:
URL-encode the filter JSON and put it into a
single query string parameter named filter.:
{
"page": 1,
"pageSize": 10,
"filters": [
{
"field": "name",
"operator": "eq",
"value": "John"
}
]
} // encoded to %7B%22page%22%3A1%2C%22pageSize%2....GET /api/users?filter=api/users?filter=%7B%22page%22%3A1%2C%22pageSize%22%3A10%2C%22filters%22%3A%5B%7B%22field%22%3A%22name%22%2C%22operator%22%3A%22eq%22%2C%22value%22%3A%22John%22%7D%5D%7D HTTP/1.1
Accept: application/jsonThe following considerations apply to HTTP GET requests:
- HTTP GET requests should be URL-encoded to prevent issues with special characters.
- HTTP GET requests size limits may apply, consider using POST for large filter models.
- HTTP GET requests parameters are visible in logs and browser history.
- HTTP GET requests should be kept short and readable for maintainability.
POST /api/users/search HTTP/1.1
Host: api.example.com
Content-Type: application/json
Accept: application/json
{
"page": 1,
"pageSize": 20,
"filters": [
{
"customType": "daterange",
"customParameters": {
"field": "createdAt",
"startDate": "2024-01-01T00:00:00Z",
"endDate": "2024-12-31T23:59:59Z",
"inclusive": true
}
},
{
"field": "department.name",
"operator": "eq",
"value": "Engineering",
"logic": "and"
}
],
"orderings": [
{
"field": "lastName",
"direction": "asc"
}
],
"includes": [
"department",
"assignments"
]
}The following considerations apply to HTTP POST requests:
- HTTP POST requests can handle larger payloads than GET requests.
- HTTP POST requests are more secure for sensitive data.
- HTTP POST requests can be used for complex filter models.
- HTTP POST requests are not cached by browsers.
{
"success": true,
"messages": [
"Data retrieved successfully"
],
"errors": [],
"value": [
{
"id": 1,
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"department": {
"id": 1,
"name": "Engineering"
}
}
],
"currentPage": 1,
"totalPages": 5,
"totalCount": 100,
"pageSize": 20,
"hasPreviousPage": false,
"hasNextPage": true
}{
"success": false,
"messages": [
"Failed to retrieve data"
],
"errors": [
{
"code": "INVALID_FILTER",
"message": "Invalid filter parameters provided"
}
],
"value": null,
"currentPage": 0,
"totalPages": 0,
"totalCount": 0,
"pageSize": 0,
"hasPreviousPage": false,
"hasNextPage": false
}success: Indicates if the request was successfulmessages: Array of informational or error messageserrors: Array of structured error objects when success is falsevalue: Collection of items for the current page
currentPage: Current page number (1-based)totalPages: Total number of pages availabletotalCount: Total number of items across all pagespageSize: Number of items per pagehasPreviousPage: Indicates if a previous page existshasNextPage: Indicates if a next page exists
- Request Method Selection
- Use GET for simple queries and basic filtering
- Use POST for complex filters or when URL length might be an issue
- Consider using POST when sending sensitive filter data
- Performance Considerations
- Keep page sizes reasonable (recommended: 10-50 items)
- Use includes selectively to prevent excessive data loading
- Consider adding indexes for commonly filtered fields
- Error Handling
- Always check the
successproperty in responses - Handle error messages appropriately in your client application
- Log error details for debugging purposes
- Security
- Validate all filter inputs server-side
- Implement appropriate rate limiting
- Consider adding pagination limits to prevent DOS attacks
Matches exact values
{
"field": "status",
"operator": "eq",
"value": "active"
}Matches values that are not equal
{
"field": "status",
"operator": "neq",
"value": "deleted"
}{
"field": "age",
"operator": "gt",
"value": 18
}{
"field": "price",
"operator": "gte",
"value": 100.00
}{
"field": "stock",
"operator": "lt",
"value": 10
}{
"field": "temperature",
"operator": "lte",
"value": 25.5
}{
"field": "description",
"operator": "contains",
"value": "premium"
}{
"field": "title",
"operator": "doesnotcontain",
"value": "test"
}{
"field": "email",
"operator": "startswith",
"value": "admin"
}{
"field": "code",
"operator": "doesnotstartwith",
"value": "TMP"
}{
"field": "filename",
"operator": "endswith",
"value": ".pdf"
}{
"field": "url",
"operator": "doesnotendwith",
"value": "/temp"
}{
"field": "deletedAt",
"operator": "isnull"
}{
"field": "email",
"operator": "isnotnull"
}{
"field": "notes",
"operator": "isempty"
}{
"field": "phoneNumber",
"operator": "isnotempty"
}Matches if any child element satisfies the condition
{
"field": "orders",
"operator": "any",
"value": {
"field": "total",
"operator": "gt",
"value": 1000
}
}Matches if all child elements satisfy the condition
{
"field": "orderItems",
"operator": "all",
"value": {
"field": "quantity",
"operator": "gt",
"value": 0
}
}Matches if no child elements satisfy the condition
{
"field": "reviews",
"operator": "none",
"value": {
"field": "rating",
"operator": "lt",
"value": 3
}
}Custom filter types provide more specialized filtering capabilities. They are used by setting the
customTypeproperty instead of using the standardoperator.
Filter entries within a specific date range
{
"customType": "daterange",
"customParameters": {
"field": "createdAt",
"startDate": "2024-01-01T00:00:00Z",
"endDate": "2024-12-31T23:59:59Z",
"inclusive": true
}
}Filter based on relative date periods
{
"customType": "daterelative",
"customParameters": {
"field": "lastLogin",
"unit": "day",
"amount": 7,
"direction": "past"
}
}Filter entries within a specific time range
{
"customType": "timerange",
"customParameters": {
"field": "shiftStart",
"startTime": "09:00:00",
"endTime": "17:00:00",
"inclusive": true
}
}Filter based on relative time periods
{
"customType": "timerelative",
"customParameters": {
"field": "lastActivity",
"unit": "hour",
"amount": 2,
"direction": "past"
}
}Search across multiple fields
{
"customType": "fulltextsearch",
"customParameters": {
"searchTerm": "important document",
"fields": [
"title",
"description",
"content"
]
}
}Match against a list of possible values
{
"customType": "textin",
"customParameters": {
"field": "status",
"values": "active;pending;review"
}
}Exclude matches from a list of values
{
"customType": "textnotin",
"customParameters": {
"field": "category",
"values": "archived;deleted;draft"
}
}Filter numbers within a range
{
"customType": "numericrange",
"customParameters": {
"field": "price",
"min": 10.00,
"max": 50.00,
"inclusive": true
}
}Match against a list of numeric values
{
"customType": "numericin",
"customParameters": {
"field": "priority",
"values": "1;2;3"
}
}Exclude specific numeric values
{
"customType": "numericnotin",
"customParameters": {
"field": "errorCode",
"values": "404;500;503"
}
}Filter by enum values using names or integers
{
"customType": "enumvalues",
"customParameters": {
"field": "status",
"values": "Active;Pending"
}
}Explicit null check filter
{
"customType": "isnull",
"customParameters": {
"field": "canceledAt"
}
}Explicit non-null check filter
{
"customType": "isnotnull",
"customParameters": {
"field": "completedAt"
}
}Use pre-registered domain specifications
For the underlying specification model itself, including ISpecification<T>, composition, and built-in uniqueness specifications, see Domain Specifications.
{
"customType": "namedspecification",
"specificationName": "IsActive",
"specificationArguments": []
}Combine multiple specifications with logical operators
{
"customType": "compositespecification",
"compositeSpecification": {
"nodes": [
{
"name": "IsActive",
"arguments": []
},
{
"logic": "and",
"nodes": [
{
"name": "HasValidLicense",
"arguments": []
},
{
"name": "IsInRegion",
"arguments": [
"EU"
]
}
]
}
]
}
}Complex filters allow you to create sophisticated queries by combining different filter types, using nested conditions, and applying custom filter types. They are particularly useful when simple equality or comparison filters aren't sufficient.
Useful for finding records within a specific date range that match certain status criteria.
{
"page": 1,
"pageSize": 20,
"filters": [
{
"customType": "daterange",
"customParameters": {
"field": "createdAt",
"startDate": "2024-01-01T00:00:00Z",
"endDate": "2024-12-31T23:59:59Z",
"inclusive": true
}
},
{
"field": "status",
"operator": "eq",
"value": "active",
"logic": "and"
}
]
}Use Case: Finding all active users who registered during 2024.
Perfect for implementing search functionality across multiple text fields.
{
"filters": [
{
"customType": "fulltextsearch",
"customParameters": {
"searchTerm": "project management",
"fields": [
"title",
"description",
"skills",
"notes"
]
}
}
]
}Use Case: Searching for employees with specific skills or experience across their profile data.
Useful when you need to filter based on related entity properties.
{
"filters": [
{
"field": "department.name",
"operator": "eq",
"value": "Engineering",
"logic": "and"
},
{
"field": "projects",
"operator": "any",
"value": {
"field": "status",
"operator": "eq",
"value": "Active"
}
}
],
"includes": [
"department",
"projects"
]
}Use Case: Finding engineers who are assigned to active projects.
Combines multiple date-based filters for temporal analysis.
{
"filters": [
{
"customType": "daterange",
"customParameters": {
"field": "hireDate",
"startDate": "2023-01-01T00:00:00Z",
"endDate": "2023-12-31T23:59:59Z",
"inclusive": true
}
},
{
"customType": "daterelative",
"customParameters": {
"field": "lastActivity",
"unit": "day",
"amount": 30,
"direction": "past"
},
"logic": "and"
}
]
}Use Case: Finding employees hired in 2023 who have been active in the last 30 days.
Useful for financial or metric-based filtering.
{
"filters": [
{
"customType": "numericrange",
"customParameters": {
"field": "salary",
"min": 50000,
"max": 100000
}
},
{
"field": "performance.rating",
"operator": "gte",
"value": 4,
"logic": "and"
},
{
"field": "projects",
"operator": "any",
"value": {
"field": "budget",
"operator": "gt",
"value": 100000
},
"logic": "and"
}
]
}Use Case: Finding high-performing employees within a specific salary range working on high-budget projects.
Useful for scheduling and availability queries.
{
"filters": [
{
"customType": "timerange",
"customParameters": {
"field": "workingHours.start",
"startTime": "09:00:00",
"endTime": "17:00:00",
"inclusive": true
}
},
{
"field": "timezone",
"operator": "eq",
"value": "UTC+1",
"logic": "and"
}
]
}Use Case: Finding employees working during specific hours in a particular timezone.
Combines enum values with collection checks.
{
"filters": [
{
"customType": "enumvalues",
"customParameters": {
"field": "employmentType",
"values": "FullTime;PartTime"
}
},
{
"field": "skills",
"operator": "all",
"value": {
"customType": "enumvalues",
"customParameters": {
"field": "level",
"values": "Expert;Advanced"
}
},
"logic": "and"
}
]
}Use Case: Finding full-time or part-time employees who are experts in all their listed skills.
Useful for advanced text search scenarios.
{
"filters": [
{
"field": "email",
"operator": "endswith",
"value": "@company.com"
},
{
"customType": "textin",
"customParameters": {
"field": "department",
"values": "Engineering;Research;Development"
},
"logic": "and"
},
{
"field": "notes",
"operator": "contains",
"value": "leadership",
"logic": "and"
}
]
}Use Case: Finding internal employees from specific departments with leadership mentions in their notes.
{
"filters": [
{
"field": "teams",
"operator": "any",
"value": {
"field": "members",
"operator": "all",
"value": {
"field": "skills",
"operator": "any",
"value": {
"field": "level",
"operator": "gte",
"value": 3
}
}
}
},
{
"customType": "daterange",
"customParameters": {
"field": "projects.deadline",
"startDate": "2024-01-01T00:00:00Z",
"endDate": "2024-12-31T23:59:59Z",
"inclusive": true
},
"logic": "and"
}
],
"includes": [
"teams",
"teams.members",
"teams.members.skills",
"projects"
]
}Use Case: Finding teams where all members have advanced skills (level ≥ 3) and are working on projects due in 2024.
This appendix provides detailed information about using the Filtering feature in an Angular application.
This implementation provides a complete Angular solution including:
- Type-safe interfaces
- Reusable service layer
- Component implementation with pagination
- Error handling
- HTTP parameter building
// models/filter.model.ts
export interface FilterCriteria {
field: string;
operator: string;
value?: any;
logic?: 'and' | 'or';
customType?: string;
customParameters?: Record<string, any>;
}
export interface FilterModel {
page: number;
pageSize: number;
filters: FilterCriteria[];
orderings?: Array<{
field: string;
direction: 'asc' | 'desc';
}>;
includes?: string[];
}
export interface ResultPaged<T> {
items: T[];
totalCount: number;
pageNumber: number;
pageSize: number;
totalPages: number;
}// services/api.service.ts
import {Injectable} from '@angular/core';
import {HttpClient, HttpParams} from '@angular/common/http';
import {Observable} from 'rxjs';
import {environment} from '../environments/environment';
import {FilterModel, ResultPaged} from '../models';
@Injectable({
providedIn: 'root'
})
export class ApiService<T> {
constructor(
private http: HttpClient,
private baseUrl: string
) {
}
// POST (body)
searchFiltered(filterModel: FilterModel): Observable<ResultPaged<T>> {
return this.http.post<ResultPaged<T>>(`${this.baseUrl}/search`, filterModel);
}
// GET (querystring)
getFiltered(filterModel: FilterModel): Observable<ResultPaged<T>> {
let params = new HttpParams()
.set('page', filterModel.page.toString())
.set('pageSize', filterModel.pageSize.toString());
filterModel.filters.forEach((filter, index) => {
params = params
.set(`filters[${index}].field`, filter.field)
.set(`filters[${index}].operator`, filter.operator);
if (filter.value !== undefined) {
params = params.set(`filters[${index}].value`, filter.value.toString());
}
if (filter.logic) {
params = params.set(`filters[${index}].logic`, filter.logic);
}
if (filter.customType) {
params = params.set(`filters[${index}].customType`, filter.customType);
if (filter.customParameters) {
Object.entries(filter.customParameters).forEach(([key, value]) => {
params = params.set(
`filters[${index}].customParameters.${key}`,
value.toString()
);
});
}
}
});
filterModel.orderings?.forEach((order, index) => {
params = params
.set(`orderings[${index}].field`, order.field)
.set(`orderings[${index}].direction`, order.direction);
});
filterModel.includes?.forEach((include, index) => {
params = params.set(`includes[${index}]`, include);
});
return this.http.get<ResultPaged<T>>(this.baseUrl, {params});
}
}// services/user.service.ts
import {Injectable} from '@angular/core';
import {HttpClient} from '@angular/common/http';
import {environment} from '../environments/environment';
import {User} from '../models';
import {ApiService} from './api.service';
@Injectable({
providedIn: 'root'
})
export class UserService extends ApiService<User> {
constructor(http: HttpClient) {
super(http, `${environment.apiBaseUrl}/api/users`);
}
}// components/user-list/user-list.component.ts
import {Component, OnInit} from '@angular/core';
import {UserService} from '../../services/user.service';
import {User, FilterModel, ResultPaged} from '../../models';
import {finalize} from 'rxjs/operators';
@Component({
selector: 'app-user-list',
template: `
<div class="filters">
<button (click)="applyDepartmentFilter('Engineering')">
Engineering Only
</button>
<button (click)="applyDateRangeFilter()">
Last 30 Days
</button>
</div>
<div *ngIf="loading">Loading...</div>
<div *ngIf="error" class="error">
{{ error }}
</div>
<table *ngIf="users">
<thead>
<tr>
<th>Name</th>
<th>Email</th>
<th>Department</th>
</tr>
</thead>
<tbody>
<tr *ngFor="let user of users.items">
<td>{{ user.firstName }} {{ user.lastName }}</td>
<td>{{ user.email }}</td>
<td>{{ user.department }}</td>
</tr>
</tbody>
</table>
<div class="pagination" *ngIf="users">
<button (click)="previousPage()" [disabled]="users.pageNumber === 1">
Previous
</button>
<span>Page {{ users.pageNumber }} of {{ users.totalPages }}</span>
<button (click)="nextPage()" [disabled]="users.pageNumber === users.totalPages">
Next
</button>
</div>
`
})
export class UserListComponent implements OnInit {
users: ResultPaged<User> | null = null;
loading = false;
error: string | null = null;
private currentFilter: FilterModel = {
page: 1,
pageSize: 10,
filters: []
};
constructor(private userService: UserService) {
}
ngOnInit() {
this.loadUsers();
}
loadUsers() {
this.loading = true;
this.error = null;
this.userService.getFiltered|searchFiltered(this.currentFilter)
.pipe(
finalize(() => this.loading = false)
)
.subscribe({
next: (result) => {
this.users = result;
},
error: (error) => {
this.error = 'Failed to load users. Please try again.';
console.error('Error loading users:', error);
}
});
}
applyDepartmentFilter(department: string) {
this.currentFilter = {
...this.currentFilter,
filters: [
{
field: 'department',
operator: 'eq',
value: department
}
]
};
this.loadUsers();
}
applyDateRangeFilter() {
const endDate = new Date();
const startDate = new Date();
startDate.setDate(startDate.getDate() - 30);
this.currentFilter = {
...this.currentFilter,
filters: [
{
customType: 'daterange',
customParameters: {
field: 'createdAt',
startDate: startDate.toISOString(),
endDate: endDate.toISOString(),
inclusive: true
}
}
]
};
this.loadUsers();
}
nextPage() {
if (this.users && this.currentFilter.page < this.users.totalPages) {
this.currentFilter.page++;
this.loadUsers();
}
}
previousPage() {
if (this.currentFilter.page > 1) {
this.currentFilter.page--;
this.loadUsers();
}
}
}graph TD
A[Client Request] -->|FilterModel JSON| B[API Controller]
B -->|FilterModel| C[Query Handler]
C -->|FilterModel| D[Repository FindAllAsync]
D -->|Build| E[SpecificationBuilder]
D -->|Build| F[OrderOptionBuilder]
D -->|Build| G[IncludeOptionBuilder]
E -->|Specifications| FO[FindOptions]
F -->|OrderOptions| FO
G -->|IncludeOptions| FO
FO -->|-| H[(Database Query)]
H -->|ResultPaged| I[Response]
Build a Filter Model using Fluent C# syntax.
Can be used in a Blazor or server side environment to construct complex filters.
var filterModel = FilterModelBuilder.For<PersonStub>()
.SetPaging(2, PageSize.Large) // Fluent paging setup
.AddFilter(p => p.Age, FilterOperator.GreaterThan, 25) // Age > 25
.AddFilter(p => p.FirstName, FilterOperator.Contains, "A") // FirstName contains "A"
.AddFilter(p => p.Locations,
FilterOperator.Any, b => b
.AddFilter(loc => loc.City, FilterOperator.Equal, "Berlin")
.AddFilter(loc => loc.PostalCode, FilterOperator.StartsWith, "100")) // Any location with City = New York or ZipCode starts with "100"
.AddCustomFilter(FilterCustomType.FullTextSearch)
.AddParameter("searchTerm", "John")
.AddParameter("fields", new[] { "FirstName", "LastName" }).Done()
.AddOrdering(p => p.LastName, OrderDirection.Descending) // Order by LastName Descending
.AddOrdering(p => p.FirstName, OrderDirection.Ascending) // Then order by FirstName Ascending
.AddInclude(p => p.Locations)
.Build();
filterModel.Page.ShouldBe(2);
filterModel.PageSize.ShouldBe((int)PageSize.Large);
// etc.The AddInclude method is available in two overloads to support different scenarios:
Use lambda expressions for compile-time safety and refactoring support:
var filterModel = FilterModelBuilder.For<Customer>()
.AddInclude(c => c.Orders) // Single navigation property
.AddInclude(c => c.Addresses) // Multiple includes
.Build();Use string paths for dynamic includes or nested navigation properties:
var filterModel = FilterModelBuilder.For<Customer>()
.AddInclude("Orders") // Simple property path
.AddInclude("Orders.OrderItems") // Nested navigation path
.AddInclude("Addresses.City") // Multiple levels deep
.Build();Both overloads support conditional inclusion using the condition parameter:
var includeOrders = true;
var includeAddresses = false;
var filterModel = FilterModelBuilder.For<Customer>()
.AddInclude(c => c.Orders, condition: includeOrders) // Will be included
.AddInclude("Addresses", condition: includeAddresses) // Will be skipped
.Build();Expression-Based (AddInclude(c => c.Property)):
- Provides compile-time safety and IntelliSense support
- Best for known, statically-defined relationships
- Automatically refactored when property names change
- Limited to direct property access (single level)
String-Based (AddInclude("Property.Nested")):
- More flexible for dynamic scenarios
- Supports deeply nested navigation paths
- Useful when property names come from configuration or user input
- Can specify complex paths like
"Orders.OrderItems.Product"
var filterModel = FilterModelBuilder.For<Order>()
.SetPaging(1, 20)
.AddFilter(o => o.Status, FilterOperator.Equal, "Shipped")
.AddInclude(o => o.Customer) // Type-safe
.AddInclude("Customer.Addresses") // Nested path
.AddInclude("OrderItems.Product") // Multi-level navigation
.AddInclude("OrderItems.Product.Category") // Deep navigation
.Build();The ThenInclude feature enables type-safe chaining of navigation properties for eager loading deeply nested entity graphs.
Reference Navigation (single related entity):
var filterModel = FilterModelBuilder.For<Customer>()
.AddInclude(c => c.BillingAddress)
.ThenInclude(a => a.City)
.ThenInclude(c => c.Country)
.Build();Collection Navigation (collection of related entities):
var filterModel = FilterModelBuilder.For<Customer>()
.AddInclude(c => c.Orders) // ICollection<Order>
.ThenInclude(o => o.OrderItems) // Lambda parameter is element type
.ThenInclude(i => i.Product)
.Build();Supports all common collection types: IEnumerable<T>, ICollection<T>, IReadOnlyCollection<T>, IReadOnlyList<T>, List<T>.
var filterModel = FilterModelBuilder.For<Order>()
.AddInclude(o => o.ShippingAddress)
.ThenInclude(a => a.City)
.AddInclude(o => o.OrderItems)
.ThenInclude(i => i.Product)
.AddInclude(o => o.Customer)
.ThenInclude(c => c.BillingAddress)
.Build();var filterModel = FilterModelBuilder.For<Product>()
.AddInclude(p => p.Category)
.ThenInclude(c => c.ParentCategory, condition: includeDetails)
.Build();When condition: false, all subsequent ThenIncludes in that chain are skipped.
ThenInclude works seamlessly with other builder methods:
var filterModel = FilterModelBuilder.For<Customer>()
.AddFilter(c => c.IsActive, FilterOperator.Equal, true)
.AddInclude(c => c.Orders)
.ThenInclude(o => o.OrderItems)
.AddOrdering(c => c.LastName, OrderDirection.Ascending)
.SetPaging(1, 25)
.Build();var filterModel = FilterModelBuilder.For<Order>()
.AddFilter(o => o.Status, FilterOperator.Equal, OrderStatus.Active)
.AddInclude(o => o.Customer)
.ThenInclude(c => c.BillingAddress)
.ThenInclude(a => a.City)
.AddInclude(o => o.OrderItems)
.ThenInclude(i => i.Product)
.ThenInclude(p => p.Category)
.AddOrdering(o => o.OrderDate, OrderDirection.Descending)
.SetPaging(1, 20)
.Build();This Filtering feature described here is designed to provide a pragmatic, flexible filtering solution for REST APIs and Repositories.
It is not intended to replace or compete with comprehensive query technologies like:
- GraphQL: A complete query language that provides a type system and allows clients to specify exactly what data they need
- OData: A standardized protocol for building and consuming RESTful APIs with rich query capabilities
- When already using the bITdevKit ecosystem, providing seamless integration with its repository and specification patterns
- For REST APIs needing structured filtering
- When requiring a balance between flexibility and simplicity
- Need for a typed, maintainable filtering solution without the overhead of implementing larger query frameworks
If the application requires complex schema definitions, introspection, or full query language capabilities, consider using GraphQL or OData instead. The Filtering feature focuses on providing a straightforward, typed approach to common filtering scenarios while maintaining REST principles and leveraging DevbITdevKitKit features.
Remember: Choose the simplest tool that meets your requirements. The feature provides a lightweight, code-based approach to handle filtering, while staying consistent with the bITdevKit philosophy of simple, effective solutions to common development problems.