Skip to content

3.2.0: DCMP change request 2 (DW-12, DW-13, DW-15) and the fail-opens found checking it - #5

Merged
Sajadh92 merged 22 commits into
masterfrom
fix/projected-rows-3.2.0
Sep 20, 2026
Merged

Sajadh92 merged 22 commits into
masterfrom
fix/projected-rows-3.2.0

Conversation

@Sajadh92

Copy link
Copy Markdown
Owner

DCMP runs every read as IQueryable<Entity>, then a SQL projection into its own row type, then ApplyPolicy(ctx).ToListAsync(filter). Step 0 of DCMP-161 ran that shape on PostgreSQL 17 and found two blocking gaps and one missing feature. Checking the first gap, and seven rounds of independent review of the fix, found more ways a [DwDenied] value reached a result. Most were already in 3.1.0. All are fixed here, and each has a test that goes red when its fix is removed.

From DCMP

  • DW-12. With a select-denied field and no Selects, the synthesized projection kept scalars only, so every nested object and list of a projected row came back null or empty.
    • It now carries what an unguarded call would return, less what the policy withholds.
    • An object member is kept whole when nothing it can hold is denied, and narrowed when something is.
    • This holds wherever the source carries the member: a row a projection builds, a row in memory, and an entity's columns, owned and complex members, read from the EF Core model.
  • DW-13. [DwEntity(DefaultOrder)] applies to a projected source when its outermost Select builds the type in an initializer that assigns every default field a column.
    • A column is a mapped member, read directly, through reference navigations or through EF.Property.
    • A computed value never takes the default, so a default can never make a query fail that ran unguarded.
  • DW-15. Every async terminal, guarded and unguarded, has CancellationToken overloads.
    • They are overloads rather than optional parameters, so 3.1 binaries still bind.
    • On EF Core, the dynamic and summary reads and the summary count go through EF Core's async operators.

Security fixes: a denied value that reached the result

  • A denial beneath a member, with no Selects, was never enforced, in both tiers. That covers projected lists, rows in memory, and an entity's included, auto-included, lazily loaded or owned members.
    • What loads is read from the EF Core model.
    • Includes named from another root count, re-rooted by Select(o => o.Customer), SelectMany or Join, as do projections hidden behind another Select.
    • So does an object a reshaping lambda gets from an application's method, or captures: another query with its own include or projection, or an object in memory.
  • A query through a provider that wraps EF Core's (LinqKit's AsExpandable, DelegateDecompiler's Decompile) ran tracking, since EF Core's AsNoTracking hands such a query back unchanged. The context filled in navigations it already held, denied ones included, and a masked value became a pending change the next SaveChanges would write. AsNoTracking now goes into the query itself.
    • Every lazy-loader form counts: proxies, ILazyLoader, the constructor-taken delegates (sync and async) and an injected DbContext.
    • An unmapped getter over a mapped field counts.
  • Subtypes and other declarations. A denied field on a derived entity, a subclass, an open generic subtype or an application's subclass of a framework class was invisible through the base type. So was a [DwDenied] on another declaration of the member: an override of either accessor, a public member a subtype hides with new, and an interface member or its implementation, explicit, inherited or declared by an open generic class, through a variant instantiation too (IFeed<VisaCard> for a member typed IFeed<Card>). A query over a hierarchy root, a base-typed member, a framework collection, a projection constructing a subtype, and a rule on a subtype's field all returned it.
  • A "*" deny with exact allows treated every path the walk never asked about as allowed: past four segments, around a cycle, with no setter, or on a subtype. Each such path is now asked of the policy.
  • Selects naming a member carried denials the gate could not see:
    • a navigation whose key is denied;
    • a member typed IReadOnlyList<T>;
    • a denial past four segments, in a framework generic, or in an entity navigation's owned chain or converted column;
    • Main.Lead not gating Main.Id.
  • A field denied at the top of T holding no simple value (a blob, list, owned or JSON member) synthesized nothing.
  • A converted value that can hold any object, a column or a member of an EF 8 complex property, is left out when a projection is built for another field, as 3.1.0 left it out: a value converter can return an application type carrying a denied field.
  • An application's own collection class, generic or not, hid its own denied members, and two members sharing a name (one hidden with new under another type, or spelled in another case) hid the second one's.
  • The attribute walker read any namespace starting with "System" as the framework's.
  • Also closed: a rule in another letter case, a non-generic collection, and two members differing only in case.

Precision: what does not change

The first cut projected far more than needed. These were fixed so ordinary queries read as they did in 3.1.0:

  • A denial beneath a navigation nothing loads asks for nothing.
  • A member that can hold object, such as a geometry, JSON bag or BitArray column, asks for no projection.
  • A reshaped chain with no include and no built, returned or captured object is read from the model. Specifications, repository queries, FromSql, Set through a context interface and query functions are read as EF Core reads them; query-syntax joins, lets, left joins and composite keys build nothing; and a value that only feeds a predicate or a key (EF.Functions.Like, new DateTime(...)) does not count.
  • BitArray and the framework's string collections hold values, not objects.
  • A projected member is read as the type its initializer constructs.
  • Under a "*" deny, members whose every path is named are kept.

Behaviour changes to call out

  • Entity navigations are left out of a synthesized projection. On an entity, once a denial needs a projection, navigations EF Core does not own are left out, included ones too, as 3.1.0 already did for a top-level denial. The trace now records each one.
  • Hierarchy roots come back as T. A root whose derived type declares a denied field returns root-type rows. Over an abstract root the typed terminal fails with SelectTypeMustHaveParameterlessConstructor; the dynamic terminal works.
  • A [DwDenied] on an override, a member hidden with new, or an implementation denies the base path for every row, in every clause. A new member counts because whether it reads the member it hides cannot be told from outside.
  • Rows in memory are projected whenever a loaded subtype declares a denial, since the policy does not inspect rows.
  • A narrowed null reference comes back as an empty object, as for dotted Selects.
  • ToListAsync(filter, default) no longer compiles. default is ambiguous between bool and CancellationToken.

Known limits, documented

  • A member typed object, a framework interface or a non-generic collection is opaque: it never asks for a projection. With nothing else denied, a converter returning an application type through an object column, or an unmapped object getter over a private navigation, comes back as loaded.
  • A denial on an override or a new member counts for every loaded subtype, including a class EF Core does not map, such as a view model deriving from an entity.
  • Types from an unloadable AssemblyLoadContext stay referenced by the policy caches.
  • A repository or specification method in a reshaping lambda runs once more per guarded read, and one that returns a different query each call is enforced as it answered the guard.
  • A wrapping provider that hides the EF Core root behind its own expression runs tracking; LinqKit and DelegateDecompiler show it.
  • On EF Core 6, a reshaped query whose projection EF Core 6 cannot translate fails guarded, where it ran unguarded.

Verification

  • Both test legs pass locally: EF Core 8 (2522 tests) and the EF Core 6.0.22 floor (1818). Every review round's probes are kept as the Review*Tests files.
  • Mutation checks: removing any fix or precision rule turns tests red. The exceptions are defensive checks no current model reaches, such as an unbound type parameter and narrowing into a type the core cannot build.
  • DCMP's acceptance holds: a DCMP-shaped probe on EF Core 9, Npgsql 9 and PostgreSQL 17 gives the expected answers. B1b keeps Name.En, Contents and holders, with the forced scope returning the tenant's row and the platform template. C1, E1, A9 (through a shadow property), A10, A11 and B4 hold, and a canceled token stops the typed, dynamic and summary reads.
  • Gates: version gate and site build pass, and the hidden-character scan is clean.
  • Reviews: my own pass, plus independent security, over-blocking, correctness and docs-against-code reviews in seven rounds, and a 264-call behavioural sweep of the demo API against 3.1.0 (no answer differs). EF Core binary compatibility was checked against 6, 7, 8, 9 and 10.

🤖 Generated with Claude Code

Sajadh92 and others added 22 commits September 19, 2026 04:28
…h one is enforced

DCMP's second change request (DW-12, DW-13, DW-15), and three fail-opens found while checking it.

DW-12. With a select-denied field and no Selects, the projection synthesized for a guarded query
kept scalars only, so every nested object and list of a row projected before ApplyPolicy came
back null or empty. A synthesized projection now carries what an unguarded call would return,
less what the policy withholds. A member holding a value is kept when allowed, and so is a
collection of values (byte[], List<string>). A member holding an object, or a list of them, is
kept whole when nothing beneath it is denied, and narrowed as a caller naming it would have it
narrowed when something is. That applies where the source carries the member: every member of a
projected or in-memory row, and an entity's owned and complex members, read from the EF Core
model. An entity's other navigations are left out, as before. What a source carries is read
from the query once (RowShape), so the sanitizer stays pure.

F1, both tiers. A denial only beneath a member synthesized nothing, so the whole row came back
and a projected list, an object in memory, an included navigation or an owned member carried the
denied value out. Everything beneath every object member is now gated, whether or not the source
carries it.

F2, Convenience. Naming a navigation whose element key is denied narrowed the key away, and the
core's projection builder added it back. That narrowing is now refused in both tiers, as naming
a sibling of the key already was. A synthesized projection leaves such a member out whole.

F3, both tiers. The projection gate read collections through a narrower list than the attribute
walker, so a member typed IReadOnlyList<T> hid every denial beneath it. The gate now reads a type
the way the walker does (AttributePolicyProvider.Peeled / NavigationTypeOf). A narrowing the core
cannot validate is refused, and a synthesized projection leaves the member out whole. A member in
memory or an EF Core complex property is never narrowed, since the core's narrowing needs EF Core
and compares the member to null.

DW-13. A projection hid the declared default order whatever it assigned. A Select that builds the
type in an initializer and assigns every field the default names, at every level of a nested
path, now takes the default. A Select, or a Filter carrying a projection, composed on the guarded
handle keeps the chain unordered. A composed Filter whose orders were all dropped gets no default
later in the chain, as a composed Order already did not.

DW-15. Every async terminal, guarded and unguarded, has overloads taking a CancellationToken:
Filter, dynamic Filter, Summary and Segment. They are overloads, not an optional parameter, so
code compiled against 3.1 still binds. The token reaches the count and the read. A dynamic or
grouped read on EF Core now runs through EF Core's asynchronous operators, reached by reflection
(AsyncReads), instead of a synchronous read on a pool thread.

The EF Core query-root finder moves from SegmentComposer to QueryRoot so both can read the model.
No frozen file is touched. Each security fix was mutation-checked: removing it turns the new
tests red. EF Core 8 leg: 2102 passed. EF Core 6.0.22 leg: 1409 passed. A DCMP-shaped probe on
EF Core 9, Npgsql 9 and PostgreSQL 17 gives the answers DCMP's acceptance asks for.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
All four packages move to 3.2.0 together. The core's release notes describe the three security
fixes, the synthesized projection that keeps what the source carries, the default order on
projected sources and the CancellationToken overloads. The companions say they changed nothing
of their own, and the ASP.NET Core package says what /simulate now reports.

llms.txt carries the new overloads and the method count (28), the ambiguity a default literal
now causes, the enforcement table's new rows and the "allowed members" rule that replaces
"allowed scalars", the default-order rule for projections, the trace's "left out whole"
decisions, the simulator's entity reading, the 3.2.0 history entry, and two limits: an entity's
navigations are left out once a projection is needed, and a type held in a dictionary is not
policed through it. The trap that said nothing takes a CancellationToken is replaced.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…y query

Found by my own review pass. RowShape counted any Select in the chain as a projection whose
every member the source carries. A Select that hands back the entity itself,
Select(o => o.Customer) or Select(s => s), builds no row: its navigations hold a value only when
something includes them. Kept in a synthesized projection they were loaded, so the guarded query
returned more than the unguarded one. Allowed data only, but a navigation the caller never asked
for, and possibly a large one. A source now counts as projected only when the outermost Select
constructs the row, in an initializer or with a constructor. Anything else is read as an entity
query from the EF Core model. Removing the check turns the new test red.

AsyncReads reached EF Core's CountAsync and ToListAsync through MethodInfo.Invoke, which wraps
anything the operator throws before its task exists in a TargetInvocationException. The async
Summary used to count synchronously, so a query EF Core could not translate failed with EF
Core's own exception, and now it does again (BindingFlags.DoNotWrapExceptions). A provider that
fails while building the query proves it; wrapping turns the test red.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Three reviews of the first cut: security, correctness, and a sweep of the demo API against
master. The first cut kept object members whole wherever the gate found nothing denied beneath
them, and asked for a projection whenever a denial sat anywhere beneath a member. Both were wrong.

What asks for a projection. Only a denial whose value can reach the result. On an entity that
means one beneath a column, an owned or complex member, or a navigation something loads: an
Include or ThenInclude on the chain (read from the expression, and any form it cannot read
counts as loading everything), an automatic include, or a lazy loader (an ILazyLoader service
property, which proxies add too). A denial beneath a navigation nothing loads never leaves the
database. In a connected model such denials sit beneath almost every type, and the first cut
projected nearly every entity query: abstract and table-per-hierarchy types, types with no public
parameterless constructor, and converter-mapped members all broke. They are back to 3.1.0's
shape. A lazy loader could also carry a denied value out after the query; a navigation it can fill
now counts as loaded.

What a member may be kept whole around. The gate asks the providers' fragments for every denied
path beneath a member, not only the paths its walk produces. That finds a denial beneath a
property with no setter, one reached around a cycle by a runtime rule, and one deeper than the
walk. It also scans every type the member can hold, however deep and through the type arguments
of framework generics such as Dictionary<string, T>, for a field denied for Select and for a
member typed object. A forced scope beneath a member cannot be applied to what it holds, so such
a member is left out whole. So is one with a transform on a property that has no setter.

What a projection holds. A projected row carries the members its initializer assigns: an
unassigned one used to be narrowed through EF.Property, which EF Core cannot translate. A member
is narrowed only where the core's narrowing translates: an object the initializer builds, a
subquery list the core can bind (not an array or a set), or a navigation that is neither complex
nor stored as JSON. An entity keeps every mapped column, converted and JSON ones included, whole,
and no member EF Core does not map, which made EF Core read the denied column to compute it. Rows
in memory keep their values and leave their objects out, as in 3.1.0, since a kept object is the
caller's own and a transform would change it in place. A member named with a word the parser
keeps is skipped. The walk now stops where the walker stops, four segments, so a narrowing never
projects a field no policy can speak about.

Found in passing, all present in 3.1.0:
- The walker read a namespace starting with "System" as the framework's, so SystemsCorp.Payroll
  got no fragment beneath its types and a [DwDenied] field there was returned and filterable.
- Naming Main.Lead kept it whole without gating Main's key, which the builder adds.
- A denied member that is not a simple value (a blob, an owned object, a JSON column) never asked
  for a projection, so with nothing else denied it came back. The demo API read Employee's denied
  WorkSchedule this way.

DW-13: a default whose field the projection computes with an application method is not applied;
EF Core evaluates such a method on the client and cannot order by it.

Every rule was mutation-checked. EF Core 8 leg: 2131 passed. EF Core 6.0.22 leg: 1435 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… on its own

It filters the rows that hold the member, as it always has for a list returned whole, and asking
would leave out every included list of a scoped child type in a multi-tenant model. When a
projection is needed for another reason, such a member is still left out whole.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hanged

llms.txt section 17 now states when a denial asks for a projection (only where its value can
reach the result: an entity's column, owned or complex member, or a navigation the query loads; a
projection's assigned members; anything in memory), what each source keeps, when a member is kept
whole, narrowed or left out whole, and what caller-written Selects refuse. The 3.2.0 history
entry lists the three fail-opens the review found in passing, and the limits add the forced
scope on a list's element type and members the policy cannot see into. Two stale lines are
corrected: the async Summary no longer counts synchronously, and trap 37 no longer says every
projected query goes unordered.

The core's release notes are rewritten to match, and the ASP.NET Core package's say what
/simulate now reports.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…rrowing

From my own pass over the round-2 change.

A rule on a path the type does not have, one written for a member since removed, counted as a
denial beneath the member above it, so the strict tier refused naming that member. Such a rule
holds nothing a projection could carry, and 3.1.0 never looked at it. It is skipped now.

A member named in Selects is narrowed when a transform beneath it lands on a property with no
setter. Where the source cannot be narrowed that way, it was refused as a denied field, though
nothing is denied. It is kept whole instead, as in 3.1.0.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…oading proxies

From my own pass. Include paths were read only from the query's own chain. An Include on a
join's inner source loads a navigation of the rows the join returns, and EF Core applies it, so a
denial beneath that navigation asked for no projection and came back. Every Include and
ThenInclude in the tree is counted now; one off the chain the paths are read from means every
navigation counts as loaded.

Lazy-loading proxies were covered by reasoning only: they give each entity type an ILazyLoader
service property, which is what the check reads. A test with UseLazyLoadingProxies proves it,
and the test project references Microsoft.EntityFrameworkCore.Proxies at the leg's EF Core
version. Turning either check off turns a test red.

EF Core 8 leg: 2134 passed. EF Core 6.0.22 leg: 1438 passed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…vigation

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The four async method pages carry the CancellationToken overloads, the token reaching the count
and the read, the default-literal ambiguity, and the dynamic and summary reads going through EF
Core. The extensions index counts 28 methods; README and the landing page counted 17 and 21.

The configuration page says when a request with no Selects needs a projection, what each source
keeps, when a member is kept whole, narrowed or left out whole, and what a caller naming a
member is refused. The security page lists the denials the gate could not see and the limits
that remain. The breaking-changes page adds points 25 to 29. The attributes page gives the
default-order rule for projections, and the admin page says how /simulate reads a type.

README gains the 3.2.0 highlights; DOC.md carries the same sections as the site. llms.txt's
table row and simulator limit now say what section 17 says.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ber can hold

A security re-review of b71b8c4 found nine ways a value denied for Select still
reached the result. Four were new in 3.2.0, members it kept whole that 3.1.0 dropped;
five were already in 3.1.0. A docs review found a tenth and an over-block. All fixed:

- A member declared as a base type or interface holds its subtypes. Their denied
  fields are read too: every loaded subtype for a projected or in-memory row, the
  model's derived types for an entity. A hierarchy root with a derived type's denial
  is projected to the root type; an abstract one is refused.
- A "*" deny with exact allows denies every path the walk never asks about: past four
  segments, around a cycle, a property with no setter. Such a member is never kept
  whole, and triggers the projection when its value loads.
- A rule spelled in another letter case reaches a projected row's assigned member.
- A collection that is not generic, and a framework interface, can hold anything.
- An include named from another root, re-rooted by Select, SelectMany or Join, and a
  projection hidden behind another Select, count as loading every navigation.
- An initializer after a constructor with arguments counts every member as assigned.
- A lazy loader the constructor takes, kept in a field or any property, counts.
- What a named or included entity navigation carries is read from the model: its
  columns (a converted Dictionary of a policed type included), owned chain at any
  depth, and what loads beneath it. A denial beneath a navigation nothing loads no
  longer asks for a projection, which had dropped every Include in a connected model.
- A member the model does not map holds nothing EF Core read, and asks for nothing.
- DefaultOrder applies only to a column: Regex.Replace and other framework calls
  made the guarded query throw where the unguarded one ran.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The refusal added for a projection over an abstract T, or one with no parameterless
constructor, also refused the dynamic terminals, which build their own class and return
the root's allowed members. The typed terminals already fail closed there with
SelectTypeMustHaveParameterlessConstructor, before any row is read.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… corrections

The agent reference, the site, README, DOC.md and the release notes say what 6c9e171 and
0aba4bb changed:

- Section 17 reads what a member carries from the source: the model for an entity, so
  only what loads counts, and every loaded subtype otherwise.
- Every navigation counts as loaded on a chain the library cannot read.
- Unmapped members ask for nothing.
- A "*" deny denies what the walk never asks.
- Rows of a derived type come back as T.

The docs review of f7e1cc6 found claims the code did not support, now corrected:

- Named entity navigations were said to be closed.
- The async rationale was wrong: only the Summary count was synchronous.
- DefaultOrder is now a column-only rule.
- Convenience refuses a framework generic it cannot narrow.
- Naming a navigation under Strict needs its fields.
- The trace list is split.
- Constructor-built rows are described consistently.
- A transform can narrow a clean navigation.
- History wording is fixed.
- The dynamic read falls back outside EF Core.
- llms.txt is written by hand. The version gate's phrase follows that wording.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…epoch

Each subtype search asked every loaded assembly for its references and its types. A scan
over a connected model searches once per type it reaches, so the first query repeated that
work for every one. The references are now read once per assembly-load epoch, and an
assembly's types only when a search reaches an assembly that could declare a subtype.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…inary queries

A security review and a blast-radius review of 6c9e171 pulled in opposite directions:
nine more ways a denied value still reached the result, and five classes of ordinary
queries projected, refused or broken for nothing. Both are answered here.

Security fixes:
- A member the model does not map counts as loaded again. A getter over a mapped field
  or a private auto-included navigation hands out what EF Core read.
- A [DwDenied] on an override, or on an interface's implementation, applies to the member
  through the base type or the interface. The attribute walker reads it, so Where, Order,
  Group and Select all see it, and its cache follows the subtype index.
- The subtype index covers open generic subtypes and applications' subclasses of
  framework classes (Exception, Stream). A type parameter left open holds anything.
- A rule on a path through a subtype's member, or through one of two members differing
  only in case, is enforced beneath a member instead of dropped as stale.
- A projection that builds a subtype of T is projected to T when the subtype declares
  a denial.
- An async lazy-loader delegate and an injected DbContext count as loading.
- An application's collection that implements only IEnumerable holds anything.

Precision:
- A member that can hold an object of any type no longer asks for a projection. An
  entity's values come from the database and never count as holding one.
- A reshaped chain counts every navigation as loaded only when it has an include or
  builds an object; otherwise the model decides.
- A projected member is read as the type its initializer constructs.
- Under a "*" deny, each path the walk skips is asked of the policy, not refused.
- An initializer after a constructor with arguments narrows its own bindings again.
- A member a projection leaves out that the unguarded call would have returned is
  recorded in the trace.

The subtype index is built once per load of an assembly that could declare a subtype,
never for framework or resource assemblies.

The two reviews' probes are kept as ReviewLeakTests, ReviewOverBlockTests and
ReviewDefaultOrderTests. A few fail-closed outcomes are asserted as such: in-memory rows
where any loaded subtype declares a denial, and a "*" deny whose subtype paths are not
named.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The agent reference, the site, README, DOC.md and the release notes say what 203f5c9
changed:
- A deny-family attribute on an override, on an implementation, or on the interface member a
  class implements applies to the path.
- Subtypes include open generics and applications' subclasses of framework classes.
- A projection constructing a subtype of T is read as it.
- A rule through a subtype's member is enforced.
- Unmapped members count as loaded and are read as their type.
- An injected DbContext and the async loader delegate count as loaders.
- A reshaped chain counts every navigation only with an include or a built object.
- A member that can hold anything asks for no projection, and an entity keeps it.
- Under a "*" deny, a skipped path is asked of the policy.
- A member a projection leaves out that the unguarded call returned is in the trace.

New limits: rows in memory are projected whenever a loaded subtype declares a denial, and a
default order does not reach a projection over an intermediate row.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…hat a lambda hands its rows

A [DwDenied] on another declaration of a member was missed in several places:
an open generic class implementing a generic interface, an explicit
implementation reached through a framework generic, an interface a subtype
adds over a member it inherits, an interface declaration read by the gate's
scan or its fallback for a subtype's own member, an override of the setter
alone, and a member a subtype hides with new. The walker now reads every
declaration a row can run, once per member and load epoch, and the gate and
the startup scan read the same.

A reshaped chain counted as building its rows only when a lambda constructed
an object. An application's method, a captured query with its own include or
projection, and an object captured from memory hand a row what no include
accounts for, and count now. A value that only feeds a predicate or a key no
longer counts.

Also: a converted column that can hold any object is left out when a
projection is built for another field; an application's own non-generic
collection has its own members read; an open generic subtype over another
closed instantiation is no longer one of a type's subtypes; the trace records
a member left out only when a projection is built; the subtype index no
longer holds on to assemblies the domain already lists.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
… limits left open

The override rule now names a member hidden with new, a setter-only override
and the implementations an open generic class or a subtype gives. A reshaped
chain's lambda counts when it hands its rows an object from an application's
method or a captured query or object. A converted column that can hold any
object is left out when a projection is built. The limits add a subclass EF
Core does not map, opaque converted and unmapped object members with nothing
else denied, and unloadable load contexts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…on classes; read specifications as EF Core does

Security:
- A variant generic interface: a member typed IFeed<Card> holds an
  IFeed<VisaCard> implementation, whose denial the subtype index and the
  walker now read. The open generic case was a regression in ca3f154.
- A provider wrapping EF Core's (LinqKit's AsExpandable, DelegateDecompiler's
  Decompile) got no AsNoTracking, since EF Core's extension hands such a query
  back unchanged: the context filled in navigations it already held, and a
  masked value became a pending change. The call goes into the query itself.
- A converted member inside an EF Core 8 complex property is read as
  converted, so a projection leaves out one that can hold any object.
- Two members sharing a name, one hidden with new under another type or
  spelled in another case: the core reads one, and a row carries both. Either
  one's denial now leaves the name out.
- An application's own collection class, generic or not, has its own members
  read.

Precision:
- A call that reads nothing of the lambda's and returns a query or an
  expression (a specification, a repository's query, FromSql, Set through a
  context interface) is evaluated as EF Core evaluates it; a context's query
  function is a query root; an anonymous object carrying range variables or a
  composite key builds nothing. Ordinary reshaped queries read as in 3.1.0
  again, and no longer throw for abstract, constructor-bound or DDD roots.
- BitArray and the framework's string collections hold values.
- A private member a subtype hides with new no longer denies the base path.

The review tests' probe logs no longer write to a local path.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…the review's corrections

What a reshaped chain counts as building now names specifications,
repository queries, FromSql, query functions and anonymous carriers. The
converted-column rule covers complex properties and names its scope; the
security notes no longer say 3.1.0 kept such a column. The override rule
names public hiding members and variant instantiations, the validator reads
them too, and a dry run's trace is described as it behaves. A guarded query
through a provider wrapping EF Core's runs untracked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ction of values a value

A join on a filtered set held its inner query inline, over a root EF Core put
in the tree, and the evaluator added in 689d70e compiled it, failed on the
root, and counted the chain as building: included data was dropped, and
DDD, constructor-bound and abstract rows threw. A query the tree already
holds is read where it stands again.

An application's collection of values stays a value unless a member of its
own is denied, at any collection layer, so rows in memory and a "*" deny keep
it as before; one with a denied member of its own is read as an object.
Members sharing a name on an entity are read from what the query loads. Rows
in memory are projected when a member a base type declares, and the row type
hides with new, is denied.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…last review left open

A repository or specification method runs once more per guarded read, a
wrapper that hides the EF Core root runs tracking, a framework-typed member
holding an application's collection class is read as the framework type, and
EF Core 6 cannot translate some projections a reshaped query needs.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Copilot AI lite review requested due to automatic review settings September 19, 2026 08:30
@cloudflare-workers-and-pages

Copy link
Copy Markdown

Deploying doc-dynamicwhere with  Cloudflare Pages  Cloudflare Pages

Latest commit: b61a499
Status: ✅  Deploy successful!
Preview URL: https://1f4165c3.doc-dynamicwhere.pages.dev
Branch Preview URL: https://fix-projected-rows-3-2-0.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

🔵 Needs a closer look

It introduces wide-ranging security/behavior changes across the policy gate, projection/default-order logic, and async execution paths, so it warrants final human validation despite strong test coverage.

Review effort: Lite
Findings: 1 Low severity

Open (1)
What changed in this PR

This PR bumps DynamicWhere.ex to v3.2.0 and implements a set of security/behavior fixes driven by DCMP findings (DW-12/13/15), alongside documentation updates and a large new regression test suite intended to prevent future “fail-open” policy leaks.

Changes:

  • Fixes multiple policy-enforcement gaps (notably “denied beneath a member” cases and subtype/alternate-declaration visibility) and expands projection/default-order handling.
  • Adds async terminal overloads that accept CancellationToken, including dynamic/grouped paths using EF Core async operators via reflection.
  • Updates docs/website + release notes and adds extensive review/regression tests covering the newly closed leak vectors.
File Description
README.md Updates 3.2.0 install/version messaging and adds 3.2.0 highlights section.
OfficialWebsite/​package.json Docs site version bump to 3.2.0.
OfficialWebsite/​lib/​nav.ts Updates site version constant to 3.2.0.
OfficialWebsite/​app/​page.tsx Updates homepage marketing copy to “28 extension methods”.
OfficialWebsite/​app/​docs/​policies/​security/​page.tsx Expands security doc content to cover 3.2.0 “gate could not see” denials.
OfficialWebsite/​app/​docs/​policies/​providers/​page.tsx Updates provider install snippets to 3.2.0.
OfficialWebsite/​app/​docs/​policies/​page.tsx Documents guarded handle terminals + new CancellationToken overloads.
OfficialWebsite/​app/​docs/​policies/​attributes/​page.tsx Updates DefaultOrder/projection behavior docs and adds clarifying callouts.
OfficialWebsite/​app/​docs/​policies/​admin/​page.tsx Documents simulation vs real-source differences for “no Selects” cases.
OfficialWebsite/​app/​docs/​page.tsx Updates docs landing version/install snippet to 3.2.0.
OfficialWebsite/​app/​docs/​installation/​page.tsx Updates install commands/snippets to 3.2.0.
OfficialWebsite/​app/​docs/​extensions/​to-list-async-summary/​page.tsx Documents new token overloads + EF Core async count/read behavior.
OfficialWebsite/​app/​docs/​extensions/​to-list-async-segment/​page.tsx Documents new token overload for segment terminal.
OfficialWebsite/​app/​docs/​extensions/​to-list-async-filter/​page.tsx Documents new token overloads for typed filter async terminal.
OfficialWebsite/​app/​docs/​extensions/​to-list-async-dynamic-filter/​page.tsx Documents EF Core-based async read for dynamic filter + token overloads.
OfficialWebsite/​app/​docs/​extensions/​page.tsx Updates extension-method catalog to include token overloads and new behaviors.
OfficialWebsite/​app/​docs/​errors/​page.tsx Updates error docs for projection-related failures now triggered by more deny cases.
OfficialWebsite/​app/​docs/​ai/​page.tsx Updates AI/reference page version wording and method count.
DynamicWhere.Tests/​ReviewComplexPropertyTests.cs Adds EF Core 8-only tests for complex/JSON + converted-member denial handling.
DynamicWhere.Tests/​Policies/​ReviewTrackingTests.cs Adds tests covering reshaped chains + tracking leakage via wrapping providers.
DynamicWhere.Tests/​Policies/​ReviewTrackingKit.cs Adds shared test helpers for tracking/variance/converter/shared-name scenarios.
DynamicWhere.Tests/​Policies/​ReviewSubtypeIndexTests.cs Adds tests for subtype indexing, including open/closed generic relationships.
DynamicWhere.Tests/​Policies/​ReviewSharedNameTests.cs Adds tests for denials on overrides/new/explicit interface members and name clashes.
DynamicWhere.Tests/​Policies/​ReviewReshapeTests.cs Adds tests ensuring denials are enforced across reshaped query chains.
DynamicWhere.Tests/​Policies/​ReviewOwnedPrecisionTests.cs Adds tests for owned members with converted JSON “bags” and narrowing behavior.
DynamicWhere.Tests/​Policies/​ReviewOpaqueCollectionTests.cs Adds tests for enumerable application types inside framework generics and subtype rows.
DynamicWhere.Tests/​Policies/​ReviewLeakTests7.cs Adds tests for denials on “other declarations” (interfaces/overrides) affecting base paths.
DynamicWhere.Tests/​Policies/​ReviewLeakTests6.cs Adds tests ensuring owned members kept whole don’t leak via unmapped getters.
DynamicWhere.Tests/​Policies/​ReviewLeakTests5.cs Adds tests for entities that lazy-load via injected DbContext patterns.
DynamicWhere.Tests/​Policies/​ReviewLeakTests4.cs Adds tests for interface-declared members denied by implementation on in-memory/entity reads.
DynamicWhere.Tests/​Policies/​ReviewLeakTests3.cs Adds tests for re-rooted entity queries and generic derived entities in policy enforcement.
DynamicWhere.Tests/​Policies/​ReviewJoinRootTests.cs Adds tests for joins over roots a typed projection cannot construct (model-read behavior).
DynamicWhere.Tests/​Policies/​ReviewGateTests.cs Adds tests for projection gate behavior across collections/shared names/framework value collections.
DynamicWhere.Tests/​Policies/​ReviewDefaultOrderTests.cs Adds tests for DefaultOrder reaching projected rows via initializer/column-only rules.
DynamicWhere.Tests/​Policies/​ReviewDeclarationPrecisionTests.cs Adds tests ensuring unrelated declarations don’t incorrectly deny reachable paths.
DynamicWhere.Tests/​Policies/​ReviewConverterTests.cs Adds tests for converted object columns beside denials across converter configuration styles.
DynamicWhere.Tests/​Policies/​ReviewConvertedColumnTests.cs Adds tests for converted columns holding policed types (interface vs direct denial).
DynamicWhere.Tests/​Policies/​ReviewCollectionClassTests.cs Adds tests for application collection subclasses with their own denied members.
DynamicWhere.Tests/​DynamicWhere.Tests.csproj Adds EF Core proxies package; updates floor-leg test allowlist (CancellationTests).
DynamicWhere.Tests/​DefaultOrderTests.cs Adds DefaultOrder tests for projected sources and nested initializer conditions.
DynamicWhere.Tests/​ComplexMemberTests.cs Adds tests for EF Core complex properties/JSON owned members vs synthesized projection behavior.
DynamicWhere.ex/​Source/​SegmentComposer.cs Refactors primary-key root/model lookup to centralized QueryRoot helper.
DynamicWhere.ex/​Source/​AsyncReads.cs Adds EF Core reflection-based async ToList/Count for dynamic/grouped reads + token support.
DynamicWhere.ex/​Policies/​Validation/​PolicyModelValidator.cs Extends “DeniedOnlyOverridably” to include denials on other declarations (override/interface).
DynamicWhere.ex/​Policies/​Source/​KnownSubtypes.cs Adds subtype index to discover loaded subtypes (incl. generic/variance-aware handling).
DynamicWhere.ex/​Policies/​Source/​DefaultOrder.cs Updates DefaultOrder rules to apply to certain projected sources (initializer + column-only).
DynamicWhere.ex/​Policies/​Resolution/​PolicyResolver.cs Adds internal fragments sweep helper for projection gate “ask what rules name” behavior.
DynamicWhere.ex/​Policies/​DTOs/​PolicyTrace.cs Adds decision count + withdrawal to discard trace entries from abandoned actions.
DynamicWhere.ex/​DynamicWhere.ex.csproj Version bump to 3.2.0 + updated release notes describing security/behavior changes.
DynamicWhere.ex.Policies.Redis/​DynamicWhere.ex.Policies.Redis.csproj Version bump to 3.2.0 + release notes updated for lockstep release.
DynamicWhere.ex.Policies.EntityFrameworkCore/​DynamicWhere.ex.Policies.EntityFrameworkCore.csproj Version bump to 3.2.0 + release notes updated for lockstep release.
DynamicWhere.ex.Policies.AspNetCore/​DynamicWhere.ex.Policies.AspNetCore.csproj Version bump to 3.2.0 + release notes updated for lockstep release.
build/​check-version.ps1 Updates version-check prose pattern for the AI doc page wording change.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +12 to +13
// EF Core 8 only (complex types, JSON columns): kept at the test project's root so the EF Core 6 floor leg,
// which compiles Policies/** only, leaves it out.
@Sajadh92
Sajadh92 merged commit a7b06e1 into master Sep 20, 2026
6 checks passed
@Sajadh92
Sajadh92 deleted the fix/projected-rows-3.2.0 branch September 20, 2026 00:31
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