Sample app¶
A minimal ASP.NET Core 9 Web API showing how to wire Filtering.Net end-to-end against an EF Core 9 + PostgreSQL backend. The sample is structured as a feature catalog — each grouping in Filters/UserFilter.cs demonstrates one library feature.
Where to find it¶
samples/UserManagement.WebApi/ on GitHub.
What it demonstrates¶
| Feature | Where in the sample |
|---|---|
[GenerateFilter<T>] partial class + DI registration |
Filters/UserFilter.cs, Program.cs |
| Built-in primitive profiles (Int32, String, Bool, DateTime, Guid) | Filters/UserFilter.cs (Id, Age, IsActive, CreatedAt, ExternalId) |
[Map(Sortable = true, DefaultSortDirection = SortDir.Desc)] |
MapAge, MapCreatedAt |
[Map(Only = new[] { ... })] operator allow-list |
MapEmail (eq / contains / isNull only) |
Custom [FilterProfile<T>(BasedOn = ...)] with custom [FilterOperator] |
Filters/StringFilterPlus.cs — adds fuzzy and ilike to string columns |
EF.Functions.* inside an operator expression |
Filters/StringFilterPlus.cs — the ilike operator calls EF.Functions.ILike (Npgsql ILIKE) |
Typed-value JSON deserialization + JsonSerializerContext wiring |
Json/SampleJsonContext.cs, Program.cs AddFiltering(SampleJsonContext.Default) |
[InterceptValue] pre-validation hook |
MapEmail — lowercases the value via NormalizeEmail |
| Auto-emitted enum profile | MapStatus (the generator emits Filtering.Net.Generated.UserStatusFilter automatically) |
| Navigation path with friendly alias | MapDepartmentName — maps Department.Name as departmentName |
| Three controller endpoints | Controllers/UsersController.cs |
Endpoints¶
POST /users/search— validate + filter + page usingIQueryable<User>.ApplyPagedAsync(...).POST /users/validate— preview validation without hitting the database.POST /users/export— same filter shape but no paging cap (for "export-all" workflows).
Prerequisites¶
- .NET 9 SDK
- Docker (optional — only for the bundled
docker-compose.ymlPostgres container)
Run locally¶
Start Postgres:
Run the API (the host calls Database.MigrateAsync + a one-shot seed at startup, so the schema and demo data are populated automatically on first launch):
If you'd rather apply migrations explicitly before starting the API:
dotnet ef database update --project samples/UserManagement.WebApi
dotnet run --project samples/UserManagement.WebApi
The seeder lives in Data/DatabaseInitializer.cs and only runs when the Users table is empty — it inserts 4 departments (Engineering, Marketing, Support, Finance) and 10 users covering every UserStatus value, mixed IsActive, ages 23–52, and CreatedAt spread across Jan–Feb 2026.
Example requests¶
Substring match on Name (uses contains from the inherited built-in profile):
curl -X POST http://localhost:5000/users/search \
-H "Content-Type: application/json" \
-d '{
"where": { "field": "Name", "op": "contains", "value": "ali" },
"sort": [{ "field": "Name", "dir": 0 }],
"page": 1,
"pageSize": 10
}'
Custom fuzzy operator from StringFilterPlus (case-insensitive substring):
curl -X POST http://localhost:5000/users/search \
-H "Content-Type: application/json" \
-d '{ "where": { "field": "Name", "op": "fuzzy", "value": "ALI" } }'
Custom ilike operator from StringFilterPlus (SQL LIKE pattern via EF.Functions.ILike — Postgres-specific case-insensitive match). The caller supplies the LIKE pattern with % / _ wildcards:
curl -X POST http://localhost:5000/users/search \
-H "Content-Type: application/json" \
-d '{ "where": { "field": "Name", "op": "ilike", "value": "ali%" } }'
Aliased navigation path — targets the related Department.Name column under the friendly key departmentName:
curl -X POST http://localhost:5000/users/search \
-H "Content-Type: application/json" \
-d '{ "where": { "field": "departmentName", "op": "eq", "value": "Engineering" } }'
Enum match on the auto-emitted UserStatus profile:
curl -X POST http://localhost:5000/users/search \
-H "Content-Type: application/json" \
-d '{ "where": { "field": "Status", "op": "in", "value": ["Active", "Pending"] } }'
Operator-restriction violation — this fails validation because Email is restricted to eq / contains / isNull:
curl -X POST http://localhost:5000/users/validate \
-H "Content-Type: application/json" \
-d '{ "where": { "field": "Email", "op": "startsWith", "value": "alice" } }'
# 400 Bad Request — operator not allowed on Email
Project layout¶
samples/UserManagement.WebApi/
├── Models/ # User + Department EF entities (User carries a UserStatus enum)
├── Filters/
│ ├── UserFilter.cs # [GenerateFilter<User>] partial — feature-by-feature catalogue
│ └── StringFilterPlus.cs # custom [FilterProfile<string>] adding the fuzzy operator
├── Json/SampleJsonContext.cs # JsonSerializerContext for trim/AOT-clean typed-value deserialization
├── Data/
│ ├── AppDbContext.cs # EF Core context
│ └── DatabaseInitializer.cs # MigrateAsync + idempotent demo-data seeder
├── Migrations/ # EF Core migrations (InitialCreate)
├── Controllers/UsersController.cs
├── Program.cs # WebApplication setup + AddFiltering(SampleJsonContext.Default) + DatabaseInitializer.MigrateAndSeedAsync
├── appsettings.json
└── docker-compose.yml # Postgres container with named volume + healthcheck
Adapting to other databases¶
Swap UseNpgsql(...) in Program.cs for UseSqlServer, UseSqlite, etc. The generated filter is provider-agnostic — only the EF.Functions.* translation surface changes per provider, and the source generator's FN1007 allow-list expands automatically when EF Core is referenced.
Trim / AOT¶
The sample uses AddFiltering(SampleJsonContext.Default) rather than the parameterless overload so the typed-value deserialization in MapName (via the fuzzy operator) doesn't trigger IL2026 / IL3050 warnings under PublishAot=true. Filter classes whose properties only need element-extracted values (no custom operators with typed values, no [PropertyMap] overrides) don't emit the resolver-accepting constructor at all — the generator gates that emission per class.