Breaking Changes & Known Limitations
DynamicWhere.ex is intentionally opinionated about how queries are shaped. The eleven points below cover constraints, surprises, and corner cases — read them before designing an API around the library so you can pick the right entry points and avoid runtime exceptions in production.
1. Parameterless Constructor Required for Select Projection
Select<T>(fields) requires T to have a parameterless (default) constructor. If T does not have one, a LogicException is thrown. Most EF Core entity classes have parameterless constructors by default.
Select<T> or for the typed Filter projection. Use SelectDynamic instead, or add an explicit parameterless constructor to your DTO.2. Segment Operations are Async-Only
ToListAsync<T>(Segment) is the only entry point for segment queries. There is no synchronous ToList<T>(Segment) variant. Each ConditionSet is materialized independently into memory, then set operations are performed in‑memory.
Segment and ToListAsyncSegment.3. Case-Insensitive Operators use .ToLower()
All I* operators (e.g., IContains, IEqual) normalize both sides via .ToLower(). This works correctly with SQL Server (COLLATE is typically case‑insensitive), but be aware of potential performance or behavior differences on case‑sensitive database collations (e.g., PostgreSQL with C locale).
LOWER(column) predicate, which can turn a fast seek into a table scan. If you target PostgreSQL with C locale, consider a functional index on LOWER(column) or use the case‑sensitive operator variants.4. Enum Filtering Requires String Storage
The Enum data type assumes enum values are stored as strings (not integers) in the database. If your database stores enums as integers, use DataType.Number instead.
.HasConversion<string>() if you want to filter with DataType.Enum, or leave it as the default integer mapping and filter with DataType.Number. Mismatched configurations either throw InvalidFormat at validation time or silently return zero rows.5. Having Clause Fields Reference Aliases, Not Entity Properties
In a Summary, the Having.ConditionGroup.Conditions[].Field must match an AggregateBy.Alias, not an entity property path.
Having condition that references an entity property directly (e.g., "UnitPrice" instead of the alias "AvgPrice") throws HavingFieldMustExistInAggregateByAlias. See the error code reference.6. GroupBy Flattens Dotted Field Names in Results
Dotted GroupBy fields (e.g., Category.Name) produce flattened alias keys in the dynamic result objects (e.g., CategoryName). Order fields in Summary.Orders should use the dotted form; the library handles alias mapping internally.
row.CategoryName — not row.Category.Name. When writing Summary.Orders entries, keep the dotted form ("Category.Name") and the library will map it to the flattened alias for you.7. Collection Navigation Auto-Wraps with .Any()
When a condition's Field path traverses a collection property, the library automatically inserts .Any() lambdas. This means the filter checks if any item in the collection matches — there is no built‑in .All() support.
.Any() condition or split into two filters. See the nested‑collection example.8. Thread-Safe Cache, But Configuration Changes are Eventually Consistent
CacheExpose.Configure() is thread‑safe, but already‑in‑progress operations may use the previous configuration until they complete.
9. getQueryString Parameter Requires EF Core Provider
Passing getQueryString: true to ToList / ToListAsync calls .ToQueryString() which requires an active EF Core database provider. It will fail on pure in‑memory IEnumerable<T> calls (use the IEnumerable overloads which internally call AsQueryable() first, but ToQueryString() may not be supported).
getQueryString when the source is a real DbSet<T> or an EF Core‑backed IQueryable<T>. On in‑memory collections you'll get a provider exception. Use it as a development aid, not as a production feature.10. SelectDynamic / FilterDynamic / ToListDynamic / ToListAsyncDynamic Return Non-Generic Types
These methods return IQueryable or FilterResult<dynamic> instead of the strongly‑typed equivalents. Downstream code must work with dynamic objects. Property names in the dynamic result follow these rules:
- Non‑dotted paths (
Name,Category,OrderItems, …) are projected as‑is — access them by their exact field name at runtime. - Dotted paths through reference navigations (e.g.,
Category.Name) produce nested dynamic objects reflecting the navigation hierarchy — access them asresult.Category.Name, not as a flatCategoryName. - Dotted paths through collection navigations (e.g.,
Category.Vendors.Id) generate aSelectlambda per collection segment — the result is a nested collection of dynamic objects accessible asresult.Category.Vendors[0].Id. - Multiple dotted fields sharing the same root segment (e.g.,
Category.Name+Category.Id) are merged into a single nested object:result.Category.Nameandresult.Category.Id. - Mixed whole‑navigation + sub‑field paths: when both
"Category"and"Category.Name"are requested, the sub‑field projection takes precedence and"Category"is silently dropped.
SelectDynamic preserves the navigation hierarchy (nested), whereas the typed GroupBy result flattens dotted names (point 6). They are different on purpose — pick the extension method that matches the shape your client expects.11. All Filter Extensions Apply Order and Page Before the Select Projection
All Filter extensions — both typed (Filter<T>, ToList<T>(Filter), ToListAsync<T>(Filter)) and dynamic (FilterDynamic<T>, ToListDynamic<T>, ToListAsyncDynamic<T>) — apply ordering and pagination on the typed IQueryable<T> before the select projection. This ensures that field names referenced in orders always resolve against the original entity type T, regardless of which fields are projected.
Select.Fields list. The library resolves orders[].field against T's original property graph, then projects to the requested subset. This is the right behaviour for almost every list endpoint — it just surprises people who expected the order field to also need to be in the projection.See also
- Error Codes Reference → the 22 stable validation messages.
- Cache configuration → context for point 8.
SelectDynamic→ context for points 1 and 10.