DynamicWhere.ex 3.2.0
DynamicWhere.ex 3.2 answers the second change request DCMP raised on 3.1, and closes the ways a [DwDenied] value could still reach a result. Most of those were already in 3.1.0. If you guard queries with ApplyPolicy, upgrade.
dotnet add package DynamicWhere.ex --version 3.2.0Packages
| Package | Version |
|---|---|
| DynamicWhere.ex | 3.2.0 |
| DynamicWhere.ex.Policies.EntityFrameworkCore | 3.2.0 |
| DynamicWhere.ex.Policies.Redis | 3.2.0 |
| DynamicWhere.ex.Policies.AspNetCore | 3.2.0 |
Why to upgrade
- A denial beneath a member, with no
Selects, was never enforced, in both tiers. A denied field came back through a projected list, a row in memory, and an entity's included, auto-included, lazily loaded or owned members. What a query loads is now read from the EF Core model, including includes named from another root, re-rooted bySelect,SelectManyorJoin, projections hidden behind anotherSelect, and an object a reshaping lambda gets from an application method or captures. - A query through a provider that wraps EF Core's — LinqKit's
AsExpandable, DelegateDecompiler'sDecompile— ran with tracking, because EF Core'sAsNoTrackinghands 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 nextSaveChangeswould write.AsNoTrackingnow goes into the query itself. - Denials on subtypes and other declarations were invisible. A denied field on a derived entity, a subclass, an open generic subtype or a subclass of a framework class did not apply through the base type, and neither did a
[DwDenied]on another declaration of the same member: an override of either accessor, a member a subtype hides withnew, or an interface member or its implementation — explicit, inherited, declared by an open generic class, or reached through a variant instantiation. - 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 asked of the policy now. Selectsnaming a member carried denials the gate could not see: a navigation whose key is denied, a member typedIReadOnlyList<T>, a denial past four segments, in a framework generic, or in an owned chain or converted column, andMain.Leadnot gatingMain.Id.- A field denied at the top of
Tholding no simple value — a blob, list, owned or JSON member — synthesized nothing. - An application's own collection class hid its own denied members, and two members sharing a name (one hidden with
newunder 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.
Read before upgrading
Five behaviours changed. The full list is breaking changes, points 25 to 29. The ones most likely to reach you:
- Entity navigations are left out of a synthesized projection. 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 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 withSelectTypeMustHaveParameterlessConstructor; the dynamic terminal works. - A
[DwDenied]on an override, a member hidden withnew, or an implementation denies the base path for every row, in every clause. Anewmember 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.defaultis ambiguous betweenboolandCancellationToken. Name the argument or passCancellationToken.None.
What's new
- DW-12: a synthesized projection 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 — on a row a projection builds, a row in memory, and an entity's columns, owned and complex members.
- DW-13:
[DwEntity(DefaultOrder)]applies to a projected source when its outermostSelectbuilds the type in an initializer that assigns every default field a column. 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
CancellationTokenoverloads. 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, so the token reaches the database.
Precision: what does not change
The guard projects only where a denied value can reach the result. 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— a geometry, JSON bag orBitArraycolumn — asks for no projection. - Specifications, repository queries,
FromSql,Setthrough a context interface and query functions are read as EF Core reads them; query-syntax joins, lets, left joins and composite keys build nothing; a value that only feeds a predicate or a key, such asEF.Functions.Likeornew DateTime(...), does not count. BitArrayand 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.
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 anobjectcolumn, or an unmappedobjectgetter over a private navigation, comes back as loaded. - A denial on an override or a
newmember 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
AssemblyLoadContextstay 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 with 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. It fails closed.
Verified
- 2,522 tests on EF Core 8 and 1,818 on the declared EF Core 6.0.22 floor.
- Every fix was mutation-checked: removed on purpose, and a test turned red. Every review round's probes ship as the
Review*Testsfiles. - Seven rounds of independent review — security, over-blocking, correctness, and the docs against the code — plus a line-by-line pass.
- A DCMP-shaped probe on EF Core 9, Npgsql 9 and PostgreSQL 17 gives the expected answers, including a canceled token stopping the typed, dynamic and summary reads.
- A 264-call behavioural sweep of the demo API against 3.1.0: no answer differs.
- EF Core binary compatibility checked against EF Core 6, 7, 8, 9 and 10.
Compatibility
Targets net6.0, runs on .NET 6 through 10, with EF Core 6.0.22 or later. A project with no policy attributes and no DwPolicy.Configure call is affected only by the behaviour changes above.