Skip to content

TcfOss.Filtering Packages

Filtering, sorting, paging, and export library for .NET and TypeScript.

Package Selection

Need Package
Wire-format DTOs only TcfOss.Filtering.Contracts
Apply filters to IQueryable<T> (no EF) TcfOss.Filtering.Linq
Async ToDataResultAsync, LIKE support TcfOss.Filtering.EntityFrameworkCore
Export to CSV TcfOss.Filtering.CsvOutput
Export to XLSX TcfOss.Filtering.XlsxOutput
Catch bad filter payloads in ASP.NET Core TcfOss.Filtering.AspNetCore
Build filters in C# code (Blazor / WPF) TcfOss.Filtering.Programmatic
Newtonsoft.Json filter deserialization TcfOss.Filtering.Contracts.Newtonsoft
TypeScript / frontend @tcfoss/filtering-contracts (npm)

Core Flow

Frontend  →  API  →  Contracts DTOs  →  Mapper  →  Linq filters  →  IQueryable  →  DataResult

Contracts

DataRequest is the primary wire-level request. DataResult<T> is always the response:

var request = new DataRequest
{
    Filter   = new SimpleFilter("name", FilterOperators.Contains, "smith"),
    Sorts    = [new SortComponent("name", SortDirections.Ascending)],
    Page     = 1,
    PageSize = 20,
};
// result.Data        — T[] for the current page
// result.TotalCount  — total rows matching the filter
// result.Page / result.PageSize

Filter types and all operator/constant wire values: references/contract-keys.md

Linq: Applying filters to IQueryable

Import exactly one queryable namespace per file to avoid extension ambiguity.

using TcfOss.Filtering.Linq.Queryable;

var mapper = new ReflectionFilterMapper<Employee>();
DataRequest request = mapper.ToDataRequest(dto);

DataResult<Employee> result = dbContext.Employees.ToDataResult(request);

Mappers (always use one—never pass raw client field names to Dynamic LINQ)

Mapper When to use
ReflectionFilterMapper<T> Validates field paths exist on T via reflection
DictFilterMapper Explicit field → value-parser dictionary
BasicFilterMapper Base class; subclass for custom logic

Access control on ReflectionFilterMapper<T>:

// Only these paths are accepted
var mapper = new ReflectionFilterMapper<Employee>(whitelist: ["Name", "Department", "HireDate"]);

// Block sensitive paths (* = direct child, ** = any descendant)
var mapper = new ReflectionFilterMapper<Employee>(blacklist: ["UserAccount.**"]);

Any disallowed field throws UnknownFieldException.

EF Core integration

using TcfOss.Filtering.EntityFrameworkCore.Queryable;

DataResult<Employee> result = await dbContext.Employees
    .ToDataResultAsync(request, cancellationToken);

// Dynamic field selection
DataResult<dynamic> result = await dbContext.Employees
    .ToDataResultAsync(dynamicDataRequest, cancellationToken);
  • Automatically uses EF-aware CustomParsingConfig—required for Like/NotLike operators.
  • Do not import both TcfOss.Filtering.Linq.Queryable and TcfOss.Filtering.EntityFrameworkCore.Queryable in the same file.

Exporting

Full API reference: references/exporting.md

// CSV, synchronous
Stream csv = dbContext.Employees.ToCsvStream(filter, sorts, maxRows: 10_000);

// XLSX, synchronous (mirrors CSV API)
Stream xlsx = dbContext.Employees.ToExcelStream(filter, sorts, maxRows: 10_000);

// Async streaming (large datasets, EF Core)
using TcfOss.Filtering.EntityFrameworkCore.Queryable;

Stream csv = await dbContext.Employees
    .ApplyFiltering(filter, new ValueManager())
    .ApplySorting(sorts)
    .AsAsyncEnumerable()
    .ToCsvStreamAsync(responseStream);

ASP.NET Core error handling

app.UseFilterDeserializationExceptionHandler();
// Returns 400 { "error": "..." } instead of 500 on bad filter payloads

Programmatic filters (Blazor / WPF / server-side construction)

Build strongly-typed filter objects in C# code, convert to Contracts wire models, then map to Linq. The intermediate step gives you the mapper's validation.

TypeScript / Frontend

import { FilterOperators, SortDirections } from '@tcfoss/filtering-contracts';
import type { DataRequest } from '@tcfoss/filtering-contracts';

const request: DataRequest = {
  filter: {
    filterType: 'simple',
    field: 'name',
    operator: FilterOperators.Contains,
    value: 'smith',
  },
  sorts: [{ field: 'name', direction: SortDirections.Ascending }],
  page: 1,
  pageSize: 20,
};

Optional Zod validation schemas are also exported from the package.

Security checklist

  • Always use a mapper—never expose raw client field names to Dynamic LINQ.
  • Use whitelist/blacklist on ReflectionFilterMapper to prevent field enumeration of sensitive properties.
  • Register UseFilterDeserializationExceptionHandler() to avoid leaking stack traces via 500 responses.
  • Like/NotLike (lk/nlk) require EF Core paths; they throw InvalidOperationException on non-EF paths.

Common mistakes

Mistake Fix
Import both Linq and EF queryable namespaces in the same file Use only one per file
Use lk/nlk without EF Core Use TcfOss.Filtering.EntityFrameworkCore.Queryable
Construct Linq filters directly from client input Use a mapper
Expect FilterDeserializationException to auto-return 400 Register UseFilterDeserializationExceptionHandler()
Newtonsoft.Json without the converter Register FilterConverter in JsonSerializerSettings