DynamicWhere.ex
DynamicWhere.exv3.5.0·docs

Operator

Operator declares the comparison applied by a Condition. There are 28 operators total. Every operator that starts with I is the case-insensitive variant — both sides are normalized with .ToLower() before comparison. On PostgreSQL the six pattern variants can match with ILIKE instead, since 3.5.0 — see case-insensitive matching.

Required values at a glance

Each operator expects a specific number of entries in Condition.Values. Sending the wrong count throws a LogicException whose message is one of ConditionWithOperator[<op>]MustHasOnlyOneValue, ConditionWithOperator[Between-NotBetween]MustHasOnlyTwoValues, ConditionWithOperator[In-IIn-NotIn-INotIn]MustHasOneOrMoreValues, or ConditionWithOperator[IsNull-IsNotNull]MustHasNoValues. There is no error-code property — the string is the message.

Value countOperators
0IsNull, IsNotNull
1All equality, contains, starts-with, ends-with, and ordered comparisons (20 operators total — the 28 minus the two null checks, the two range checks, and the four In variants).
2 (exactly)Between, NotBetween
1+In, IIn, NotIn, INotIn

Equality

OperatorDescriptionRequired Values
EqualEquality (case-sensitive for text).1
IEqualEquality (case-insensitive).1
NotEqualInequality (case-sensitive).1
INotEqualInequality (case-insensitive).1

Contains / StartsWith / EndsWith

OperatorDescriptionRequired Values
ContainsText contains (case-sensitive).1
IContainsText contains (case-insensitive).1
NotContainsText does not contain (case-sensitive).1
INotContainsText does not contain (case-insensitive).1
StartsWithStarts with (case-sensitive).1
IStartsWithStarts with (case-insensitive).1
NotStartsWithDoes not start with (case-sensitive).1
INotStartsWithDoes not start with (case-insensitive).1
EndsWithEnds with (case-sensitive).1
IEndsWithEnds with (case-insensitive).1
NotEndsWithDoes not end with (case-sensitive).1
INotEndsWithDoes not end with (case-insensitive).1
Note
Case-insensitive I* operators emit .ToLower() on both sides of the comparison. On SQL Server this is typically free (default collations are case-insensitive). On case-sensitive collations (e.g. PostgreSQL with C locale) this still works but may sidestep an index. On PostgreSQL, TextMatching.ILike lets a pg_trgm index serve IContains, IStartsWith, IEndsWith and their negations.

In / NotIn (set membership)

OperatorDescriptionRequired Values
InValue is in the set (case-sensitive for text).1+
IInValue is in the set (case-insensitive).1+
NotInValue is not in the set (case-sensitive).1+
INotInValue is not in the set (case-insensitive).1+
Long lists
A list of up to 32 values is written as one chain of comparisons, and a longer one as a balanced tree of such chains, which returns the same rows. Before 3.1.0 every list was one chain, and a single condition carrying about seven hundred values could overflow the request thread's stack and end the process — see breaking changes. Under ApplyPolicy, MaxConditionValues (default 1000) bounds the values one condition may carry.

Ordered comparisons & ranges

OperatorDescriptionRequired Values
GreaterThanGreater than.1
GreaterThanOrEqualGreater than or equal.1
LessThanLess than.1
LessThanOrEqualLess than or equal.1
BetweenInclusive range — first value is the lower bound, second is the upper bound.2 (exactly)
NotBetweenOutside the inclusive range defined by the two values.2 (exactly)

Null checks

OperatorDescriptionRequired Values
IsNullProperty is NULL.0
IsNotNullProperty is NOT NULL.0
On a date member that cannot be null
With DataType.Date or DataType.DateTime, the library reads the member's type first. A non-nullable DateTime, DateTimeOffset or DateOnly of the entity itself can never be null, so IsNull answers the constant false — no rows — and IsNotNull the constant true — every row. On PostgreSQL that is WHERE FALSE and no predicate at all. Reached through a navigation, as in Approval.ApprovedAt, they test the navigation instead: IsNull matches the rows with no Approval. On a nullable date member they test the column as usual. See breaking changes.
Warning
Sending any value with IsNull / IsNotNull throws ConditionWithOperator[IsNull-IsNotNull]MustHasNoValues. Send an empty array: "values": [].

JSON examples

Range (Between):

{
  "sort": 1,
  "field": "Price",
  "dataType": "Number",
  "operator": "Between",
  "values": [100, 500]
}

Set membership (IIn):

{
  "sort": 1,
  "field": "Country",
  "dataType": "Text",
  "operator": "IIn",
  "values": ["USA", "Canada", "UK"]
}

Null check (IsNull):

{
  "sort": 1,
  "field": "DeletedAt",
  "dataType": "DateTime",
  "operator": "IsNull",
  "values": []
}

Case-insensitive matching on PostgreSQL

New in 3.5.0. By default the I* operators lower both sides, field.ToLower().Contains("value"), which a relational provider writes as lower(column) LIKE …. A pg_trgm GIN or GiST index on the column cannot serve that, and it can serve column ILIKE '%value%'. How the case-insensitive operators match is one choice for the whole process, TextMatching, in DynamicWhere.ex.Enums; no request carries it.

ValueMeaning
Lower (0)Both sides lowered. The default, unchanged, and what every provider and LINQ to objects can run.
ILike (1)PostgreSQL's ILIKE, through Npgsql's EF.Functions.ILike, for IContains, INotContains, IStartsWith, INotStartsWith, IEndsWith and INotEndsWith.

A deployment on PostgreSQL chooses it once, at startup:

using DynamicWhere.ex.Enums;
using DynamicWhere.ex.Source;

DwText.Configure(o => o.CaseInsensitive = TextMatching.ILike);

// Or from configuration. A key nothing answers to — "CaseInsensitiv" — refuses to start,
// and so does a single value where the section belongs: "DynamicWhere:Text": "ILike".
DwText.Configure(new DwTextOptions().Bind(configuration.GetSection("DynamicWhere:Text")));
{
  "DynamicWhere": {
    "Text": {
      "CaseInsensitive": "ILike"
    }
  }
}

With ILike chosen, a condition on Name with the value "ab" compiles on Npgsql to:

OperatorSQL
IContainsi."Name" IS NOT NULL AND i."Name" ILIKE '%ab%' ESCAPE '\'
INotContainsi."Name" IS NOT NULL AND NOT (i."Name" ILIKE '%ab%' ESCAPE '\')
IStartsWithi."Name" IS NOT NULL AND i."Name" ILIKE 'ab%' ESCAPE '\'
INotStartsWithi."Name" IS NOT NULL AND NOT (i."Name" ILIKE 'ab%' ESCAPE '\')
IEndsWithi."Name" IS NOT NULL AND i."Name" ILIKE '%ab' ESCAPE '\'
INotEndsWithi."Name" IS NOT NULL AND NOT (i."Name" ILIKE '%ab' ESCAPE '\')
  • The predicate is built and parsed exactly as before, and the parsed lambda is then rewritten into EF.Functions.ILike, with a backslash as the escape character. The value is the one the condition sent, trimmed and lowered, with every backslash, % and _ in it escaped, so it matches as text and never as a pattern. A negation keeps its NOT, and the null guard stays in front. getQueryString shows the ILIKE SQL.
  • IEqual, INotEqual, IIn and INotIn stay lower(column) = value, which an index on lower(column) serves, and a Having condition stays lowered, since an aggregate uses no index. The case-sensitive operators are not affected.
  • It applies wherever the library filters through Where: a Filter, every set of a Segment and a Summary's row filter, guarded or not. Only the predicate the library parsed is rewritten; one the caller composed with LINQ beneath it is left as written.
  • Only a query EF Core's own provider translates is rewritten. LINQ to objects, the IEnumerable<T> overloads and a provider that wraps EF Core's, such as LinqKit's AsExpandable(), keep lowering.
  • Against PostgreSQL 16 it returns the same rows as lowering, and as LINQ to objects, for all six operators over ASCII values carrying %, _, backslashes and quotes, through the plain, guarded and segment paths.
The choice is the whole process's
Every EF Core query in the process is rewritten, whatever database it reaches. A process that also queries another database through EF Core — SQLite in tests, say — must not choose ILike: such a query fails translation with InvalidOperationException rather than match wrongly.
Member (DynamicWhere.ex.Source)Description
DwText.Configure(Action<DwTextOptions>)Fills in a fresh DwTextOptions and configures it.
DwText.Configure(DwTextOptions)Checks and freezes the options and sets them for the process. Throws ArgumentNullException for null; ArgumentException for a value TextMatching does not define; InvalidOperationException when ILike is chosen and Npgsql.EntityFrameworkCore.PostgreSQL cannot be loaded — its EF.Functions.ILike is found by name, so the package references no provider and a deployment without it refuses to start — and InvalidOperationException on a later call asking for a different choice. A later call asking for the choice in force does nothing, so several hosts in one process, such as WebApplicationFactory hosts in one test run, can run the same startup. DwDates.Configure, by contrast, refuses every second call.
DwText.OptionsThe DwTextOptions in force. Frozen; Lower until a deployment configures otherwise.
DwText.IsConfiguredtrue once Configure has succeeded.
DwTextOptions.CaseInsensitiveTextMatching, Lower by default. Setting it once the options are frozen throws InvalidOperationException.
DwTextOptions.IsFrozentrue once the options have been handed to DwText.Configure.
DwTextOptions.Bind(IConfiguration)Extension method. Reads CaseInsensitive from a section and returns the same instance; an absent section changes nothing. Throws InvalidOperationException for a key nothing answers to, for a value it cannot read as a TextMatching such as a misspelt "ILikee", for a single value where the section belongs ("DynamicWhere:Text": "ILike"), or when the options are already frozen.