DynamicWhere.ex
DynamicWhere.exv3.4.0·docs

Error Codes Reference

Every validation failure in DynamicWhere.ex throws a LogicException (which inherits from Exception). The Message property carries one of the 31 stable error strings listed below, so you can pattern‑match them in middleware and surface meaningful problems to API callers.

How errors are raised

Validation runs clause by clause while the query is composed, not in a single pass before it. Most shapes — Filter, ConditionGroup, GroupBy, PageBy — are checked before their part of the query executes, but two entry points reach the database first: ToListDynamic and ToListAsyncDynamic run the COUNT query before validating Orders, Page and Selects. ToListAsync(Segment) combines its sets into one query and validates every clause before that query runs. The async overloads are async methods, so their exceptions surface at the await rather than at the call.

LogicException is the usual type, but not the only one a caller sees. A null argument raises ArgumentNullException; a guarded type raises PolicyException, which derives from LogicException; and input that passes validation but not the expression parser raises ParseException from System.Linq.Dynamic.Core.

Changed in 3.3.0: a malformed request is a LogicException
A null element inside Conditions, SubConditionGroups, ConditionSets, Orders or AggregateBy used to raise NullReferenceException, and a null or blank Selects entry used to raise ArgumentNullException from the name lookup — a five-hundred for a request that was simply malformed. They are ListOf[{list}]MustNotHasNullEntry and ConditionMustHasValidFieldName now. A ConditionSet whose ConditionGroup is null, and a null Summary.GroupBy, are still ArgumentNullException. See breaking point 45.
Changed in 3.3.0: no Number value reaches the parser
A DataType.Number value is read as the expression parser reads it, so one the parser cannot read — "1,000", "+5", "NaN" — or cannot compare with the member the condition names is InvalidFormat at validation, where it used to pass validation and throw ParseException when the query was built. See breaking point 44.
Surfacing errors in an API
Wrap the call in a try / catch (LogicException ex) and map it to a 400 Bad Request with the message as the validation reason. Other exception types should bubble up as 500s.
app.MapPost("/customers/search", async (Filter filter, AppDbContext db) =>
{
    try
    {
        var result = await db.Customers.ToListAsync(filter);
        return Results.Ok(result);
    }
    catch (LogicException ex)
    {
        // ex.Message is one of the stable error codes below
        return Results.BadRequest(new { error = ex.Message });
    }
});

All 31 error codes

Codes wrapped in (parens) are parameterized — the bracketed token in the message is replaced at runtime with the offending operator, alias, aggregator, type, or field name.

One failure still carries a literal message rather than one of these codes: Unsupported combination of DataType '{type}' and Operator '{op}'. for a pair the predicate builder has no form for. It is the only one left — the Select projection refusal became the code SelectTypeMustHaveParameterlessConstructor in 3.1.0.

Changed in 3.1.0
A Select<T> or Filter.Selects whose T has no parameterless constructor used to throw the English sentence Select projection requires a parameterless constructor on type '{T}'. Its Message is now the stable code SelectTypeMustHaveParameterlessConstructor, and the type's name moved to a new property on the exception, LogicException.Subject (string?). Any middleware matching on that old sentence — or reading the type name out of it — needs updating. See breaking changes.
New in 3.1.0: AmbiguousDateFormat
A Date or DateTime value that leads with a day or a month — 01/09/2026, 09/15/2026 — is refused with its own code rather than read one way or the other, and the field is on Subject. Its fix differs from InvalidFormat's: the value is a date, so either the client sends ISO 8601 or the deployment declares the order it uses through DwDates.Configure. See date formats and breaking changes.
New in 3.1.0: StartsWithReservedName
A field path whose first segment is one of the expression parser's own words — new, iif, np, isnull, is, as, cast, true, false, null, in any letter case — is refused with FieldPath[{path}]StartsWithReservedName and that segment on Subject. The parser reads its own functions and literals before it looks for a member, so the path never reached the member: before 3.1.0 seven of the names raised ParseException, True and False an InvalidOperationException, and Null was read as the null literal, so the query returned no rows and no error. See breaking changes.
Changed in 3.1.0: an unknown field under a strict policy
On a query guarded by ApplyPolicy under the Strict tier, outside a dry run, a field path that names nothing on the type no longer raises ConditionMustHasValidFieldName. It is refused the way a field denied for every feature is: a PolicyException with the code of the clause it appeared in — FieldDeniedForWhere … FieldDeniedForSegment — and FieldPath "*", so the answer does not say whether the field exists. Unguarded queries, the convenience tier and a dry run still raise ConditionMustHasValidFieldName. See what a strict refusal says and breaking changes.
Error CodeMessageTriggered When
SetsUniqueSortListOfConditionsSetsMustHasUniqueSortValueDuplicate Sort in ConditionSets
ConditionsUniqueSortAnyListOfConditionsMustHasUniqueSortValueDuplicate Sort in Conditions
SubConditionsGroupsUniqueSortAnyListOfSubConditionsGroupsMustHasUniqueSortValueDuplicate Sort in SubConditionGroups
RequiredIntersectionConditionsSetOfIndex[1-N]MustHasIntersectionMissing Intersection on set index 1+
InvalidFieldConditionMustHasValidFieldNameEmpty or invalid field name. On a strict-tier guarded query an unknown name is a PolicyException instead
StartsWithReservedName(path)FieldPath[{path}]StartsWithReservedNameA field path whose first segment is one of the expression parser's own words — new, iif, np, isnull, is, as, cast, true, false, null, in any letter case. Raised wherever a path is validated, so a condition Field, Orders, Selects, GroupBy.Fields, AggregateBy.Field and the member a [DwAlias] stands for all answer alike. A DefaultOrder entry naming one is skipped, and reported by the startup scan. Only the first segment counts: Owner.New names the member. The segment, trimmed, is on Subject. On a strict-tier guarded query it arrives as that clause's FieldDeniedFor* instead
InvalidValueConditionValuesAreNullOrWhiteSpaceDefined but never thrown — a null value normalizes to an empty string, which Text and Enum accept and every other DataType rejects with InvalidFormat
RequiredValuesConditionWithOperator[In-IIn-NotIn-INotIn]MustHasOneOrMoreValuesIn / NotIn with 0 values
NotRequiredValuesConditionWithOperator[IsNull-IsNotNull]MustHasNoValuesIsNull / IsNotNull with values
RequiredTwoValueConditionWithOperator[Between-NotBetween]MustHasOnlyTwoValuesBetween without exactly 2 values
RequiredOneValue(op)ConditionWithOperator[{op}]MustHasOnlyOneValueSingle‑value operator with wrong count
InvalidPageNumberPageNumberMustBeGreaterThanZeroPageNumber ≤ 0
InvalidPageSizePageSizeMustBeGreaterThanZeroPageSize ≤ 0
MustHaveFieldsMustHasFieldsEmpty fields list in Select
InvalidFormatInvalidFormatValue doesn't parse for declared DataType. A Number value must be a literal the expression parser reads — invariant, no thousands separator, no leading plus, no NaN — and one it can compare with the member the condition names (3.3.0). A Date / DateTime value must be ISO 8601, year-first, or a format declared through DwDates.Configure, read as the member's own date type — the same reading at validation and when the predicate is built — so 12:00, 1/9 or Sep 2026 raises this on every server. Raised by a date value, it carries the field on Subject
AmbiguousDateFormatAmbiguousDateFormatA Date / DateTime value leads with a day or a month — 01/09/2026, 15/09/2026, 09/15/2026, 01.09.2026, 1/9/26, with or without a time — and no format declared through DwDates.Configure reads it. Refused by shape, whatever the numbers. The field, or the Having alias, is on Subject
InvalidAliasAggregationMustHasValidAliasAlias is not a plain identifier — empty, starting with a digit, or carrying any character that is not a letter, digit, or underscore
GroupByMustHaveFieldsGroupByMustHasAtLeastOneFieldGroupBy with no fields
GroupByFieldsMustBeUniqueGroupByFieldsMustBeUniqueDuplicate GroupBy fields
GroupByFieldCannotBeComplexTypeGroupByFieldCannotBeComplexTypeGroupBy field ends on a navigation or on a collection of entities
GroupByFieldCannotBeCollectionGroupByFieldCannotBeCollectionTypeGroupBy field ends on a collection of collections
AggregationFieldMustBeSimpleTypeAggregationFieldMustBeSimpleTypeAggregation field ends on a navigation or on a collection of entities
AggregationFieldCannotBeCollectionAggregationFieldCannotBeCollectionTypeAggregation field ends on a collection of collections
AggregationAliasesMustBeUniqueAggregationAliasesMustBeUniqueDuplicate aliases
AggregationAliasCannotBeGroupByField(alias)AggregationAlias[{alias}]CannotBeUsedInGroupByFieldsAlias clashes with a GroupBy field
UnsupportedAggregatorForType(agg, type)Aggregator[{agg}]IsNotSupportedForFieldType[{type}]Invalid aggregator for the field's type
SummaryOrderFieldMustExistInGroupByOrAggregate(f)SummaryOrderField[{f}]MustExistInGroupByFieldsOrAggregateByAliasesOrder on a non‑grouped, non‑aggregated field
HavingFieldMustExistInAggregateByAlias(f)HavingField[{f}]MustExistInAggregateByAliasesHaving references an unknown alias
OrderFieldCannotEndOnComplexCollection(f)OrderField[{f}]CannotEndOnCollectionOfComplexElementsOrder path ends on a collection of entities — sort by a scalar inside it
SelectTypeMustHaveParameterlessConstructorSelectTypeMustHaveParameterlessConstructorSelect<T> or Filter.Selects on a T the projection cannot construct — a positional record, most often. The type's name is on Subject, not in the message. Also reached by a typed guarded query whose policy denies a field for Select — since 3.2.0 whatever the field holds, and beneath a member where its value can reach the result — because the deny synthesizes a projection
NullEntry(list)ListOf[{list}]MustNotHasNullEntryA list of the request shape holds a null entry — Conditions, SubConditionGroups, ConditionSets, Orders or AggregateBy, spelled as the shape declares it. New in 3.3.0: such an entry used to surface as a NullReferenceException from wherever it was first touched. See breaking point 45
Why messages and not numeric codes?
The messages are stable across versions and self‑documenting, which keeps client error handling readable. If you need numeric codes for i18n, map them in your API layer using the Error Code column as the key — the Error Code names are also stable.

LogicException.Subject

LogicException gained a second constructor in 3.1.0 — LogicException(string message, string? subject) — and the matching read-only property Subject (string?). It carries what a refusal is about where the code alone does not say: the rejected type's Name on SelectTypeMustHaveParameterlessConstructor, the field — or the Having alias — on AmbiguousDateFormat and on an InvalidFormat raised by a Date or DateTime value, and the offending first segment, trimmed, on FieldPath[{path}]StartsWithReservedName. Under ApplyPolicy the field is named as the caller wrote it, so a [DwAlias] name is never swapped for the member it hides. It is null for every other code, including InvalidFormat on a Guid, Number or Boolean value; the parameterized codes already interpolate their operator, alias, aggregator, type, or field name into the message themselves.

Keeping the type name out of the message is the point: a code that carried it would be a different string on every type, and neither your middleware nor an error envelope could match on it.

catch (LogicException ex)
{
    // ex.Message   -> "SelectTypeMustHaveParameterlessConstructor"
    // ex.Subject   -> "CustomerRow"
    //
    // ex.Message   -> "AmbiguousDateFormat"
    // ex.Subject   -> "CreatedAt"
    //
    // null for most other codes
    return Results.BadRequest(new { error = ex.Message, subject = ex.Subject });
}

Each error is raised by a specific validation entry point. Follow the link for the full validation rules and the exact shape that triggers each code:

  • Condition validation → InvalidField (also raised for any other blank or unresolvable field path — OrderBy, GroupBy, AggregateBy, Having, Summary.Orders), RequiredValues, NotRequiredValues, RequiredTwoValue, RequiredOneValue(op), InvalidFormat, AmbiguousDateFormat (the two format codes are also raised for a date value in a Having condition), StartsWithReservedName(path) (also raised for any other field path whose first segment is one of the parser's own words — OrderBy, Selects, GroupBy, AggregateBy).
  • ConditionGroup validation → ConditionsUniqueSort, SubConditionsGroupsUniqueSort.
  • Page validation → InvalidPageNumber, InvalidPageSize.
  • GroupBy validation → GroupByMustHaveFields, GroupByFieldsMustBeUnique, GroupByFieldCannotBeComplexType, GroupByFieldCannotBeCollection, and the AggregateBy codes InvalidAlias, AggregationFieldMustBeSimpleType, AggregationFieldCannotBeCollection, AggregationAliasesMustBeUnique, AggregationAliasCannotBeGroupByField(alias), UnsupportedAggregatorForType(agg, type) — all reachable through Group as well as through Summary.
  • Summary validation → SummaryOrderFieldMustExistInGroupByOrAggregate(f), HavingFieldMustExistInAggregateByAlias(f), plus every GroupBy code above from the nested GroupBy.
  • Segment validation → SetsUniqueSort and RequiredIntersection, plus the ConditionGroup errors of each set and the Selects, OrderBy and Page errors of the segment itself.
  • Select / SelectDynamic → MustHaveFields, for an empty Select, SelectDynamic, or Filter.Selects list, and SelectTypeMustHaveParameterlessConstructor, for a typed Select<T> the projection cannot construct.
  • Order → OrderFieldCannotEndOnComplexCollection(f), when an order path ends on a collection of entities.

See also