Repository navigation
3.3.0 — configuring twice, a path the query cannot compute, and public Clone - #6
Conversation
…ath no database can compute is refused DW-16: DwPolicy.Configure takes a second call asking for what is already in force, inside the lock that does the configuring, so an integration suite starting many hosts over one composition root needs no check of its own. A different posture is still refused: the tier, the flags, the salt, the store failure mode, both intervals, every cap, the exposed catalogue and the provider types are all compared. The token vault, the service provider and the provider instances are not, since a second host builds its own. AddDwPolicies registers the posture in force rather than the instance it just built. The suite's own bootstrap drops the lock this feature replaces. DW-17: a path beneath a member with no fragment, whose leaf no database can compute, is refused in the strict tier where the provider used to throw. RowShape.Expresses reads the query the rows come from: an entity's model, a projection's initializers, and a member a projection copies from the entity. It answers false only where the whole set of members a container can produce is known, so rows in memory, a framework member the provider translates, a value beneath a converted column and a source the library cannot read are all left alone. The trace records the refusal, and the trace is now assigned before sanitizing so a refusal leaves it readable. D-3: Clone is public on Filter, Segment and Summary. A caller reading the same request again with another page rebuilt it around the caller's own clauses, which leaves both requests holding one condition tree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…lic Clone, and the group floor Version 3.3.0 in the four packages, with release notes, and every documented version reference with it. DW-16 and DW-17 are written up in DOC.md, llms.txt, the README highlights, the breaking-changes page (points 30 to 32), the configuration page and the security page. Clone gets a section on the Filter, Segment and Summary pages and an entry in the shapes table. D-1: [DwEntity(DefaultOrder)]'s own remarks said a projected query takes no default. It has since 3.2.0, and the remarks now say what makes one take it. D-2: DwCaps.MinGroupSize ships on at 5, so a guarded summary drops groups of fewer than five rows and says nothing about it. The aggregation sections lead with that rather than leaving it to the caps table. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ture value is proved compared A clause composed on the guarded handle — Where, Order, Select, Page — now passes the source's shape to the sanitizer, so it refuses a path the query cannot compute exactly as a terminal does. Without it the same filter was refused through ToList and ran to the provider's own failure through Where. The posture comparison is now checked by reflection: every settable value on DwPolicyOptions and DwCaps is changed in turn and the second configure must refuse it. A value added later and forgotten in the comparison would let a second host run with a posture it did not ask for, and this fails until somebody decides which column it belongs in. TokenVault and Services are named as the two that are not compared, and a stale name there fails too. Also probed: a member a projection never assigns fails unguarded, which is why refusing it guarded is right rather than an over-block. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t a loaded run can meet The 3.2.0 publish run failed on one test — the store conformance watch, which waits up to ten seconds for a store to report a change — while the same suite had just passed twice on CI. The cause is not that store: four reads in the wrapper suite blocked a thread-pool thread each for a whole query, which is enough, on a two-core runner in Release, to leave the watch's reader unscheduled until its budget runs out. Those four reads are awaited now, through a ZwKit.CodeAsync that matches Code, and the build is warning-free again (xUnit1031). The watch test waits on the watch rather than on the clock, and its budget is sixty seconds: only a store that never reports waits that long, and a budget tight enough to catch a real hang is also tight enough to fail a healthy run under load. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…pe can compute The walk looked through an entity type's derived types for a member the queried type does not map, and counted it as a column. EF Core does not: it translates a member against the type the query is over, and a member mapped one level down fails with "Translation of member 'Licence' on entity type 'ZyParty' failed. This commonly occurs when the specified member is unmapped" — which is the five-hundred this release exists to turn into a refusal. So the look through the derived types made DW-17 miss the case it was written for. The queried type's own model decides now. Query the derived type to filter on such a member, which already worked and still does. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
A projection the library can read into was refused for a member the initializer does not build, whatever provider ran it. That is EF Core's answer to give: a provider of its own may evaluate the getter in memory, as rows in memory do, and refusing there turns a query that worked into a refusal. A projected shape now records whether it was read through EF Core's provider, and answers "cannot say" otherwise. An entity shape is unaffected: it has a model, which is EF Core's by definition. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…e is compared by every name, and a refused segment keeps its trace What the security review of the branch found. High: handing the posture in force back with a policy source beside it was read as "the same posture" and did nothing. The instance carries the same values by definition and says nothing about the sources, so the shortcut skipped the only comparison that mattered: the resolver was never rebuilt, the source was never consulted, and every rule in it — a denial included — quietly did not apply. DwPolicy.Options is what AddDwPolicies registers, so the call is one a module adding its own store would write. The shortcut compares the sources now. Low: the catalogue was compared by what Entities reports, which is the last name each type was exposed under, while every earlier name stays resolvable. Two catalogues reporting the same pairs could answer differently to an administrative request. DwEntityCatalog.SameAs compares both name maps. Low: the segment terminal still assigned LastTrace after sanitizing, so a refused segment left the previous request's trace on the handle — and a strict refusal names no field, which makes the trace the only record of which field it was. Ruled out by the same review, with probes: the refusal is indistinguishable from an unknown name and from a denial in every clause, caps and cost budget included; Expresses can only add a refusal, never skip a check; the trace record reaches no caller; forced predicates never pass through the name resolver; and public Clone gives no TOCTOU, since sanitizing clones before it reads anything. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ute, and a cap's own default is not a difference What the over-blocking review of the branch found. A projection is the last thing the provider builds, and EF Core evaluates that one on the client when it cannot translate it. So Selects naming an unmapped getter — on the entity, across a navigation, on an owned or JSON member — returned its value on 3.2.0 and was refused here, in both tiers of the strict path and in a Segment. Five shapes, on both EF legs. The refusal now applies to the clauses the database has to compute: a filter, an order, a grouping key, an aggregated field, and each of those inside a Segment. The posture comparison read IsMinGroupSizeSet, which nothing in enforcement reads. The documented appsettings sample writes "MinGroupSize": 5, so a host binding it and a host on the defaults enforce the same floor and were refused — the second host this feature exists to let start. The floor that applies is compared now, not whether somebody wrote it down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nder it From the docs-against-code review: - A constructor with arguments does not leave a projected query in its own order; only a projection with no initializer does. A Filter carrying Selects is ordered like any other filter — it is a composed Select that leaves the rest of the chain unordered. - Count was listed as a framework member that runs. No path can name it: the library's own validation refuses Lines.Count before any of this is asked. - Three places still said a second Configure is refused, on pages that now describe the opposite. - LastTrace's sub-bullet still said the trace holds the last call that got past sanitizing. - AddDwPolicies does not "do nothing": it binds, changes no posture, and registers the one in force. Its exception list said "and" for two alternative causes. - The [DbFunction] example cannot be reached by a property path. The rule behind it is the model's, and that is what the text says now. From the fixes in this branch: Selects is not refused, a column only a subtype maps is refused through the base type, another provider's rows decide for themselves, and the posture comparison reads the floor that applies rather than whether it was written down. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… ruled out The first round's over-blocking and posture probes, adopted as tests. They cover what the fixes must not take back: a projection naming an unmapped getter on the entity, across a navigation, on an owned member and on a JSON member; the framework members the provider translates; and the posture comparison property by property, the token vault, the service provider, provider instances and provider order included. One case asserted the opt-out being compared, which the fix removed on purpose; it asserts the value that applies instead. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…l is read on both branches Two findings from the second review, both about which rows the new refusal is allowed to speak for. A provider that wraps EF Core exists to rewrite the members EF Core cannot translate: LinqKit's AsExpandable, DelegateDecompiler's Decompile. Until now the projection branch asked whether EF Core ran the query and the entity branch did not ask at all, so the same wrapper over an entity was held to EF Core's model and a query that ran in 3.2.0, and still runs unguarded, was refused. Both branches now ask the same question, and it is asked of EF Core's own provider rather than of IAsyncQueryProvider, which a wrapper may implement and a provider that is not EF Core's may implement too. The provider is matched by name, so no internal type is referenced and EF Core 6 to 10 answer alike. A projected member assigned by a conditional recorded nothing, so nothing beneath it could be refused. That is not an exotic shape: the core's own typed Select null-guards every nested node it builds, so composing Select and then filtering lost the refusal the bare handle gives. Both branches are now read — a nested initializer, a member copied from the entity, a value built and left empty, a null — and a member one branch assigns two ways, or assigns in a way this shape cannot read, is left alone rather than refused on half of what builds it. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…and Clone says what it copies IncludeTraceInResult defaults to the tier's own answer, and the tiers are equal before it is read, so a host writing that answer out enforces exactly what a host leaving it null does. It was compared as written, so the second host was refused — the same shape the group floor's comparison already had fixed, applied to the one other value with a default of its own. Clone's newly public contract said the copy shares no object with the original. Every node is copied; a condition's values are the caller's own objects in a new list, which the internal method that implements it has always said. The three public contracts now say it too, along with the projection exemption on the method that implements it, whose summary still described the behaviour before Selects was exempted. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…anged under it The documentation review found eight places where the text and the code had parted company, and the code review left three more to write down. Corrected: llms.txt still said the entity row reads a derived type's model, which one commit had already removed everywhere else, and two comments in the code said the same; the table's "Runs" cell for a converted column promised behaviour the library does not control, so the cells that only mean "not refused" now say "left alone", with a line saying what that is and is not; two of the four places saying a second Configure is refused were missed; a Segment carries no grouping key and no aggregated field; the group floor is now on every aggregation entry rather than on four of seven; and the security page's new section no longer arrives between numbered channels six and seven. Written down: the refusal is EF Core's own provider's to make, so a provider in front of it is left alone; a row the library itself projected reads like any other; the refusal raises no [DwAudit] event, as an unknown name raises none; a simulation has no source, so it cannot refuse such a path at all; the trace flag and a type's reported name in the posture comparison; Clone's real contract; and a condition's values being read more than once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…t stays ruled out Four files, 120 or so facts: the source table row by row, the posture property by property, every clause that refuses and every one that does not, the mapping shapes a refusal must not misread — JSON owned types, primitive collections, TPH, TPT, table splitting, keyless entities — and the shapes that decide whether a projected member can be spoken for. Where a probe asserted a contract the review then settled the other way, it asserts the settled one: a wrapping provider is answered for the same way over either kind of row, Clone shares the caller's values, and the two divergences that stay — a simulation cannot refuse an uncomputable path, and a condition's values are read more than once — are pinned as the limits they are rather than as bugs. One probe drove DwPolicy's static provider list by reflection to reach a comparison; it now reads the comparison itself, since the list is process-wide and driving it made every suite configuring the posture in parallel fail. The mapping probes stay off the floor leg: they map with ToJson and a primitive collection, which EF Core 6 has neither of. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… from nothing is claimed as nothing The third review found the base-type walk in the provider test catching the very thing the test exists to exclude. A provider built by deriving from EF Core's own rewrites what EF Core cannot translate exactly as one built by wrapping it does, and was held to EF Core's model while the wrapping one was left alone. EF Core's own provider derives from object in 6, 7, 8, 9 and 10, and a DbSet's queryable carries that exact type through every operator, so an exact comparison matches every EF Core query the walk matched and stops matching a subclass. A projected member assigned from something the shape cannot read — a method call, a captured value, a subquery, two branches building it two ways — had its name recorded by the level above it, so the shape answered "yes, the query can express this" for a path no database computes. Only a proven no is acted on, so no query behaves differently; the shape now says it cannot say, rather than making a claim it cannot support. The review's probes are kept, including the one that drives EF Core's own in-memory provider: it is EF Core's provider, so its rows are EF Core's to speak for, and the getter fails there as it fails in SQLite. A probe adopted in round 2 drove DwPolicy's static provider list by reflection, which refused the bootstrap of any suite configuring the posture in parallel — about one floor-leg run in eight. It reads the comparison key directly now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… read The provider test is EF Core's own type rather than a type derived from it, and a rewrite inside EF Core's own pipeline — a [DbFunction], a member-translator plugin, a replaced query preprocessor — leaves EF Core's provider in place, so a member it computes without a mapping is refused with the rest. A projection is read only as far as its initializer can be read: a nested initializer, a member copied from the entity, a value built and left empty, a null, and a conditional over those. A member assigned from a method call, a captured value, a subquery, or by two branches that build it two ways is left alone and still fails in the provider, as it did before 3.3.0, and past MaxComplexDepth the shape stops reading and stops speaking. The source table says so, and the sentence promising a deep copy no longer says it twice. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Seven places where the text and the code had parted company. The security page said a path the policy leaves alone fails as the unguarded query does. Most of them run: rows in memory and a framework member return rows, and beneath a converted column the provider decides. It was the one page round 2's "left alone is not a promise that the path runs" never reached, and it carried the stronger claim. The in-memory summary entry was the one aggregation entry with no group floor, and the floor applies there: ApplyPolicy takes an IEnumerable and hands it to the same guarded queryable, so a summary read that way drops groups of fewer than five rows like any other. README's own bullet had lost the Segment clauses the other three documents carry. A cap's default is not generally compared by the value that applies: only the group floor's getter answers that way, and DefaultPageSize is applied as the smaller of itself and MaxPageSize while being compared as written. The generalisation is now the one true sentence. A [DbFunction] maps a method, and no field path can name a method, so it was never an example of a member the refusal reaches; a member translator plugin and a replaced query preprocessor are. The website's Condition page, which is where a caller reads about Values, now carries the limit that a value is read more than once. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ads from
The fourth review found the last over-block, and the plainest one: the
ordinary DTO-with-nested-DTO-list projection.
Lines = o.Lines.Select(l => new LineRow { Label = l.Sku }).ToList() was
recorded as a member copied whole from the entity's Lines navigation,
because the routine that reads a member chain strips a sequence operator off
the head of the expression. That is right where it was written for — a
filtered include reads its navigation through Where, OrderBy, Skip and their
kin, and the member it reaches is the navigation — and wrong here: the row's
Lines holds LineRows, not Lines, so every path beneath it was read out of the
entity's model. A LineRow member the entity happens to declare and not map
was refused, though the query runs and ran in 3.2.0.
The reader now declines to strip those operators, so such a member records
nothing and is left alone, which is what the source table already said about
a member a subquery builds.
The probes are kept, along with the round-3 one that still asserted through
the base-type walk the same round replaced, and a shadow property's refusal
is recorded as the limit it has always been: a field path names CLR members,
and a shadow property has none.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The JSON Cookbook claimed thirteen examples and shipped twelve: DOC.md carried an empty heading for a tenth whose body sat under a duplicated thirteenth, and the website and the sidebar numbered around the gap. The examples are numbered without one now, in all three places, and the count is the one on the page. The routes are unchanged. llms.txt still generalised the posture comparison to "a cap's own default", which the other documents had already narrowed: only the group floor's getter answers with the value that applies, and every other cap is compared as written. A null assignment on its own records nothing and is left alone, so listing it among the shapes a projection is read through was wrong; it carries weight as a branch beside one that assigns. The site's own metadata, which reaches every page through the JSON-LD, said .NET 6 through 9 where the library targets 6 through 10. The in-memory summary overload's floor is written down in llms.txt too, and DOC.md now says its breaking points are numbered as that document numbers them. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ovider test reads the assembly The fourth review found an inference channel the strict tier had left open. An audited field records one event per use, and the query is refused rather than the record dropped once MaxAuditEvents is reached — fail closed, because an access with nothing written down is the one outcome [DwAudit] exists to prevent. That refusal carried CapExceeded and a SourceOrigin naming the cap, while a name matching nothing carried the ordinary field refusal and no origin. An unknown name is never audited and never reaches the cap, so the difference told a caller, one guess per request, which names are real and audited — which is to say exactly the fields [DwAudit] is put on. Under Strict, outside a dry run, the cap now raises the clause's own field refusal: same code, FieldPath "*", no origin. The request still fails, the trace still records which refusal it really was, and Convenience and a dry run still answer CapExceeded. The provider test now reads the assembly a type came from as well as its name, so a type declared under EF Core's own name elsewhere is no longer taken for EF Core's provider and rows that run are not refused on the strength of a name. The review's probes are kept. Two of them asserted contracts settled the other way: a host that replaces EF Core's query provider is left alone like any other provider the library cannot read, and a spoofed provider name no longer reaches the model. A round-3 probe walked every posture value, printed what it found and asserted nothing; the comparison is guarded for real elsewhere, and the probe is gone rather than standing in for one. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…es alone The audit cap's refusal under Strict is a behaviour change and a closed channel, so it is written where both belong: the security page's channel list, the breaking-changes page as point 33, DOC.md's own list, the release notes and README's upgrade note. Convenience and a dry run are unchanged, and the request still fails either way. The provider rule's reason is corrected. It is not that a provider deriving from EF Core's rewrites in the same way — a host replacing EF Core's query provider through ReplaceService may pass straight through. It is that the library cannot tell the two apart, and refusing on that guess would take back a query the rewriting host answers today. A projection that does not build its rows with an object initializer — an anonymous type, a constructor with arguments — says nothing about which member each value sets, so no member of such a row is refused, which the text now says rather than implying the opposite. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y where it was announced The change was written down where it was introduced and left contradicted in eight other places: llms.txt still said the cap throws CapExceeded "in both tiers and in dry run", with a SourceOrigin naming it; the caps table, the audit section, the error-code table and the refusal-audit note said the same; the configuration page said every CapExceeded under Strict has FieldPath "*", which now describes a code that tier no longer raises for the buffer; and breaking point 23 still listed the audit cap beside MaxNavigationDepth as a cap that names its cap in SourceOrigin. llms.txt also still counted eight inference channels after a ninth was added to it. The provider rule's reason was corrected in two documents and left standing in three: a provider built by deriving from EF Core's own does not necessarily rewrite anything — a host registering one through ReplaceService may pass straight through — and the real reason is that the library cannot tell the two apart, so it leaves every provider but EF Core's own alone. None of the three mentioned that the test reads the assembly as well as the name, which the method's own remark had also stopped saying. The sentence about a projection built without an object initializer reached two documents and not the other two. The companion packages' llms.txt entries still declared a dependency on DynamicWhere.ex 3.1.0, and the NuGet release notes did not mention the audit cap at all. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… clause A strict refusal names no field, so that a denied one, a misspelling and a field that does not exist cannot be told apart. The fifth review found four that named one anyway. AmbiguousFieldName told a caller that the name they wrote matches more than one field, which is to say at least one. It is refused as an unknown name is now, and the trace records which fields it matched, for the operator who has to fix the aliases. AmbiguousGroupKey reported the grouping key's canonical path — the column behind whatever alias the caller wrote — and an origin saying its values are transformed. TransformRequiresMaterialization listed every transformed column on the type to a caller who had named none of them. MissingHashSalt and MissingTokenVault named the masked field a deployment had not configured for. Those three name the clause and carry no origin. The convenience tier and a dry run are unchanged: they name fields anyway. ResultTransformer.cs held a raw NUL and a raw unit separator inside two literals, which made every grep and ripgrep treat the file as binary and skip it silently. It was the only such file in the solution, and it is where one of these four lived, which is plausibly why four rounds of review did not see it. Both are written as escapes now. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…nce operator hides The four refusals are written up as breaking point 34, as the security page's eighth channel, in the release notes and in README's upgrade note. Two limits the review named are written down rather than changed. [DwAudit] records a field the request names, plus a default-order field the query adds: a request that sends no Selects names nothing, so an audited field the row carries back is returned with no record. Closing that redefines what a use is and changes the event volume of every deployment already running the control, which is a decision for its own release; the documentation now says plainly what is recorded and what is not, and how to be recorded — name the fields, or audit Where, which a filtering request cannot leave out. A member assigned through a sequence operator, o.Lines.ToList() included, is left alone for the same reason a subquery's rows are: what the row holds is not always what the navigation holds. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…eld it reads [DwAudit] exists to answer who read a field, and a request that sent no Selects read every audited member of the row with nothing written down. The same caller naming one field recorded it; naming none recorded nothing, which made an empty Selects one token past the control. A use is what the request reads, not only what it spells out. The members a projection the caller never named hands back are recorded for Select: what the synthesized projection keeps where one is built, and every member the caller may select where none is, since the row then comes back whole. Both sets are paths whose policy the walk has already resolved, so the recording costs no resolution of its own, and only a field [DwAudit] names produces an event — one per query, not one per row. The library's own specification said the opposite, deliberately: a synthesized projection was the library resolving a policy on its own behalf rather than an access by the caller. It is an access — the caller receives the value. That test now pins the read, and the suites that filter on an audited field name a projection of their own so each one still measures the clause it is about. A deployment already running the control sees more events than it did, and MaxAuditEvents refuses rather than dropping a record, so a buffer that was never reached can now be: raise the cap, or drain per request with app.UseDwPolicyAudit(). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
[DwAudit] recording a read the request did not name is a security fix and a behaviour change, so it is written as breaking point 35, as the third of the bypasses that are not channels, in the release notes, in llms.txt's channel list and audit section, and in README's upgrade note. What was written last round as a limit is the behaviour now: a use is what the request reads, not only what it spells out. Each place says what a deployment has to do about it: more events than before, one per query rather than per row and only for a field the attribute names, and MaxAuditEvents refuses rather than dropping a record, so a buffer that was never reached can now be — raise the cap, or drain per request. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ntation The sixth review found both halves of a mistake I made in the fifth. Three of the four refusals that now name the clause read the posture's DryRun alone, where everything else in the layer reads the union of the posture's and the caller's. A dry run is what lets one canary subject run unenforced while everyone else stays enforced, and a canary meeting the hidden refusal is a canary that cannot preview the posture it exists to preview. All three read the union now, as the ambiguous-name branch already did. The helper that decides what the masking pipeline's refusals name was inserted between Apply's documentation and Apply itself, so the block attached to the helper instead: three CS1572 warnings, and Apply lost its entry in the XML that ships with the package — including the exception this release changed. The helper sits below Apply now, and all four projects build with no warnings again. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…as well as the announcement The transform refusal keeps its SourceOrigin under Strict — it names the method and what to call instead, never a field — where four documents said all of them carry none. It also lists every transformed column, not every masked one: generalized, truncated and formatted fields are in that list too. llms.txt is the reference, and it still described the old behaviour in eight places: the ambiguous-name rule, the two lists of codes that throw regardless of tier, the group-key collision, the FieldPath and SourceOrigin reference blocks, five rows of the error-code table, and a 3.3.0 history block that did not mention the change at all. The configuration page's strict-refusal section and DOC.md's blocked-action semantics — the two places the error tables point a reader at — now name the four as well. The security page's title counted its bypasses as channels: eight inference channels and three bypasses, which is what the page itself says. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…three more the review found The sixth review's security pass, including one more regression of my own. Hiding the field from the caller hid it from the audit too. RefusalAudit records AuditPath ?? FieldPath, and the four refusals changed in the fifth round set the path to "*" without setting AuditPath, so an audited refusal said "*" where it used to name the field. A record of a refusal that cannot say which field was probed answers nothing, which is what AuditPath exists for; all four set it now. A blank GroupBy.Fields entry reached the resolver and failed with ArgumentNullException, where every sibling clause guards a blank first. That is neither a refusal nor the malformed-clause failure an endpoint turns into a four-hundred — it reads to an operator as the endpoint being broken. The grouping-key loop guards it like the rest, and a blank name now fails a guarded query exactly as it fails an unguarded one in every clause. An alias spelled like another member of the same type passed the startup scan and then merged two columns on the way out: the rename wrote one column under the other's name and the other's value was gone. ValidateModel reports it, and a rename onto a name the row already carries is not applied, so a deployment that never scans keeps both columns. Left as they are, with reasons: a store refusal reports the strict tier because it does not soften in the convenience one, which its own remarks and the reference both say; and a group key holding a unit separator can invent a collision it cannot hide, which is the over-refusing direction and older than this branch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…as may not shadow a member Three lines the sixth review's fixes earned: the four refusals that name the clause still record the real field for AuditRefusals, as every refusal with a caller-facing "*" does; a blank name fails a guarded query exactly as it fails an unguarded one, in every clause; and an alias spelled like another member of the same type is reported by ValidateModel, with a generated row keeping both columns under their own names. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…d four more the review found The seventh review, and four of the six are mine from the sixth. The audit recorded what a projection would have kept, and a dry run applies no projection — so a dry run recorded a list the caller never received while handing them every member, the audited denied ones included. It records what the row carries there, with the effect the policy decided. It also recorded every member the caller MAY select rather than every member the query hands back: a navigation nothing loads was logged as read, though the caller receives null for it, and the false event counted against a cap that refuses rather than dropping a record. And it stopped at the top level, so an audited column of an owned type was handed back unrecorded — the same hole one level down. A member kept whole now records the audited paths inside it, and a member the source does not carry records nothing. The rule that stops a rename landing on a name the row already carries read a case-insensitive map, so an alias spelling its own member differently — total for Total — looked like a shadow of itself and the public name stopped being emitted, which 3.2.0 emitted. A column does not collide with itself. The scan asked reflection for one property by name with IgnoreCase, and a type with two members answering to one name — a base member hidden with new, two spelled in different cases — took an AmbiguousMatchException out of a method documented to return a report, losing every other type's errors with it. It walks the type's own properties now. Last, one older than this branch: MaxNavigationDepth counts the canonical path, so an alias the caller wrote as one token was refused with the cap's own code and an origin stating that path's depth, where a name matching nothing got the clause's refusal. Under Strict such a name is refused as an unknown name is; a caller who wrote the path themselves still meets the cap, which is what the existing test pins. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…en as one token The audit rule is stated precisely rather than loosely: a member kept whole records the audited paths inside it, a navigation nothing loads records nothing because the caller receives null for it, a value the source does not carry records nothing, and a dry run records everything the row carries, a denied member included. The navigation cap's answer to an aliased name is breaking point 35, and the audit read moves to 36. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deploying doc-dynamicwhere with
|
| Latest commit: |
9ce3016
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://f67f2e6c.doc-dynamicwhere.pages.dev |
| Branch Preview URL: | https://feat-configure-once-and-unma.doc-dynamicwhere.pages.dev |
There was a problem hiding this comment.
Copilot review overview
🔵 Needs a closer look
It includes significant behavior and security-related changes in the core policy/configuration path and warrants final human review despite strong accompanying tests and documentation.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
This PR prepares the 3.3.0 release of DynamicWhere.ex by updating package/docs versions and documenting (and testing) several behavioral changes in the policies layer—most notably allowing idempotent DwPolicy.Configure calls with the same posture, refusing “uncomputable” member paths under Strict (instead of letting EF Core throw), and making Clone() public on the request types.
Changes:
- Bump versions to 3.3.0 across the core package, policy provider packages, README, and docs site.
- Update policy enforcement/configuration behavior and related documentation (configure-twice semantics, strict refusal behavior, audit-related behavior notes).
- Add/adjust tests to cover new/changed behaviors (including new probes and async-safety improvements in tests).
| File | Description |
|---|---|
| README.md | Updates install/version snippets and adds 3.3.0 highlights/upgrade notes. |
| OfficialWebsite/package.json | Bumps docs site version to 3.3.0. |
| OfficialWebsite/lib/seo.ts | Updates JSON-LD OS string to include .NET 10. |
| OfficialWebsite/lib/nav.ts | Updates site version and renumbers JSON Cookbook nav entries for 12 examples. |
| OfficialWebsite/app/page.tsx | Updates homepage “JSON Cookbook” example count. |
| OfficialWebsite/app/layout.tsx | Updates home title to include .NET 10. |
| OfficialWebsite/app/docs/policies/security/page.tsx | Expands/updates security narrative (channels/bypasses + new sections). |
| OfficialWebsite/app/docs/policies/providers/page.tsx | Updates provider install commands to 3.3.0. |
| OfficialWebsite/app/docs/policies/page.tsx | Updates “configure twice” guidance and security page title text. |
| OfficialWebsite/app/docs/policies/configuration/page.tsx | Updates configuration docs (configure-twice section, MinGroupSize note, strict refusal notes). |
| OfficialWebsite/app/docs/policies/admin/page.tsx | Updates AspNetCore provider version and simulation note re: uncomputable paths. |
| OfficialWebsite/app/docs/page.tsx | Updates docs landing page version and install snippet. |
| OfficialWebsite/app/docs/installation/page.tsx | Updates installation instructions/version and .NET version wording. |
| OfficialWebsite/app/docs/extensions/to-list-summary/page.tsx | Adds MinGroupSize “ships on at 5” callout. |
| OfficialWebsite/app/docs/extensions/to-list-async-summary/page.tsx | Adds MinGroupSize “ships on at 5” callout. |
| OfficialWebsite/app/docs/extensions/summary/page.tsx | Adds MinGroupSize “ships on at 5” callout. |
| OfficialWebsite/app/docs/extensions/group/page.tsx | Adds MinGroupSize “ships on at 5” callout. |
| OfficialWebsite/app/docs/examples/page.tsx | Updates cookbook count and renumbers example cards (12 total). |
| OfficialWebsite/app/docs/classes/summary/page.tsx | Documents Summary.Clone() being public since 3.3.0. |
| OfficialWebsite/app/docs/classes/segment/page.tsx | Documents Segment.Clone() being public since 3.3.0. |
| OfficialWebsite/app/docs/classes/filter/page.tsx | Documents Filter.Clone() being public since 3.3.0. |
| OfficialWebsite/app/docs/classes/condition/page.tsx | Adds note about values being read twice (validation + predicate build). |
| OfficialWebsite/app/docs/ai/page.tsx | Updates AI reference version text to 3.3.0. |
| DynamicWhere.Tests/Policies/Sx6RenameProbes.cs | Adds tests/probes around alias shadowing and unknown-name normalization behavior. |
| DynamicWhere.Tests/Policies/Sx6BlankNameProbes.cs | Adds tests around blank field handling across clauses/tiers. |
| DynamicWhere.Tests/Policies/S4Model.cs | Adds EF Core model types used by new/updated policy probes. |
| DynamicWhere.Tests/Policies/Rv3InMemoryProviderProbe.cs | Adds probe ensuring EF Core InMemory provider behavior matches expectation for refusal scenarios. |
| DynamicWhere.Tests/Policies/ReviewTrackingWrapperTests.cs | Converts blocking async calls to awaited form to reduce flakiness. |
| DynamicWhere.Tests/Policies/ReviewPostureComparisonTests.cs | Adds tests verifying posture comparison semantics for configure-twice behavior. |
| DynamicWhere.Tests/Policies/ReviewMappingProbes.cs | Adds mapping-shape probes for RowShape.Expresses across EF Core features. |
| DynamicWhere.Tests/Policies/ReviewJoinEvalTests.cs | Adds async helper (CodeAsync) to avoid blocking on async in tests. |
| DynamicWhere.Tests/Policies/Pr6MappingProbes.cs | Adds EF Core 8-only mapping probes (JSON/complex/primitive collection). |
| DynamicWhere.Tests/Policies/PolicyStoreConformanceTests.cs | Adjusts timing/awaiting strategy to reduce watch/poll flakiness. |
| DynamicWhere.Tests/Policies/PolicyEndpointTests.cs | Updates endpoint bootstrap to rely on new idempotent configure behavior. |
| DynamicWhere.Tests/Policies/PolicyBootstrap.cs | Introduces shared test bootstrap posture configuration helper. |
| DynamicWhere.Tests/Policies/PolicyAuditTests.cs | Updates audit tests to account for new “no Selects” recording behavior. |
| DynamicWhere.Tests/Policies/Ov7ScaleProbes.cs | Adds scale/collision probes covering audit volume and refusal behavior. |
| DynamicWhere.Tests/Policies/Hx6DryRunProbes.cs | Adds probes around which “dry run” switches are honored by various refusals. |
| DynamicWhere.Tests/Policies/Dx7MiddlewareSurfaceProbes.cs | Adds probes validating middleware behavior with new audit recording. |
| DynamicWhere.Tests/Policies/Dx7AuditReadProbes.cs | Adds probes validating what constitutes an audited “read” after changes. |
| DynamicWhere.Tests/Policies/Dv5Model.cs | Adds model types for documentation-vs-code probes. |
| DynamicWhere.Tests/Policies/DocsReview33ProbeB.cs | Adds combined probes verifying docs claims (trace wording, floor behavior, etc.). |
| DynamicWhere.Tests/Policies/Ar7ScanProbes.cs | Adds probes for startup scan behavior around reflection edge cases. |
| DynamicWhere.Tests/Policies/Ar7OracleProbes.cs | Adds probes for strict refusal parity and posture comparisons. |
| DynamicWhere.Tests/Policies/Ar7EfAuditProbes.cs | Adds EF Core probes ensuring unloaded/unassigned members aren’t over-audited. |
| DynamicWhere.Tests/Policies/Ar7BlankProbes.cs | Adds probes for blank-name guards across clauses and tiers. |
| DynamicWhere.Tests/Policies/Ar7AuditProbes.cs | Adds adversarial probes for audit semantics and refusal/audit ordering. |
| DynamicWhere.Tests/DynamicWhere.Tests.csproj | Adds EF Core InMemory package; excludes specific probe suites on the EF Core 6 “floor” leg. |
| DynamicWhere.Tests/CloneTests.cs | Adds tests for new public Clone() deep-copy behavior. |
| DynamicWhere.ex/Policies/Validation/PolicyModelValidator.cs | Extends alias validation to catch alias-vs-property-name collisions. |
| DynamicWhere.ex/Policies/Source/PolicyQueryable.cs | Ensures LastTrace is set before sanitize; adjusts strict refusal field naming and rows-shape usage in sanitization. |
| DynamicWhere.ex/Policies/Masking/TransformPipeline.cs | Adjusts strict refusal field naming for missing salt/vault while preserving audit path. |
| DynamicWhere.ex/Policies/Discovery/DwEntityCatalog.cs | Adds catalog equality comparison used for configure-twice posture checks. |
| DynamicWhere.ex/Policies/Config/DwPolicyConfiguration.cs | Updates AddDwPolicies docs and registers the in-force posture instance into DI. |
| DynamicWhere.ex/Policies/Config/DwPolicy.cs | Implements “configure twice with same posture is a no-op” and posture/provider comparison logic. |
| DynamicWhere.ex/Policies/Attributes/DwEntityAttribute.cs | Updates DefaultOrder remarks to reflect projected-query behavior. |
| DynamicWhere.ex/DynamicWhere.ex.csproj | Bumps core package version to 3.3.0 and updates release notes. |
| DynamicWhere.ex/Classes/Complex/Summary.cs | Makes Summary.Clone() public and documents deep-copy semantics. |
| DynamicWhere.ex/Classes/Complex/Segment.cs | Makes Segment.Clone() public and documents deep-copy semantics. |
| DynamicWhere.ex/Classes/Complex/Filter.cs | Makes Filter.Clone() public and documents deep-copy semantics. |
| DynamicWhere.ex.Policies.Redis/DynamicWhere.ex.Policies.Redis.csproj | Bumps Redis provider package version to 3.3.0 and prepends release notes. |
| DynamicWhere.ex.Policies.EntityFrameworkCore/DynamicWhere.ex.Policies.EntityFrameworkCore.csproj | Bumps EF Core provider package version to 3.3.0 and prepends release notes. |
| DynamicWhere.ex.Policies.AspNetCore/DynamicWhere.ex.Policies.AspNetCore.csproj | Bumps AspNetCore provider package version to 3.3.0 and updates release notes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| /// <summary> | ||
| /// DOC PROBE ONLY — not part of the suite. Checks the documented "Configuring twice" comparison | ||
| /// list against what SamePosture actually compares. | ||
| /// </summary> |
The class comment called it a doc probe "not part of the suite", but it holds eleven facts with assertions, is excluded nowhere, and runs in CI — and two of those facts are the reflection guard the "Configuring twice" documentation rests on. The comment now says what the class is and why it is kept. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y has The CI run on 09386e0 — a commit holding one XML comment — failed on A_watch_yields_the_version_on_every_write with OperationCanceledException at ten seconds, while the same suite had just passed three times on the commit before it. This is the flake 073614c diagnosed for the store conformance watch: the reader runs on the thread pool, and three thousand tests on a two-core runner can leave it unscheduled until the budget runs out. That commit fixed the conformance watch and left this one, which is the same shape, at ten seconds. The budget is sixty seconds now and the write loop waits on the reader rather than on the clock, so it stops writing the moment the watch has reported twice. The assertions are untouched: removing the watcher registration from InMemoryPolicyStore still fails the test, so the longer budget buys patience, not a pass. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The offset is (PageNumber - 1) * PageSize, worked out in 32 bits, and for a large enough page number the product wraps. A negative offset is an error on SQL Server and PostgreSQL, so a request became a 500, and it is the first page again on SQLite and in memory, so a page far past the last row returned rows. The policy layer caps PageSize and not PageNumber, so a guarded query took the same path. It is worked out in 64 bits now and held to what Skip can take, in Page and in the three summary terminals. A page past the last row is an empty page however far past it is, as it always was for a page number that did not wrap. Found by round 8's own pass. The mutation (the 32-bit product back) turns four tests red. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…till policed The attribute walk names the paths of the declared types, four segments deep, and a path that matches no fragment is allowed. Round 8 found four places a value sat where no fragment reached, each reproduced with a probe first. All four are in 3.2.0 as well. Beneath a member whose type the framework declares. Salary.Value and Salary.HasValue on a decimal?, Secret.Length on a string, Born.Year on a DateTime, Lines.Count on an application's own collection class: the pipeline validates each, the provider translates each, and no attribute can be placed there. A [DwDenied] decimal? was filtered on, sorted by, grouped by with its values as the keys, aggregated, and handed back by a dynamic projection, under Strict. A generalized member gave its stored value the same way, an audited one was read with nothing recorded, and a weighted one cost the default. Such a path takes every fragment of the member it reads, whichever provider supplied it. What is said to the caller about the member, its alias, its required filter, its forced scope and its description, stays the member's alone. A subtype's member is not such a path: a grant of Zone does not grant what a subtype of Zone declares. Past the walk's depth. Caps.MaxNavigationDepth can be raised above four, and a denied member at five segments was then filtered on, grouped by and returned. The attributes of the member at the end of such a path are read directly. Where a transform cannot be applied. On either kind of path there is nothing to apply the member's transform to, so every way the path hands a value back, Select, Group and Aggregate, is refused. Filtering and ordering follow the member's own decision, as they do along a named path. In the rows themselves. The outbound walk transformed along named paths only, so a masked member five segments down, one only a subtype of the row declares, and one on an object a dictionary holds all came back as stored, at the default caps, under Strict. The rows are walked by run-time type as well, and a member that declares a transform and was not transformed along a path is transformed by its own attributes, once. Only what can lead to such a member is read, and a model with no transform attribute pays nothing. And the walk itself returned at its depth limit with the type still marked as being inside it, so a type first met there read as a cycle wherever it was met again. What a cycle leaves out, a forced scope, a required filter and an alias, was then left out of a path reaching the type directly. Which of two members was declared first decided whether a tenant scope applied. Thirteen mutations, each red. One of them was a regression of this commit's own first cut, caught by round 5's deny-by-default tests: the member a path reads was taken for any segment the navigated type did not declare, so a subtype's member inherited the grant of the member above it. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…of what they read The audit middleware drained the request's events with the request's own abort token. A client that closed the connection, as the rows arrived or the moment they had, cancelled the write that follows the response: the sink threw, the middleware logged it, and the events went with the context. An audited read with nothing written down, for the price of a socket. The drain has a budget of its own now, thirty seconds, which the caller cannot cancel and a hung sink cannot outlast. The new test fails on the old code. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Where a rule lives is read before the transaction that moves or deletes it, so two writers of one rule could read the same answer. The slower one then cleaned up after a copy the faster had already moved and left that writer's copy behind, with no owner entry pointing at it: a rule still applying to a caller nobody any longer wrote it for, which no later write or delete could find. The transaction is conditional on the owner entry now. A writer that loses the race is told nothing was stored, as a failed commit always was, and writes again. Eight writers moving one rule four hundred times each leave one copy under the owner; without the condition the same test fails every run. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
EF Core 6.0.22 asks for 6.0.1 or later, and 6.0.1 is the last version open to CVE-2024-43483 (GHSA-qj66-m88j-hmgj), so a host on the EF Core 6 floor resolved a vulnerable version through all four packages. 6.0.2 is the patched one. A host on EF Core 8 or later already resolves a newer version and sees no change. `dotnet list package --vulnerable --include-transitive` is clean for the four shipping projects. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… a hit
Every property lookup of every query, several per field, took one global lock
to read the cache configuration, copied it, allocated an input object to carry
four references into the tracking call, built a closure over the copy whether
or not the entry was already cached, and then, under LRU, which is the default,
wrote the entry's last-access time under that entry's lock. Request threads
querying one entity type queued behind each other twice per lookup.
One million lookups of one cached member, before and after:
1 thread 152 ms, 167 MB allocated -> 35 ms, 22 MB
8 threads 2697 ms -> 108 ms
The configuration in force is a private copy nothing edits, read with one
volatile read; a caller outside still gets a copy. A hit returns before any
closure is built. The tracking call takes its arguments directly. A last-access
time is refreshed once it is a second old rather than on every read: eviction
only asks which entries are oldest, and one read a moment ago is already among
the newest.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ns unguarded A request body carrying "conditions": null or "subConditionGroups": null overwrites the list's initializer. The pipeline takes such a group, and every other reader in the gate checks for it. The group floor's walk over Having, which looks for its own reserved alias, did not, so with the floor on, which is the default, a summary that runs unguarded failed guarded with a NullReferenceException. The floor still applies to such a summary; three tests, each red without the check. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d the outbound walk documents its second pass Governing and Unwalked are asked of every path a query resolves, and nearly every one is a single member or within the walk's depth: both answer before splitting anything. Find is the provider's own again, and GraphWalker.Apply documents the two parameters the second pass added, which had cost the project two CS1573 warnings. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he navigation above it is kept bd9ac24 refused Select, Group and Aggregate on any path the walk cannot name whose member is transformed. That is right beneath a member: Bonus.Value is a decimal where Bonus is what is rounded, so there is nothing to apply the chain to. It was wrong for a member past the walk's depth, which is still a member. Run against a database, the first cut showed it. The projection gate asks what each loaded path denies, and a masked column five segments down an Include chain now answered "Select": the gate synthesized a projection and left the whole included navigation out, where the outbound walk's second pass would have masked the column and kept the rest. Closing a leak by withholding what the policy allows is an over-block, and the EF Core test written to confirm the fix is what caught it. A member past the walk keeps the Select effect the election gave it. Named in a projection it comes back transformed: the chains of members a projection names past the walk are handed to the outbound walk beside the type's own list and applied by path, so a generated row is reached as a typed one is. As a grouping key or an aggregate it is still refused, because a summary's own transform finds its columns by the type's list, which stops at four segments. Rd8EfCoreTests runs the round's findings on SQLite: the provider translates Price.Value, which is what made it a way round a denial; a masked column only a derived entity maps is masked on a hierarchy's rows; and the five-segment Include keeps its graph. Five more mutations, each red. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ed member is transformed SelectDynamic, Group, FilterDynamic and Summary hand back a query for the caller to run, which the library never sees materialized, so they are refused with TransformRequiresMaterialization on a type whose values are transformed on the way out. Whether a type is one was read from the paths the policy names. A type whose only transforms sit off them, on a member a subtype declares or five segments down, got the query, and its rows exactly as stored: the same gap the outbound walk's second pass closed for the terminals, one method call away from them. The refusal asks what a row of the type can hold as well. With no named column to list, it names the clause. A type nothing transforms anywhere still gets its query. The new test fails on the old code. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…s show it The gate records a use by path, before the query runs. A member only a subtype of the row declares, or one past the four segments the policy names, has no path the gate could ask about: handed back inside a row or a navigation kept whole, it was read with nothing written down. Two of four audited members in the probe were recorded; the subtype's and the one at segment five were not. It is the same gap the outbound walk's second pass closed for transforms, and the pass that finds those members finds these. The pass reports each audited member it meets where the policy names no path to it, once per path, and the terminal records it for Select with the effect it came back with. A member the declared types hold within the walk's depth is the gate's and is left to it, and so is a path the projection spells out, however long: a typed projection builds the real types, so without that a named deep column was recorded twice. A member the projection left out is not a read. At the cap it does what the gate does: the rows are withheld, with the clause's own refusal where the tier hides existence. The pass runs for a model that declares an audit as it does for one that declares a transform, and for neither otherwise. Seven mutations, each red. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… no attribute provider Which fragments match a path beneath a framework-typed member is about the type's shape, not about who supplied the fragments, so a resolver built over a store alone answers as one that reads attributes does. The remark on Candidates said both lookups were asked only of a resolver that reads attributes; only the read past the walk's depth is. The remark now says so, and a test pins it. Mutation M78: gating the governing-member match on ReadsAttributes turns the new test red in both tiers. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…he README and the release notes Breaking points 37 to 43 on the site and 31 to 37 in DOC.md: a path beneath a framework-typed member takes that member's policy, a transformed member no path reaches is transformed, a type first met at the depth limit is not a cycle, paths past four segments where MaxNavigationDepth is raised, a page offset past Int32, the four composable methods refused where only an unnamed member is transformed, and [DwAudit] recording a member no path names. llms.txt carries each of them in the section it belongs to, plus the audit drain's own budget, the Redis conditional commit, the patched Microsoft.Extensions.Caching.Memory, the null Having lists and the cache lookup that takes no lock. Three statements the 3.3.0 audit change had already made false are corrected, and so is the transforms page, which called the walk's depth a configured cap and a deeper field quietly unprotected. Verified: site build, version gate, Release build with no warnings, and a scan of every changed file for hidden characters. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Five packages the test project resolved carried an open advisory, none of them shipped: System.Net.Http 4.3.0 and System.Text.RegularExpressions 4.3.0 through xunit's NETStandard.Library graph, the native SQLite build EF Core's provider asks for (every SQLitePCLRaw.lib.e_sqlite3 up to 2.1.11), SSH.NET 2023.0.0 through Testcontainers 4.0.0, and on the floor leg System.Text.Json 6.0.0 through EF Core 6's SQLite provider. Each patched version is named directly, and Testcontainers moves to 4.15.0, which asks for SSH.NET 2026.0.0. Its builders take the image in the constructor now; the parameterless one is obsolete. dotnet list package --vulnerable --include-transitive is clean on both legs, and the floor leg still resolves EF Core 6.0.22. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…host's culture does
The builder writes a number into the expression unquoted, exactly as sent, and
validation checked it with TryParse in the host's culture. The two disagreed.
A thousands separator, a trailing sign, a leading plus, ".5", "5.", "NaN",
"Infinity" and an integer past UInt64 all passed and then failed in the parser
with its own exception, which a host maps to a server error; "1,5" passed on a
German host and not on an English one; and "Infinity" was written into the
expression as a name, so on a type with a member called Infinity the condition
compared two columns.
NumberValue reads a value in two steps. The first is the parser's own grammar
for a number, in the invariant culture, within the range the parser holds an
integer in. The second asks the parser whether that literal compares with the
member the condition names, because its promotion rules are its own: 1.5 does
not compare with an int?, 1e5 does not with a decimal, and no number does with
a string or with a list of numbers. The common pairs are settled without
asking. A HAVING alias has no member to ask about, so only the grammar is read
there. Everything refused is a LogicException with InvalidFormat, and every
value refused is one the parser refused: nothing that ran is refused now.
Validator.cs is one of the four frozen files. Sajjad lifted the freeze for this
change on 2026-09-21 ("fix all 4", told the fix sits there); the edit is the
two Number branches and nothing else.
A test runs 32 member paths against 30 literals and 10 operators and holds the
pipeline to the parser's answer, worked out in the test. Mutations M79 to M89
all turn it red.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…efused as one A request body can say "conditions": [null], "orders": [null] or "selects": [null]. Nothing read a list expecting that, so the null surfaced wherever it was first touched: a NullReferenceException from the sort-order check, from the ordering, or, under a policy, from the copy the sanitizer takes before it reads anything, and an ArgumentNullException from the name lookup. A host maps those to a server error. RequestShape walks a filter, a segment, a summary, a condition group, a grouping and an ordering once, and every method that takes one runs it before anything else reads the lists, with or without a policy. A null condition, sub-group, condition set, order or aggregate is a LogicException naming the list, ListOf[Conditions]MustNotHasNullEntry. A projection name that is null or blank is refused with InvalidField, as a grouping field always was, where it used to be an ArgumentNullException. A list that is itself null still means what it meant. Clone copies a null entry as a null entry rather than failing on it, so the refusal is the running method's and reads the same for a copy. Mutations M90 to M103 all turn the new tests red. One probe that recorded the old ArgumentNullException for a blank projection name now pins the refusal. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…gives no value back
A vault stores a mapping under the scope and a plain SHA-256 of the value. The
remark on DwToken.KeyFor argued a key would protect nothing, since the store is
already secret. That holds only while the store holds the values, and it does
not: it holds digests, and a tokenized column is nearly always drawn from a
space small enough to hash whole. A backup, a replica or a dump of the vault
gives back every phone number in it, and with them the value behind every token
ever handed out.
DwToken.KeyFor(scope, value, key) is an HMAC-SHA256 under a key of at least
sixteen bytes, over the scope and the value, written "hmac:{scope}:{hex}", so
one value in two scopes shares no digest. RedisTokenVault and EfTokenVault take
the key in a new constructor, and the store and the key then have to be taken
together. InMemoryTokenVault draws a key of its own, since its mappings die
with the process anyway. The unkeyed KeyFor and the unkeyed constructors are
unchanged, and their remarks now say what they are.
A deployment that adds a key keeps every token it has issued. A value first met
under the key is looked up under its unkeyed key too, and the token found there
is the one written under the keyed key. The unkeyed mapping stays until the
vault is told to retire it, which is safe once every instance holds the key:
one still running without it would mint a new token for a value whose unkeyed
mapping is gone. A vault that retires does so the first time it meets a value,
whether it wrote the keyed mapping or found it, so values adopted before
retiring began go too. Another key is another vault: every value is met for the
first time again.
Tests run the durable suite under a key on SQLite, and adoption, retiring and
an eight-way race on a real Redis and a real PostgreSQL. Mutations M104 to M118
all turn them red.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, the site, DOC.md, the README and the release notes Breaking points 44 and 45 on the site and 38 and 39 in DOC.md: a Number value is read the way the parser reads it, and a null entry in a request's list is a malformed request. The vault key is additive, so it is documented where tokenization and the vaults are: a new section on the transforms page, the use-case and provider pages, the security checklist, and sections 18, 24, 27, 28 and 31 of llms.txt, which names every new public member. Statements the three changes made false are corrected: the Number row that said TryParse in the server's culture, the ParseException examples "1,000" and "NaN", the NullReferenceException and ArgumentNullException rows, the claim that a vault's key is public so anyone can compute it, and the count of stable error strings, which is thirty-one. Verified: site build, version gate, Release build with no warnings, and a scan of every changed file for hidden characters. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>

DCMP's third change request, after
DCMP-161shipped on 3.2.0 and went into production across six modules. Two items were places DCMP still had code the package should own, three were documentation corrections — and seven rounds of review on top of them found six more security fixes and two over-blocks, each of which is in here too.Every fix has a test that goes red when the fix is removed.
From DCMP
DwPolicy.Configurethrew on every call after the first, so an integration suite starting severalWebApplicationFactoryhosts over one composition root had to readIsConfiguredfirst — a check-then-act two hosts starting at once can both pass, and DCMP shipped a lock and a hand-written re-registration around it.DryRun,IncludeTraceInResult,AuditRefusals,HashSalt,StoreFailure,MaxSnapshotAge,RefreshInterval, every cap, the exposed entity catalogue — the same types, every name each answers to, and the name each is reported under — and the provider types in order. The group floor andIncludeTraceInResultare compared by the value that applies rather than by whether somebody wrote it down; every other cap is compared as written.TokenVault,Servicesand the provider instances. A second host builds its own and no two are ever the same reference, so they stay as the first call left them — a second host runs with the first host's vault, container and rule stores, documented as a limit.AddDwPoliciesregisters the posture in force rather than the instance it just built. This suite's own bootstrap drops the lock and the check the feature replaces, which is the code DCMP is deleting.RowShape.Expressesreads the query the rows come from: an entity's EF Core model, a projection's own initializers, and a member a projection copies from the entity.Selectsis exempt: EF Core evaluates the last projection on the client, so selecting such a member returns its value exactly as before.[DwEntity(DefaultOrder)]'s own remarks still said a projected query takes no default. It has since 3.2.0.DwCaps.MinGroupSizeships on at 5, so a guarded summary drops every group with fewer than five rows and says nothing about it. Every aggregation entry leads with that now, the in-memory overload included.Clone()is public onFilter,SegmentandSummary— every node new, a condition's values the caller's own objects in a new list.What the review found on top
Seven rounds, three agents each — security, over-blocking against 3.2.0, documentation against code.
[DwAudit]could be evaded in one token. A request that sent noSelectsreceived the whole row with nothing written down, where naming the same field recorded it. A use is what the request reads now, not only what it spells out: every audited member the query hands the caller back is recorded forSelect, one event per query rather than per row. That is the members a synthesized projection keeps, the audited paths inside a member it keeps whole, and — where no projection is built, or where a dry run applies none — every audited member the row carries. A navigation nothing loads is not recorded, because the caller receives null for it. Deployments running[DwAudit]will see more events, andMaxAuditEventsrefuses rather than dropping a record — raise the cap, or drain per request withapp.UseDwPolicyAudit().CapExceededplus an origin naming the cap, against the ordinary field refusal, said which names are real and audited. UnderStrictoutside a dry run the cap refuses with the clause's own field refusal; the request still fails, and the trace still records which refusal it was.Strict. An ambiguous name said the caller's guess matched at least one real field, and is refused as an unknown name is now;AmbiguousGroupKey,TransformRequiresMaterialization,MissingHashSaltandMissingTokenVaultname the clause. The refusal audit still records the real field.MaxNavigationDepthtold a real alias from a name that matched nothing. The cap counts the canonical path, so an alias standing for a deep path was refused withCapExceededand an origin stating that path's depth, where a made-up name got the clause's own refusal. UnderStrictoutside a dry run such a name is refused as an unknown name is. A caller who wrote the path themselves already knows its depth and still meets the cap.AsExpandable(), DelegateDecompiler'sDecompile()and a host's own throughReplaceServiceexist to rewrite what EF Core cannot translate. A query that ran in 3.2.0, and still runs unguarded, was refused. The test is EF Core's own provider type from EF Core's own assembly; every other provider is left alone.Lines = o.Lines.Select(l => new LineRow { … }).ToList()holdsLineRows, notLines, so aLineRowmember the entity declares but does not map was refused although the query runs.GroupBy.Fieldsentry failed withArgumentNullExceptionwhere every sibling clause guards a blank first, so a malformed clause read as a broken endpoint rather than a four-hundred; an alias spelled like another member of the same type passed the startup scan and then merged two columns on the way out, losing one value — an alias spelling its own member in a different case still emits, because a column does not collide with itself; the startup scan itself threwAmbiguousMatchExceptionout ofValidateModel()where one name answered to two members, taking every other type's report with it; and the four blocking reads that made the 3.2.0 publish run fail on an unrelated timing test.Behaviour changes to call out
Breaking points 30 to 36, each with its own section on the breaking-changes page.
Strict(30),LastTraceis set before a request is sanitized (31),Configuretakes the same posture twice (32), the audit cap refuses like any other field underStrict(33), four more refusals name the clause underStrict(34),MaxNavigationDepthon a name the caller wrote as one token (35), and[DwAudit]records a read the request did not name (36).AmbiguousFieldNameorCapExceededunderStrict, readFieldPathoff any of the four refusals, relied on a secondConfigurethrowing, or counted audit events, sees the change.Verification
All of it re-run against this branch head.
Expresses, the provider test, the conditional reader, the trace timing, the audit recording in each of its branches, all five refusals, the alias rules, the dry-run union and the deep copy.DwPolicyOptionsandDwCapsand requires the posture comparison to refuse each one, so a value added later cannot be forgotten silently.name.enandname.arstill filter,name.isEmptyis refused rather than throwing, an order on it is refused,Selectsstill returns it, a second host with the same posture starts, a different tier is still refused.UpdatedAton a different pair of products per database, and eleven are row order on a query with noORDER BY, where the rows themselves are the same set.Review round 8, over the whole codebase (2026-09-21)
Eleven defects found by a pass over all four packages, the demo API and the docs. Each was reproduced with a probe before it was fixed, and each fix is mutation-checked. Four items first left as report-only were then fixed as well.
Security
Salary.Value,Secret.Length,Born.Year,Bag.Count,Lines.Count) matched no fragment and resolved as allowed: deny, mask, audit, cost and operator rules were all skipped. It takes the policy of the member it reads.[DwForceWhere],[DwRequireWhere]and[DwAlias]applied on a shorter path.MaxNavigationDepthraised: a denied member past four segments was not policed.[DwAudit]on a member no path names was returned unrecorded.DwToken.KeyFor(scope, value, key), new constructors onRedisTokenVaultandEfTokenVault), so a copy of the store no longer gives the values back; existing tokens are adopted, unkeyed mappings retired on request.Correctness
Int32.SummarywhoseHavingcarried a null list threwNullReferenceException.LogicExceptionInvalidFormat. (Validator.cs, freeze lifted for this change.)Selectsentry, is aLogicExceptionwith or without a policy, where it was aNullReferenceExceptionorArgumentNullException.Packaging and performance
Microsoft.Extensions.Caching.Memory6.0.2 is named directly (CVE-2024-43483 on the EF Core 6 floor).Verification of round 8
🤖 Generated with Claude Code