Defining custom profiles + custom operators¶
What this does¶
A custom profile is a static class decorated with [FilterProfile<TColumn>]. It declares a set of operators that apply to columns of CLR type TColumn. Each operator is a public static property decorated with [FilterOperator("name")] that returns an Expression<Func<TColumn, TValue, bool>> (binary) or Expression<Func<TColumn, bool>> (unary). Setting BasedOn = typeof(BuiltinProfile) inherits every operator from the base profile so you only declare the additions.
When to use¶
- Provider-specific operators —
EF.Functions.ILikeon PostgreSQL, full-text-search calls on SQL Server. - Domain-specific operators —
withinDistance,fuzzy,containsTag, anything your application's vocabulary needs.
Minimal code¶
The sample app's StringFilterPlus inherits the built-in StringFilter and adds two operators:
using System.Linq.Expressions;
using Filtering.Net;
using Microsoft.EntityFrameworkCore;
// Inherits every operator from StringFilter (eq, ne, contains, startsWith, endsWith, in, isNull) via BasedOn,
// then adds two more.
[FilterProfile<string>(BasedOn = typeof(StringFilter))]
public static class StringFilterPlus
{
// Case-insensitive substring — String.Contains translates to a SQL LIKE on most providers.
[FilterOperator("fuzzy")]
public static Expression<Func<string, string, bool>> Fuzzy =>
(column, value) => column.Contains(value.ToLower());
// EF.Functions.* inside a [FilterOperator] body — translates to PostgreSQL ILIKE under Npgsql.
[FilterOperator("ilike")]
public static Expression<Func<string, string, bool>> ILike =>
(column, pattern) => EF.Functions.ILike(column, pattern);
}
A [Map(nameof(User.Name), Profile = typeof(StringFilterPlus))] then makes fuzzy and ilike available on User.Name. The lambda body is inlined into the per-property Build method — there is no delegate dispatch at request time.
Variations¶
- Standalone profiles — omit
BasedOnand declare every operator from scratch. A standalone profile must also implement the JSON value extractors the generator calls (TryGetValue,TryGetArrayfor whichever value shapes the operators use), orFN0015fires. - Multiple profiles per CLR type — declaring more than one profile for the same
TColumnis allowed but makes built-in resolution ambiguous. Every[Map]for a property of that type must then specifyProfile = typeof(...)explicitly (the sample app'sStringFilterPlustriggers this contract forstring). - Unary operators — return
Expression<Func<TColumn, bool>>for operators likeisNullthat take no value.
Pitfalls¶
[FilterOperator]-decorated members must bepublic static, otherwiseFN0010fires.- The same operator name cannot appear twice on one profile —
FN0016flags duplicates (including duplicates that arise viaBasedOnif you re-declare an inherited operator with a different body). - Operator bodies that call methods outside EF Core's translatable allow-list emit
FN1007. The operator still compiles and runs, but the predicate may fall back to client-side evaluation or fail at query time depending on the provider.