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.
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.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.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.
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.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, 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.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 Code | Message | Triggered When |
|---|---|---|
SetsUniqueSort | ListOfConditionsSetsMustHasUniqueSortValue | Duplicate Sort in ConditionSets |
ConditionsUniqueSort | AnyListOfConditionsMustHasUniqueSortValue | Duplicate Sort in Conditions |
SubConditionsGroupsUniqueSort | AnyListOfSubConditionsGroupsMustHasUniqueSortValue | Duplicate Sort in SubConditionGroups |
RequiredIntersection | ConditionsSetOfIndex[1-N]MustHasIntersection | Missing Intersection on set index 1+ |
InvalidField | ConditionMustHasValidFieldName | Empty or invalid field name. On a strict-tier guarded query an unknown name is a PolicyException instead |
StartsWithReservedName(path) | FieldPath[{path}]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. 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 |
InvalidValue | ConditionValuesAreNullOrWhiteSpace | Defined but never thrown — a null value normalizes to an empty string, which Text and Enum accept and every other DataType rejects with InvalidFormat |
RequiredValues | ConditionWithOperator[In-IIn-NotIn-INotIn]MustHasOneOrMoreValues | In / NotIn with 0 values |
NotRequiredValues | ConditionWithOperator[IsNull-IsNotNull]MustHasNoValues | IsNull / IsNotNull with values |
RequiredTwoValue | ConditionWithOperator[Between-NotBetween]MustHasOnlyTwoValues | Between without exactly 2 values |
RequiredOneValue(op) | ConditionWithOperator[{op}]MustHasOnlyOneValue | Single‑value operator with wrong count |
InvalidPageNumber | PageNumberMustBeGreaterThanZero | PageNumber ≤ 0 |
InvalidPageSize | PageSizeMustBeGreaterThanZero | PageSize ≤ 0 |
MustHaveFields | MustHasFields | Empty fields list in Select |
InvalidFormat | InvalidFormat | Value 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 |
AmbiguousDateFormat | AmbiguousDateFormat | A 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 |
InvalidAlias | AggregationMustHasValidAlias | Alias is not a plain identifier — empty, starting with a digit, or carrying any character that is not a letter, digit, or underscore |
GroupByMustHaveFields | GroupByMustHasAtLeastOneField | GroupBy with no fields |
GroupByFieldsMustBeUnique | GroupByFieldsMustBeUnique | Duplicate GroupBy fields |
GroupByFieldCannotBeComplexType | GroupByFieldCannotBeComplexType | GroupBy field ends on a navigation or on a collection of entities |
GroupByFieldCannotBeCollection | GroupByFieldCannotBeCollectionType | GroupBy field ends on a collection of collections |
AggregationFieldMustBeSimpleType | AggregationFieldMustBeSimpleType | Aggregation field ends on a navigation or on a collection of entities |
AggregationFieldCannotBeCollection | AggregationFieldCannotBeCollectionType | Aggregation field ends on a collection of collections |
AggregationAliasesMustBeUnique | AggregationAliasesMustBeUnique | Duplicate aliases |
AggregationAliasCannotBeGroupByField(alias) | AggregationAlias[{alias}]CannotBeUsedInGroupByFields | Alias clashes with a GroupBy field |
UnsupportedAggregatorForType(agg, type) | Aggregator[{agg}]IsNotSupportedForFieldType[{type}] | Invalid aggregator for the field's type |
SummaryOrderFieldMustExistInGroupByOrAggregate(f) | SummaryOrderField[{f}]MustExistInGroupByFieldsOrAggregateByAliases | Order on a non‑grouped, non‑aggregated field |
HavingFieldMustExistInAggregateByAlias(f) | HavingField[{f}]MustExistInAggregateByAliases | Having references an unknown alias |
OrderFieldCannotEndOnComplexCollection(f) | OrderField[{f}]CannotEndOnCollectionOfComplexElements | Order path ends on a collection of entities — sort by a scalar inside it |
SelectTypeMustHaveParameterlessConstructor | SelectTypeMustHaveParameterlessConstructor | Select<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}]MustNotHasNullEntry | A 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 |
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 });
}Where each error lives
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 aHavingcondition),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 theAggregateBycodesInvalidAlias,AggregationFieldMustBeSimpleType,AggregationFieldCannotBeCollection,AggregationAliasesMustBeUnique,AggregationAliasCannotBeGroupByField(alias),UnsupportedAggregatorForType(agg, type)— all reachable throughGroupas well as throughSummary. - Summary validation →
SummaryOrderFieldMustExistInGroupByOrAggregate(f),HavingFieldMustExistInAggregateByAlias(f), plus every GroupBy code above from the nestedGroupBy. - Segment validation →
SetsUniqueSortandRequiredIntersection, plus theConditionGrouperrors of each set and theSelects,OrderByandPageerrors of the segment itself. - Select / SelectDynamic →
MustHaveFields, for an emptySelect,SelectDynamic, orFilter.Selectslist, andSelectTypeMustHaveParameterlessConstructor, for a typedSelect<T>the projection cannot construct. - Order →
OrderFieldCannotEndOnComplexCollection(f), when an order path ends on a collection of entities.
See also
- Breaking Changes & Known Limitations → behaviour that is not an error but may surprise you.
- Filter → the most common entry point that triggers these validations.