DynamicWhere.ex
DynamicWhere.exv3.0.0·docs

Precedence

A field can be spoken about by an attribute, a rule aimed at the user, a rule aimed at one of their roles, a tenant rule and a global rule, all at once. Precedence decides which one wins.

Six levels

Lowest number wins.

LevelSourceNotes
1SealedAttributeThe default for every attribute. Absolute: no runtime rule can lift it.
2DynamicUserA rule aimed at one user.
3DynamicRoleA rule aimed at a role.
4DynamicTenantA rule aimed at a tenant.
5DynamicGlobalA rule aimed at everyone.
6OverridableAttributeAn attribute written with Overridable = true.
Sealed by default is the whole design
An attribute is a statement in the source code. If a runtime rule could lift it, the source would stop being the ceiling and an operator with write access to the rule store would have write access to your security model. Opting out is one keyword; opting in should never be silent.

Breaking a tie

Four passes, in order:

  1. Level — the table above.
  2. Specificity — an exact field path beats a * wildcard within the same level.
  3. Priority — the rule Priority column, higher first.
  4. Effect — the strictest survives: Deny beats Mask beats Allow.

When two sources tie on all four, the decided effect is still fully deterministic. Only which of several equally-winning sources gets named by explain is arbitrary.

What is not elected

Three things combine instead of competing, and each for a reason.

CarrierHow it combinesWhy not elect
Forced predicatesCollected and ANDedA conjunction can only narrow. Electing one would let a low-authority rule silently discard a sealed tenant scope.
Operator restrictionsIntersectedSame reason: the result of combining two restrictions must be at least as tight as either.
Transform stagesElected per stageA rule can add a truncation on top of a sealed mask; electing the whole chain would let it discard the mask by supplying anything at all.

A fragment that carries something decides nothing

An alias, a required-filter demand, a forced predicate and a cost weight all attach to a field without saying whether it may be read. They are recorded against PolicyFeature.None so they cannot win an election they never entered — otherwise decorating a field with [DwAlias] would make it undeniable, because a sealed allowance outranks every runtime denial.

Masked and denied at once

A field that is both denied and masked is dropped, not masked. Deny is the stronger effect and wins the election; the mask never runs.

// Salary stays filterable and sortable: Allows() refuses only a denial,
// so a Mask effect on Select does not remove the field from a WHERE clause.
[DwMask(MaskStrategy.Full)]
public decimal Salary { get; set; }