DynamicWhere.ex
DynamicWhere.exv3.0.0·docs

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?

New in 3.0, and entirely opt-in
A project with no policy attributes and no 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 did

The 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

The four packages

PackageWhat it adds
DynamicWhere.exAttributes, resolver, sanitizer, transforms, in-memory store, discovery, audit
DynamicWhere.ex.Policies.RedisRuntime rules in Redis
DynamicWhere.ex.Policies.EntityFrameworkCoreRuntime rules in any EF Core provider
DynamicWhere.ex.Policies.AspNetCoreAdmin API, claims adapter, audit middleware

All four ship at the same version, and the build refuses to let them drift apart.