Skip to content

DynamicWhere.ex 3.4.0: purpose page caps, DCMP-314, and four value-type security fixes - #8

Merged
Sajadh92 merged 21 commits into
masterfrom
feat/purpose-page-caps-and-struct-select-3.4.0
Sep 25, 2026
Merged

Sajadh92 merged 21 commits into
masterfrom
feat/purpose-page-caps-and-struct-select-3.4.0

Conversation

@Sajadh92

@Sajadh92 Sajadh92 commented Sep 25, 2026 •

Copy link
Copy Markdown
Owner

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

  • Page caps per declared purpose (DwCaps.Purposes, DwPageCaps, DwPurposeCaps). A purpose (any name,
    as many as a deployment has) gets its own MaxPageSize / DefaultPageSize, selected by
    DwPolicyContext.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)

  • A transform on a struct's member was never applied. The outbound walk read the struct as a boxed copy and
    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.
  • A member of a nullable struct was not policed. The query spells it Iban.Value.Number, the policy names it
    Iban.Number, so the lookup matched nothing: denials, HasValue, and masks through Value all missed. Value
    after an application's nullable struct is now dropped wherever the policy reads a path (resolver, outbound
    walk, summary transforms, group floor, past-depth transforms).
  • The parts of a struct took nothing from the struct. With [DwDenied] on an Iban struct, Iban.Number
    was 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.
  • A typed selection beneath a list of structs returned each element whole, a [DwDenied] member included.
    Elements are now built member by member into a list or an array.

Behaviour changes

  • Under Strict, a path through a member a constructor builds is refused instead of reaching EF Core (a 500),
    and so is a clause on that member itself (ORDER BY Name, from the Copilot review). Anonymous types, framework
    types, column-mapped types, non-EF sources, Convenience and dry runs are left alone.
  • A typed selection through a struct returns its values (it returned the default), and a synthesized
    projection narrows a struct instead of leaving it out whole. Framework-typed members stay unbound.
  • A typed selection beneath a list of structs that an EF Core projection builds with a collection initializer
    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.
  • A guarded selection through a struct carries only what was named. The gate added the Id of every node a
    selected path passes through, as the builder adds a class's key, and read a struct as one: the struct's Id
    came back beside what was named, and a struct whose Id is denied had its other members refused. No builder
    adds a struct's key, so the gate now adds and gates a class's key only.
  • DwPolicyContext.Purpose is stored trimmed; a blank value is null.
  • Bind and AddDwPolicies report a value a property refuses as the InvalidOperationException they document,
    with the property's own exception inside however deep the binder wrapped it (it arrived as a
    TargetInvocationException).

Frozen files

Sixth lift, Converter.cs only, approved for the struct-select fix and extended for struct collections: two
calls, the logic in the new Source/ValueMembers.cs. Builder.cs, Validator.cs, Normalizer.cs untouched.

Verification

  • EF Core 8 leg: 3389 passed, 0 failed (container suites included). EF Core 6.0.22 floor: 2541 passed, 0 failed.
  • 39 mutation checks across the branch, each red on its own.
  • The consumer's shape (PostgreSQL 17, EF Core 9.0.10, Npgsql 9.0.4, rows projected before 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.
  • EF Core binary compatibility: all 30 member references resolve on 6.0.22, 7.0.13, 8.0.21, 9.0.10, 10.0.4.
  • Demo API sweep, 3.3.0 against head on one seeded database: 214 identical, 2 differing only in per-process
    snapshot times and tokens, 19 skipped (cache and parameter routes).
  • Version gate (release notes within nuget.org's limit), site build, hidden-character scan on the added lines,
    and the 35-section check on llms.txt.

🤖 Generated with Claude Code

Sajadh92 and others added 19 commits September 25, 2026 02:29
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>
Copilot AI lite review requested due to automatic review settings September 25, 2026 01:59
@cloudflare-workers-and-pages

cloudflare-workers-and-pages Bot commented Sep 25, 2026 •

Copy link
Copy Markdown

Deploying doc-dynamicwhere with  Cloudflare Pages  Cloudflare Pages

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

View logs

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 High severity

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 through DwPolicyContext.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.

Comment on lines +367 to +380
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;
}
}
}
Sajadh92 and others added 2 commits September 25, 2026 05:17
…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>
@Sajadh92
Sajadh92 merged commit 07be71f into master Sep 25, 2026
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants