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.
| Level | Source | Notes |
|---|---|---|
| 1 | SealedAttribute | The default for every attribute. Absolute: no runtime rule can lift it. |
| 2 | DynamicUser | A rule aimed at one user. |
| 3 | DynamicRole | A rule aimed at a role. |
| 4 | DynamicTenant | A rule aimed at a tenant. |
| 5 | DynamicGlobal | A rule aimed at everyone. |
| 6 | OverridableAttribute | An attribute written with Overridable = true. |
Breaking a tie
Four passes, in order:
- Level — the table above.
- Specificity — an exact field path beats a
*wildcard within the same level. - Priority — the rule
Prioritycolumn, higher first. - Effect — the strictest survives:
DenybeatsMaskbeatsAllow.
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.
| Carrier | How it combines | Why not elect |
|---|---|---|
| Forced predicates | Collected and ANDed | A conjunction can only narrow. Electing one would let a low-authority rule silently discard a sealed tenant scope. |
| Operator restrictions | Intersected | Same reason: the result of combining two restrictions must be at least as tight as either. |
| Transform stages | Elected per stage | A 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; }