Field-Level Policies
DynamicWhere.ex accepts a Filter from any caller and turns it into an EF Core query. The caller chooses which fields to filter on, sort by, select, group by and aggregate. Policies add the question the library could not previously answer: who is asking, and what are they allowed to see?
DwPolicy.Configure call behaves exactly as 2.1.5. There is no break in the 2.x API — see Breaking Changes.A sandwich, not a rewrite
Requests are sanitized before the query is built. Results are transformed after they materialize. The query engine in between is unchanged, which is why turning policies on cannot alter the SQL of an unguarded query.
caller request
|
v
query.ApplyPolicy(ctx) the only entry point; returns a guarded handle
|
v
sanitize drop or refuse denied fields, restrict operators,
enforce caps, inject aliases and forced predicates
|
v
existing DynamicWhere untouched
|
v
transform mask, mutate, default, generalize, truncate, format
|
v
FilterResult.Policy a PolicyTrace saying what the policy didThe whole thing in one screen
// Once, at startup. A second call is refused: the tier is read by every
// request thread, and a posture that can change mid-flight can be relaxed.
DwPolicy.Configure(new DwPolicyOptions
{
Tier = DwTier.Convenience,
HashSalt = secret,
});
// Once per request, never once per query.
var caller = await DwPolicy.PrepareAsync(
new DwPolicyContext()
.WithSubject(DwSubjectKind.User, userId)
.WithSubject(DwSubjectKind.Role, "Support")
.WithSubject(DwSubjectKind.Tenant, tenantId)
.WithValue("TenantId", tenantId));
// Then query through the guarded handle instead of the raw IQueryable.
var result = await db.Employees.ApplyPolicy(caller).ToListAsync(filter);[DwEntity(RequirePolicy = true)] // an unguarded read throws
public class Employee
{
[DwMask(MaskStrategy.Email), DwNoOrder]
public string Email { get; set; } // s*************@c******.com on the way out
[DwForceWhere(Operator.Equal, Value = "true")]
public bool IsActive { get; set; } // ANDed into every guarded query
[DwGeneralize(GeneralizeMode.Round, Step = 5000,
AllowAggregate = true, MinGroupSize = 5)]
[DwNoOrder, DwAudit, DwCost(10)]
public decimal Salary { get; set; } // rounded; aggregatable over groups of 5+
[DwDenied]
public JsonDocument? WorkSchedule { get; set; }
}Six features, per field
Every decision is made for one field and one feature: Where, Select, Order, Group, Aggregate, Segment.
Segment is its own flag rather than a combination of Where and Select, because set operations can reconstruct a hidden field from membership alone — see Security.
Where to go next
- Attributes — all eighteen, with what each one does
- Precedence — six levels, and why attributes are sealed by default
- Transforms & masking — nine mask strategies and five other transforms
- Dynamic store — rules without a redeploy
- Store providers — Redis and Entity Framework Core
- Admin API — schema, rules, explain, simulate, health
- Security & k-anonymity — the seven inference channels
- Configuration — options, caps and defaults
The four packages
| Package | What it adds |
|---|---|
DynamicWhere.ex | Attributes, resolver, sanitizer, transforms, in-memory store, discovery, audit |
DynamicWhere.ex.Policies.Redis | Runtime rules in Redis |
DynamicWhere.ex.Policies.EntityFrameworkCore | Runtime rules in any EF Core provider |
DynamicWhere.ex.Policies.AspNetCore | Admin API, claims adapter, audit middleware |
All four ship at the same version, and the build refuses to let them drift apart.