DynamicWhere.ex
DynamicWhere.exv2.1.0·docs

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.

Hard requirement
Records with positional parameters and classes whose only constructor takes required arguments are not usable as the target type for 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.

No synchronous overload
If you need to compose UNION / INTERSECT / EXCEPT across multiple condition sets you must use the async pipeline. See 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).

Mind your collation
On case‑sensitive collations the provider may not be able to use an index for a 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.

Pick the right DataType
Configure your EF Core conversion accordingly: .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.

Aliases only
A 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.

Dotted in → flattened out
Inside the result, access the grouped column as 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.

No .All() support
If you need universal quantification (every child must match), express it with a negated .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.

In-flight calls keep the old config
Treat configuration as a startup concern when possible. Hot‑swapping the cache strategy at peak traffic is safe but won't retroactively re‑classify ongoing operations. See cache configuration.

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).

EF Core only
Only enable 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 as result.Category.Name, not as a flat CategoryName.
  • Dotted paths through collection navigations (e.g., Category.Vendors.Id) generate a Select lambda per collection segment — the result is a nested collection of dynamic objects accessible as result.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.Name and result.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.
Two different shapes — typed vs. dynamic
Note that 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.

Order on the entity, project after
You can sort by a column that is not in your 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