Dynamic Policy Store
Attributes are the compile-time half. A store supplies the other half at runtime, so an operator can grant or revoke access without a redeploy — and can never grant what the source code seals.
A rule
var rule = new PolicyRule(
subjectKind: DwSubjectKind.Role,
subjectKey: "Support",
entityType: typeof(Employee).FullName!,
fieldPath: "Position", // or "*" for every field of the type
features: PolicyFeature.Select | PolicyFeature.Order,
effect: PolicyEffect.Deny,
priority: 10,
validFrom: DateTimeOffset.UtcNow,
validTo: DateTimeOffset.UtcNow.AddDays(30));A rule also carries a transform, an operator restriction, an alias, a forced predicate, a required-operator list and the discovery facts, plus audit columns. ValidFrom and ValidTo make a grant expire on its own — expiry that depends on someone remembering is expiry that does not happen.
PolicyRule constructor itself, so every store and the admin API get the same refusals. A rule naming a sealed field is rejected on write, not ignored on read.Two zones
| Zone | Holds | Lifetime |
|---|---|---|
| Broad | Global, tenant and role rules | Cached and shared across requests |
| Narrow | Per-user rules | Loaded for the identities on one context |
Each zone refuses the other rules at construction rather than filtering them out, so a store that returns user rules from the broad load fails loudly instead of quietly caching one user grants for everyone.
Prepare once per request
var caller = await DwPolicy.PrepareAsync(
new DwPolicyContext()
.WithSubject(DwSubjectKind.User, userId)
.WithSubject(DwSubjectKind.Tenant, tenantId));PolicyContextNotPrepared. Falling back to attributes alone would look exactly like a working policy with the dynamic half missing, which is the worst possible failure for this feature.A context carries the snapshot it was served, and the staleness ceiling measures how old that snapshot is — not how fresh the provider is now. A context pinned longer than MaxSnapshotAge is refused even after the provider has refreshed, which is exactly why it is built once per request.
Three failure modes
options.StoreFailure | When the store is unreachable |
|---|---|
LastKnownGood (default) | Serve the last snapshot that loaded, bounded by MaxSnapshotAge. |
FailClosed | Refuse the query with StoreUnavailable. |
StaticOnly | Fall back to attributes alone. |
All three are bounded by the ceiling. A startup load failure always throws, whatever the mode: an application that has never loaded a policy has no last known good to serve.
Refresh
The provider polls on options.RefreshInterval and, where the store supports it, also watches for change notifications. Refresh happens on a background timer and never on the query path. A query that starts on version 41 finishes on version 41 — the swap is atomic.
foreach (var provider in DwPolicy.StoreProviders)
{
Console.WriteLine(provider.Version); // snapshot version
Console.WriteLine(provider.Age); // how old it is
Console.WriteLine(provider.IsDegraded); // serving last known good
Console.WriteLine(provider.LastError); // why
}Choosing a store
InMemoryPolicyStore ships in the core package and is enough for a single process. For anything else see Store providers. All three pass one shared conformance suite, so they behave alike or the build fails.