Repository navigation
DynamicWhere.ex 3.4.0: purpose page caps, DCMP-314, and four value-type security fixes - #8
Conversation
DwCaps.Purposes gives a declared purpose its own MaxPageSize and DefaultPageSize, so an export reads every match in one statement while the screens keep their page. Any name, as many as a deployment has, matched trimmed and without regard to case; an undeclared purpose runs under the deployment's caps. A purpose value is at least one, binds from configuration, freezes with the posture and is compared by the caps that apply on a second Configure. DwPolicyContext.Purpose is stored trimmed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…d under Strict EF Core follows a member only through an initializer's binding, so a clause on Name.Ar over new LocalizedText(t.NameAr, t.NameEn) failed inside the provider: a 500 where the strict tier promises a refusal (DCMP-314). The projection shape now records what a constructor with arguments builds, at every level including a positional record row, and refuses the members it leaves unbound. Anonymous types, framework types, types the model stores as a column and sources with no entity behind them are left alone. The Dv5-E docs probe follows the new rule. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A struct is a value, not a navigation: with [DwDenied] on an Iban struct, Iban.Number could be filtered on, sorted and grouped by, an oracle on a sealed field. A path beneath a member holding an application's own struct, or a collection of them, now takes that member's denials, operators, cost and audit beside its own, as a path beneath a framework type has since 3.3.0. A transformed struct refuses Select, Group and Aggregate on its parts; a part's own transform still applies. Classes stay navigations. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The typed projection skipped every value-typed member a path went beneath, so Select(["Name.Ar"]) over a LocalizedText struct returned an empty name with nothing refused, guarded or not. An application's own struct is now built member by member, as a class navigation is; a framework-typed member stays unbound, since binding it whole would carry more than the path names. Sixth lift of the Converter freeze, approved for this fix: one call in Converter.cs, the logic in ValueMembers.cs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…only what it names The typed projection bound every collection of values whole, so Pairs.Shown over a List<Pair> handed back each Pair whole, a [DwDenied] member included, in both tiers, while the policy had approved only Pairs.Shown. Present since the policy layer shipped. Each element of a collection of an application's own structs is now built member by member into a list or an array; a null collection stays null, and a collection of nullable structs or of another shape is left unbound. Scalar collections are still bound whole. Second call of the Converter lift, approved for this fix. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The core's 3.3.0 entry is cut to its heading, as older entries are, so the notes stay far under nuget.org's 35,000-character limit. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A struct is read as a copy in a box, and the compiled setter unboxed a second copy to write into, so every mask, generalization, format, truncation, default or mutation declared on a struct's member landed on a temporary and the stored value was emitted: in both tiers, from memory and from EF Core, since the policy layer shipped. The setter now writes into the box in place, and both passes of the outbound walk write each changed struct back where it was read from, innermost first: a member, a list or array position, an outer struct. A member the policy names is transformed once, by the pass along its path. A struct that changed and cannot be written back (a member with no setter, a collection that cannot be written by position, a dictionary's value) fails the query rather than escaping. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eaving it out The gate read any type without a parameterless constructor as one the core could not build, so a struct holding a denied member was left out whole and its allowed members came back empty. The typed projection builds an application's own struct member by member now, held directly or in a list or an array, and the gate narrows it as it narrows a class. A nullable struct, or one in a collection of another shape, is still left out whole. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… names it A query reaches a member of an Iban? through the nullable, Iban.Value.Number, while the attribute walk, the transforms and the fragments name it Iban.Number. Looked up by the query's spelling the path matched nothing: a [DwDenied] on the nullable member or on a member of the struct, and a mask on one, did not reach it, in every clause and both terminals, since the policy layer shipped. The typed selection fix in this release had widened it to the typed terminal. AttributePolicyProvider.PolicyPath drops a Value after a nullable struct of the application's; the resolver, the outbound walk's carried paths, the summary's key and aggregate transforms, the group floor and the past-depth transforms all read the path that way. Governing reads a nullable's own members, HasValue included, as the nullable member. The walk steps through Value on a dynamic projection's generated row, which holds the member where the query spelled it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ception it documents The binder calls each setter by reflection, so a value the property refused arrived as a TargetInvocationException, and the same value inside a purpose as an InvalidOperationException. Both now arrive as InvalidOperationException with the property's own refusal inside. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… 3.3.0 scope in the 3.4.0 notes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…apped it Microsoft.Extensions.Configuration.Binder 8 reports a value inside a dictionary, such as a purpose's page cap, in an InvalidOperationException of its own around the TargetInvocationException, so the catch that looked only at the top let it through as the binder's exception, without the documented message and with the setter's refusal two levels down. Bind now walks the chain and reports every refused value one way: the message starts "A configured policy value was refused:" and InnerException is the property's own exception. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…notes Labelled and ordered with the other three security fixes, as the docs label it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Page caps per declared purpose (DwCaps.Purposes, DwPurposeCaps, DwPageCaps) as reference, configuration, a use case and breaking points; the four security fixes on the value-type axis: the parts of a struct take the struct member's policy, a member of a nullable struct is policed as the policy names it, a transform on a struct's member is applied, and a typed selection beneath a collection of structs carries only what it names; under Strict a path through a member a constructor builds is refused; a typed selection through a struct returns its values and the gate narrows a struct; the list-initializer limit; a context stores its purpose trimmed; and Bind reports every refused value as InvalidOperationException. Version strings to 3.4.0. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… class's KeepCarriedKeys and DeniedKey add the Id of every node a selected or narrowed path passes through, because the projection builder adds the key of each class it builds. It never adds a struct's: the typed builder builds a struct with exactly what was named and the dynamic one carries only the named path. Read as a class, a struct holding an Id had that key added to every guarded selection through it, so DeniedBy.Value.Why came back with the struct's Id beside it, typed and dynamic, and a struct whose Id is denied had a selection of its other members refused and was left out of a synthesized projection. Fail-closed either way, but a member the caller did not name, on a consumer's real row (PrincipalRef? DeniedBy). Struct nodes are skipped now; a class's key, a class element's inside a struct included, is added and gated as before. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The release notes, the 3.4.0 history, breaking point 50, DOC.md point 43 and the README say the gate no longer adds a struct's Id; the 3.2.0 text on the builder's added key now names a class's key only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Deploying doc-dynamicwhere with
|
| Latest commit: |
7fb0e26
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://54c5e173.doc-dynamicwhere.pages.dev |
| Branch Preview URL: | https://feat-purpose-page-caps-and-s.doc-dynamicwhere.pages.dev |
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
RowShape’s new constructor-built member tracking appears to miss refusing clauses on the constructed member itself for single-segment paths, which can still allow strict-tier clauses to reach EF Core and fail in-provider.
Get a fresh assessment by requesting another Copilot review.
Review effort: Lite
Findings: 1
Open (1)
What changed in this PR
This PR bumps DynamicWhere.ex to 3.4.0 and introduces purpose-scoped page caps plus a set of value-type policy enforcement and transform correctness/security fixes (struct members, nullable structs, struct collections, and EF Core projections built via constructors). It also updates the docs site content/versioning and adds extensive regression tests covering the newly enforced/refused behaviors.
Changes:
- Add page caps per declared purpose via
DwCaps.Purposes/DwPurposeCaps/DwPageCaps, applied throughDwPolicyContext.Purpose. - Fix struct/nullable-struct policy+transform correctness (policy-path normalization dropping
Value, struct member policy inheritance, transform write-back into boxes/collections, typed selection building structs/struct-collections member-by-member). - Harden EF Core projection shape refusal (constructor-built members) and improve configuration binding refusal reporting.
| File | Description |
|---|---|
| README.md | Adds 3.4.0 highlights and bumps install snippets to 3.4.0. |
| OfficialWebsite/package.json | Docs site version bump to 3.4.0. |
| OfficialWebsite/lib/nav.ts | Updates displayed docs version to 3.4.0. |
| OfficialWebsite/app/docs/policies/use-cases/page.tsx | Documents purpose page caps and constructed-member behavior. |
| OfficialWebsite/app/docs/policies/transforms/page.tsx | Documents struct-member transform fix and write-back behavior. |
| OfficialWebsite/app/docs/policies/security/page.tsx | Documents new/closed security cases for structs/nullable structs/collections. |
| OfficialWebsite/app/docs/policies/providers/page.tsx | Bumps provider install commands to 3.4.0. |
| OfficialWebsite/app/docs/policies/configuration/page.tsx | Documents purpose caps, struct narrowing/key gating, and bind refusal shape. |
| OfficialWebsite/app/docs/policies/attributes/page.tsx | Documents struct-part policy inheritance and nullable-struct Value normalization. |
| OfficialWebsite/app/docs/policies/admin/page.tsx | Bumps AspNetCore package version to 3.4.0. |
| OfficialWebsite/app/docs/page.tsx | Updates landing page version/install snippet to 3.4.0. |
| OfficialWebsite/app/docs/installation/page.tsx | Updates installation snippets to 3.4.0. |
| OfficialWebsite/app/docs/extensions/select/page.tsx | Documents typed selection behavior for structs and struct collections (3.4.0). |
| OfficialWebsite/app/docs/classes/summary-result/page.tsx | Documents purpose-default page reporting in summary results. |
| OfficialWebsite/app/docs/classes/segment-result/page.tsx | Documents purpose-default page reporting in segment results. |
| OfficialWebsite/app/docs/classes/filter-result/page.tsx | Documents purpose-default page selection behavior. |
| OfficialWebsite/app/docs/ai/page.tsx | Updates reference version statement to 3.4.0. |
| DynamicWhere.Tests/ValueMemberSelectTests.cs | Adds tests for struct/nullable struct typed selection binding. |
| DynamicWhere.Tests/Policies/StructTransformTests.cs | Adds tests for struct-member transforms, write-back, and fail-closed cases. |
| DynamicWhere.Tests/Policies/StructPartPolicyTests.cs | Adds tests ensuring struct parts inherit struct-member policy and precedence rules. |
| DynamicWhere.Tests/Policies/StructKeyTests.cs | Adds tests ensuring struct selections don’t implicitly carry/gate struct Id. |
| DynamicWhere.Tests/Policies/StructCollectionSelectTests.cs | Adds tests for typed selection beneath collections of structs. |
| DynamicWhere.Tests/Policies/S4SecurityProbesB.cs | Extends posture-diff probes to include Caps.Purposes. |
| DynamicWhere.Tests/Policies/Rd8UnwalkedPathTests.cs | Updates tests for new Governing(...).Reads/Above API. |
| DynamicWhere.Tests/Policies/PurposePageCapsTests.cs | Adds end-to-end tests for purpose page caps, binding, freezing, and trimming. |
| DynamicWhere.Tests/Policies/Pr6PrecisionProbes.cs | Updates ResultTransformer.Summary call for new signature. |
| DynamicWhere.Tests/Policies/PolicyConfigurationTests.cs | Updates tests to expect InvalidOperationException with inner setter exception. |
| DynamicWhere.Tests/Policies/NullableStructPolicyTests.cs | Adds tests for dropping Value in policy-path resolution and transform application. |
| DynamicWhere.Tests/Policies/ConstructedMemberTests.cs | Adds tests for refusing strict-tier clauses through constructor-built members. |
| DynamicWhere.Tests/Policies/ConfigureOnceTests.cs | Extends posture comparison tests to cover purpose page caps equivalence/diffs. |
| DynamicWhere.ex/Source/ValueMembers.cs | New helper for struct + struct-collection typed projection binding decisions. |
| DynamicWhere.ex/Source/Converter.cs | Integrates ValueMembers into typed projection builder. |
| DynamicWhere.ex/Policies/Source/RowShape.cs | Tracks constructor-built members and refuses unreachable paths under strict. |
| DynamicWhere.ex/Policies/Source/ResultTransformer.cs | Fixes transform lookup for nullable-struct Value paths via policy-path normalization. |
| DynamicWhere.ex/Policies/Source/PolicyQueryable.cs | Normalizes projected paths for transforms; plumbs entityType to transformer/floor. |
| DynamicWhere.ex/Policies/Source/GroupFloor.cs | Looks up group-floor transforms using normalized policy paths. |
| DynamicWhere.ex/Policies/Source/FilterSanitizer.cs | Applies purpose page caps to default paging and summary floor injection. |
| DynamicWhere.ex/Policies/Resolution/PolicyResolver.cs | Normalizes lookup paths (drops nullable struct Value); adds “above/own” fragment handling. |
| DynamicWhere.ex/Policies/Resolution/AttributePolicyProvider.cs | Implements PolicyPath normalization and expanded Governing metadata for struct/framework paths. |
| DynamicWhere.ex/Policies/Masking/MutatorCache.cs | Fixes boxed struct setter compilation using Expression.Unbox. |
| DynamicWhere.ex/Policies/Masking/GraphWalker.cs | Ensures struct transforms write back into owners/collections; normalizes projected paths. |
| DynamicWhere.ex/Policies/Context/DwPolicyContext.cs | Trims/stores purpose consistently; blank purpose becomes null. |
| DynamicWhere.ex/Policies/Config/DwPurposeCaps.cs | New purpose-name → DwPageCaps dictionary with trim/case-insensitive semantics and freezing. |
| DynamicWhere.ex/Policies/Config/DwPolicyConfiguration.cs | Wraps binder setter failures as documented InvalidOperationException with inner refusal. |
| DynamicWhere.ex/Policies/Config/DwPolicy.cs | Includes purpose caps in posture comparison; compares by effective caps. |
| DynamicWhere.ex/Policies/Config/DwPageCaps.cs | New per-purpose MaxPageSize/DefaultPageSize with validation + freeze. |
| DynamicWhere.ex/Policies/Config/DwCaps.cs | Adds Purposes, purpose-aware MaxPageSizeFor/DefaultPageSizeFor, and freezes nested caps. |
| DynamicWhere.ex.Policies.Redis/DynamicWhere.ex.Policies.Redis.csproj | Bumps version to 3.4.0 and updates release notes. |
| DynamicWhere.ex.Policies.EntityFrameworkCore/DynamicWhere.ex.Policies.EntityFrameworkCore.csproj | Bumps version to 3.4.0 and updates release notes. |
| DynamicWhere.ex.Policies.AspNetCore/DynamicWhere.ex.Policies.AspNetCore.csproj | Bumps version to 3.4.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.
| if (_constructed is { Count: > 0 }) | ||
| { | ||
| // Asked first, at every level from the row down: whatever else builds the member, a | ||
| // constructor building it in one place is where EF Core stops. | ||
| for (int i = 0; i < segments.Length; i++) | ||
| { | ||
| string level = string.Join(".", segments, 0, i); | ||
|
|
||
| if (_constructed.TryGetValue(level, out HashSet<string>? bound) && !bound.Contains(segments[i])) | ||
| { | ||
| return false; | ||
| } | ||
| } | ||
| } |
…s refused too The shape check refused every path beneath a member a projection builds with an application type's constructor, and not the member: ORDER BY Name over new LocalizedText(t.NameAr, t.NameEn), or Label IsNotNull over a class built the same way, passed and failed inside EF Core, a five-hundred where the strict tier promises a refusal. EF Core can neither order by nor compare a value it would have to build on the client, so the member itself is refused as an unknown name is. Selecting it still works. Found by the Copilot review of PR #8. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-struct sort is a limit Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

DynamicWhere.ex 3.4.0
Four packages at 3.4.0. A consumer's change request (a 500 on a composed member) and a request for export
page caps, plus review round 9 (owed from 3.3.0). The review found four defects on the value-type axis, all
present since the policy layer shipped in 3.0.0, and each is a security fix.
New
DwCaps.Purposes,DwPageCaps,DwPurposeCaps). A purpose (any name,as many as a deployment has) gets its own
MaxPageSize/DefaultPageSize, selected byDwPolicyContext.Purpose, so an export reads every match in one statement while the screens keep their page.Trimmed, case-insensitive; an undeclared purpose runs under the deployment's caps; values at least 1; only the
page caps change; binds from configuration with
ErrorOnUnknownConfiguration; frozen with the posture;compared by the caps that apply on a second
Configure.Security fixes (present since 3.0.0)
the compiled setter unboxed a second copy, so masks landed on temporaries and the stored value was emitted, in
both tiers, from memory and from EF Core. The setter now writes into the box, and both passes write each changed
struct back, innermost first. Fail-closed: a struct that changed and cannot be written back (no setter, a
HashSet, a dictionary value) fails the query.Iban.Value.Number, the policy names itIban.Number, so the lookup matched nothing: denials,HasValue, and masks throughValueall missed.Valueafter an application's nullable struct is now dropped wherever the policy reads a path (resolver, outbound
walk, summary transforms, group floor, past-depth transforms).
[DwDenied]on anIbanstruct,Iban.Numberwas filtered on (an oracle), sorted by, grouped by and returned by a dynamic projection. A path beneath a
member holding an application's own struct now takes that member's denials, operators, cost and audit, beside
its own; never its alias, required filter, forced scope or description. Classes stay navigations.
[DwDenied]member included.Elements are now built member by member into a list or an array.
Behaviour changes
and so is a clause on that member itself (
ORDER BY Name, from the Copilot review). Anonymous types, frameworktypes, column-mapped types, non-EF sources, Convenience and dry runs are left alone.
projection narrows a struct instead of leaving it out whole. Framework-typed members stay unbound.
now fails in EF Core's translation, as the dynamic terminal always did, where 3.3.0 returned every element
whole. A list read from a query, a list in memory and an array are built element by element.
Idof every node aselected path passes through, as the builder adds a class's key, and read a struct as one: the struct's
Idcame back beside what was named, and a struct whose
Idis denied had its other members refused. No builderadds a struct's key, so the gate now adds and gates a class's key only.
DwPolicyContext.Purposeis stored trimmed; a blank value is null.BindandAddDwPoliciesreport a value a property refuses as theInvalidOperationExceptionthey document,with the property's own exception inside however deep the binder wrapped it (it arrived as a
TargetInvocationException).Frozen files
Sixth lift,
Converter.csonly, approved for the struct-select fix and extended for struct collections: twocalls, the logic in the new
Source/ValueMembers.cs.Builder.cs,Validator.cs,Normalizer.csuntouched.Verification
ApplyPolicy,Strict): 16 of 16 checks pass. A reproduction of its nullable struct rows (EF Core 9.0.10 on SQLite), on
3.3.0 and on this branch under Strict: filter and sort refused on both, so the nullable-struct defect was not
live there. The same reproduction found the struct-key behaviour above; guarded output now equals the core's.
snapshot times and tokens, 19 skipped (cache and parameter routes).
and the 35-section check on
llms.txt.🤖 Generated with Claude Code