Store Providers
Two packages hold rules outside the process. Both implement the same contracts and both pass the same conformance suite as the in-memory store, so swapping one for the other changes nothing about behaviour.
Redis
dotnet add package DynamicWhere.ex.Policies.Redis --version 3.2.0var redis = await ConnectionMultiplexer.ConnectAsync(connectionString);
var store = new RedisPolicyStore(redis);
var provider = await StorePolicyProvider.CreateAsync(store, options);
DwPolicy.Configure(options, provider);Entity Framework Core
dotnet add package DynamicWhere.ex.Policies.EntityFrameworkCore --version 3.2.0var policyDbOptions = new DbContextOptionsBuilder<DwPolicyDbContext>()
.UseNpgsql(connection, sql => sql.MigrationsAssembly("YourProject"))
.Options;
// A new context per read: the provider polls on a background timer, and a
// context shared with request threads would be used concurrently.
//
// The second argument turns a rule's entity name into a Type. Without it the
// store cannot run its sealed-field check on a write and accepts the rule.
// A lambda, not DwPolicy.Options.Entities.Resolve: a method group binds the
// options in force now, which before Configure are the empty defaults, so
// every rule would pass the check.
var store = new EfPolicyStore(
() => new DwPolicyDbContext(policyDbOptions),
name => DwPolicy.Options.Entities.Resolve(name));
var provider = await StorePolicyProvider.CreateAsync(store, options);
DwPolicy.Configure(options, provider);DwPolicyDbContext is declared in the NuGet package, and EF Core looks for migrations in the assembly that declares the context. Left alone, dotnet ef migrations add refuses outright with "your target project does not match your migrations assembly", and a running application finds no migrations to apply. Point it at your own project in both places you build the context — the runtime registration and any design-time factory.Three tables, on their own migration history:
| Table | Holds |
|---|---|
DwPolicyRules | One row per rule, indexed by entity and field, and by subject. |
DwPolicyVersion | A single row carrying the snapshot version. |
DwPolicyTokens | The EfTokenVault mapping, one row per tokenized value. |
Two serializers, three stores
PolicyRuleDocument is the only place a whole rule is read or written, the EF Detail column included. PolicyPayload sits under it and handles one thing: the transform object. Neither provider parses JSON of its own, because three parsers would become three standards — and the rule carries a forced predicate, an alias and two operator lists that a serializer can drop in silence, turning a tenant scope into a rule that is still listed and no longer applies. Every enumeration is written and read by name, in JSON and in a database column alike: the zero member of several enumerations is the permissive one, so an unparsed value must not read as a plausible-looking default.
A forced predicate is written as a forced object, in a Redis document and in the EF Detail column alike. allowNull is written only when it is true, so a predicate that does not let null through is written exactly as earlier releases wrote it:
"forced": {
"fieldPath": "InstitutionId",
"operator": "Equal",
"dataType": "Number",
"contextValue": "TenantId",
"allowNull": true
}An absent or null allowNull reads as false. Anything else that is not a JSON true or false — the string "true", the number 1 — is refused. Guessing at a string would go wrong one way or the other: read as true it can widen a tenant scope nobody asked to widen, and read as false it can drop a widening somebody wrote. allowNull: true on a null check is refused too, whether or not the object also carries a value or contextValue: a widened IsNotNull would filter nothing. A null check that carries a contextValue is refused with or without the flag, because the reader builds it through ForcedPredicate.FromContext, which refuses a null check. A value on a null check is still ignored.
A refused document is never skipped: it fails the load that reads it. A broad rule fails the load: fatal at startup, a refresh failure afterwards, where StoreFailure applies. A User rule is read only when a context is prepared, so it fails DwPolicy.PrepareAsync for the callers it names, in every StoreFailure mode. It never fails startup or a refresh, and never degrades the provider.
Read-only deployments
Register only IDwPolicyStore and writes become impossible by construction rather than by convention. IDwPolicyWritableStore is a separate interface, so a replica that should never accept a rule simply does not implement it.
The conformance suite
One abstract suite runs against in-memory, a real Redis and a real PostgreSQL, covering load, version, watch and poll, the atomic swap, the zone split, startup failure, refresh failure, the staleness ceiling, the sealed-field refusal and the read-only store. Any future provider inherits all of it.