v3.1.0 — DCMP's change request, and two fixes every user needs
DynamicWhere.ex 3.1 answers the change request DCMP raised after moving onto 3.0, and closes two defects in the core that affect every user, guarded or not. If you are on 3.0.0, upgrade.
dotnet add package DynamicWhere.ex --version 3.1.0Packages
| Package | Version |
|---|---|
| DynamicWhere.ex | 3.1.0 |
| DynamicWhere.ex.Policies.EntityFrameworkCore | 3.1.0 |
| DynamicWhere.ex.Policies.Redis | 3.1.0 |
| DynamicWhere.ex.Policies.AspNetCore | 3.1.0 |
Why to upgrade
- A member named
Root,ItorParentbypassed field policies. System.Linq.Dynamic.Core read those names as its ownroot/it/parentkeywords, soRoot.Nameaddressed the row's ownName. UnderApplyPolicy, a dynamic projection ofRoot.Namereturned a[DwDenied]column, and a[DwForceWhere]scope reached through such a navigation filtered the wrong column. Expressions are now parsed with those keywords off, andParsingConfig.Defaultis no longer read. - One
InorNotIncondition with about 700 values ended the process. The values were joined as one flat chain, one level of nesting per value, and EF Core and the expression compiler walk that tree recursively. The request thread's stack overflowed, and nocatchcan stop that. A list longer than 32 values is now nested as a balanced tree; a list of 32 or fewer is written exactly as before. This affected guarded and unguarded queries alike. - Invented field names grew the process without limit. The reflection cache tracked a path before validating it, so every failed name stayed forever. Only valid paths are tracked now.
- The strict tier's refusals leaked which columns exist. A denied field and a missing name gave different answers, padding a name with dots changed the refusal, a cost-weighted hidden field broke the budget first, and
MissingContextValuenamed the scope column and the key it reads. Every one of these now answers alike. [DwForceWhere(AllowNull = true)]onIsNullorIsNotNullscoped nothing, rendering(field IS NOT NULL OR field IS NULL). It is refused now, at construction and when a stored rule is read.- Refusal audit events hold caller text, which could forge a log line. It is cut to 256 characters, and control, format, line separator and paragraph separator characters are escaped.
Read before upgrading
Eleven behaviours changed. The full list is breaking changes, points 14 to 24. The ones most likely to reach you:
- Dates read the member's type.
DateTimeOffsetmembers, nullable dates andDateOnlymembers now filter correctly, where 3.0.0 threw. - A date value is ISO 8601, year-first, or a format your deployment declares.
01/09/2026is refused as ambiguous until you callDwDates.Configure. - Segment set operations run in the database. They were combined in memory by object reference, so
Intersectreturned nothing andExceptremoved nothing whenever the query was untracked, projected or guarded. - A field path may not start with one of the parser's own words —
new,iif,np,isnull,is,as,cast,true,false,null. Seven of them used to throw from the parser, andNullreturned no rows and no error. The library refuses them by name now, in every clause. PageCounton an unpaged result is1.ApplyPolicy(ctx)refuses a context that never went throughPrepareAsync, with or without a store.- Four new caps refuse guarded requests 3.0.0 ran:
MaxConditionDepthandMaxConditionSets(10),MaxConditionValues(1000) andMaxAggregates(50). - Under
DwTier.Strictthe policy trace stays off results unless you ask for it;LastTracestill holds it.
What's new
[DwEntity(DefaultOrder = "CreatedAt desc, Id")]— the order a guarded query takes when its caller sends none.DwCaps.DefaultPageSize— a page for a guarded query that sends none.[DwForceWhere(..., AllowNull = true)]— the scope for a record that belongs to one tenant or to none.DwPolicyOptions.AuditRefusals— every refused guarded query in the audit, with the real field.DwAuditEventgainsErrorCode.DwDates.Configure— declare the date formats your deployment accepts, once, at startup.DwPolicyContext.IsPreparedandLogicException.Subjectare public.
The public API only gains members. Nothing was removed or changed.
Also fixed
- An idle rule store refused every guarded query fifteen minutes after its last write.
- The composable
GroupandSummaryreturned groups the k-anonymity floor suppresses. - A forced null check built from a context key failed every guarded query on its type.
- A
DateTimewhoseKindisLocal, compared against aDateTimeOffsetmember, filtered hours away from the moment it held. DwDates.Configurerefuses a format ISO 8601 already reads, which read a value one way on a server in UTC and another way outside it.
Verified
- 2,067 tests on EF Core 8 and 1,382 on the declared EF Core 6.0.22 floor. Release builds with zero warnings.
- Every fix was mutation-checked: broken on purpose, and a test turned red.
- Four review passes over the branch — security, QA, dates, and the docs against the code.
- 232 of 232 demo API endpoints against a seeded PostgreSQL 16.
- The docs reference names all 156 public types and 931 members, checked mechanically.
Compatibility
Targets net6.0, runs on .NET 6 through 10, with EF Core 6.0.22 or later. A project with no policy attributes and no DwPolicy.Configure call is affected only by the behaviour changes above.