Lab — Policy Context and Explicit Decision Outcomes
Learning objective: Practice replacing scattered authorization facts and boolean results with an explicit policy-context snapshot, structured decision outcomes, stable reason codes, and intentional rule precedence.
Difficulty: Beginner
Prerequisites: Complete the Policy Context and Explicit Decision Outcomes tutorial and run the Policy Context and Explicit Decision Outcomes sample.
This lab builds directly on the second foundational tutorial and its executable companion sample.
The tutorial explains why decision inputs and outcomes should be visible.
The sample demonstrates that model with deterministic scenarios.
This lab asks you to compress, extend, challenge, and make the policy contract more explicit.
A governance decision should preserve enough information for the host to understand what happened and what should happen next.
Starting Architecture
The companion sample uses this flow:
Host gathers facts
↓
Policy context snapshot
|
+-- Intent
+-- Actor
+-- Resource
+-- Environment
+-- Correlation
+-- Policy identity
↓
Policy evaluation
↓
Structured decision outcome
↓
Host-controlled next step
The important invariant is:
Same explicit context
+
Same policy behavior
↓
Same structured outcome and reason code
The sample intentionally performs no real account-disable operation. The exercise focuses on the information that reaches policy evaluation and the meaning of the decision returned from it.
Prepare the Lab
Work on a temporary branch or disposable copy of the repository so that you can safely modify the sample.
For example:
git switch -c lab/policy-context-and-outcomes
From the repository root, run the companion sample before making changes:
dotnet run --project samples/policy-context-and-explicit-decision-outcomes/PolicyContextAndExplicitDecisionOutcomes/PolicyContextAndExplicitDecisionOutcomes.csproj
The baseline should finish with:
Invariant preserved: every explicit context produced the expected structured outcome.
Scenarios verified: 7
Before continuing, locate these elements in Program.cs:
DisableAccountIntentActorContextAccountContextEnvironmentContextDisableAccountPolicyContextGovernanceDecisionOutcomeDecisionReasonGovernanceDecisionDisableAccountPolicyPolicyScenario
You should be able to explain which types contain facts, which type contains rules, and which types describe the decision result.
Part 1 — Collapse the Decision Back to a Boolean
The sample currently preserves several distinct outcomes:
Allowed
Warning
Denied
Deferred
AcknowledgmentRequired
EscalationRecommended
Temporarily add a boolean-only view of the result after policy evaluation:
GovernanceDecision decision = policy.Evaluate(scenario.Context);
bool allowed = decision.CanProceed;
Print only the scenario name and the boolean for one run.
Observe the Information Loss
Answer these questions before restoring the original output:
- Can the boolean distinguish
AllowedfromWarning? - Can it distinguish
DeniedfromDeferred? - Can it distinguish
AcknowledgmentRequiredfromEscalationRecommended? - Can the host determine why the result is
falsewithout reevaluating policy or inspecting other state? - Could two very different governance situations now look identical to downstream code?
Restore the structured output after the experiment.
A derived property such as CanProceed can be useful at a specific boundary. The problem appears when the boolean becomes the entire decision model and discards information required to route, explain, audit, or revisit the decision.
Part 2 — Add an Explicit Context Fact
Extend the account context with a simple data-classification fact:
public enum DataClassification
{
Standard,
Sensitive,
Restricted
}
Add the value to AccountContext:
public sealed record AccountContext(
string AccountId,
string TenantId,
bool IsProtected,
bool IsAlreadyDisabled,
DataClassification Classification);
Update CreateContext so each scenario receives an explicit classification. Use Standard for the existing scenarios so the baseline behavior remains unchanged.
Create a new deterministic scenario for a restricted account:
Requester is administrator
Actor and account are in the same tenant
Account is not protected
Account is not already disabled
Maintenance hold is false
Reason is supplied
Classification is Restricted
Do not add any policy rule yet.
Run the sample.
The new fact should be visible in the context, but the policy should still treat the scenario according to the existing rules.
This demonstrates:
Context fact exists
≠
Policy interpretation exists
A context should describe the evaluated situation. It should not silently decide what the fact means.
Part 3 — Interpret the New Fact with a Structured Outcome
Now introduce a policy rule for Restricted accounts.
For this lab, a reasonable design is:
Outcome: EscalationRecommended
ReasonCode: account.disable.restricted-classification
Add a policy rule similar to:
if (context.Account.Classification is DataClassification.Restricted)
{
return GovernanceDecision.Escalate(
"account.disable.restricted-classification",
"Restricted accounts require higher-authority review.");
}
Update the new scenario so it expects:
EscalationRecommended
account.disable.restricted-classification
Run the sample again and confirm that the scenario is verified.
Explain the Outcome
Write a short explanation answering:
Why is
EscalationRecommendedmore informative thanfalsefor this case?
A denial means stop.
An escalation recommendation means the current path should stop and another decision path should begin.
Those states may both have CanProceed == false, but they are not operationally equivalent.
Part 4 — Make Rule Precedence Observable
Create an overlapping scenario where more than one rule could apply:
Requester is administrator
Actor and account are in the same tenant
Account is protected
Account classification is Restricted
Maintenance hold is active
Reason is supplied
At least three outcomes are plausible from the individual rules:
Protected account
↓
EscalationRecommended
Restricted classification
↓
EscalationRecommended
Maintenance hold
↓
Deferred
Run the sample and observe which rule currently wins.
Do not assume that the current result is automatically correct merely because it appears first in the method.
Choose an intentional precedence rule and make it executable by adding the overlapping scenario to PolicyScenario with the expected outcome and reason code.
Reasonable choices include:
- Escalation outranks temporary deferral.
- A temporary operational hold short-circuits all account changes.
The expected scenario becomes a small contract documenting intended precedence.
Part 5 — Preserve Stable Reason Codes
Change the human-readable message for one rule without changing its reason code.
For example, revise:
Only administrators may disable accounts.
to:
Account disable operations require an administrator actor.
Keep:
account.disable.not-administrator
Run the sample again.
The existing scenario should continue to pass.
Now consider downstream code that depends on prose:
if (decision.Reasons[0].Message.Contains("administrators"))
{
...
}
Answer:
- Why is this fragile?
- What happens when wording is localized?
- What happens when a message is improved for clarity?
- Which part of the decision should software depend on instead?
The reason message is for people.
The reason code is the stable machine-readable contract.
Part 6 — Validate the Final Architecture
Run the modified sample again.
Confirm all of the following:
- Existing baseline scenarios still produce their intended outcomes.
- The new classification value is part of the explicit policy-context snapshot.
- Adding the context fact alone did not silently create policy behavior.
- The new
Restrictedrule returns a structured outcome rather than a boolean. - The new rule has a stable reason code.
- The overlapping scenario makes precedence observable and testable.
- Changing human-readable reason text does not break scenario verification.
DisableAccountPolicyContextstill contains facts rather than service dependencies.DisableAccountPolicyremains the component that interprets those facts.GovernanceDecisiondescribes the result but performs no side effect.
Your exact scenario count may now be greater than the original seven.
Do not preserve the original count artificially.
The useful invariant is that every declared scenario produces the expected structured result.
Part 7 — Reason About a Scattered Alternative
Consider this alternative implementation:
bool allowed =
user.IsAdministrator &&
user.TenantId == account.TenantId &&
!account.IsProtected &&
!maintenanceHold &&
account.Classification != DataClassification.Restricted;
Answer these questions:
- Where would
Deferredbe represented? - Where would
AcknowledgmentRequiredbe represented? - How would the host distinguish a protected resource from a restricted-classification resource?
- Where would stable reason codes live?
- How would you reproduce exactly which facts were evaluated during an incident?
- If the authorization expression grows across controllers and services, how easy is it to identify the actual policy contract?
- Is a compact boolean expression always wrong, or does its suitability depend on whether the domain truly has only two meaningful states?
Some operations genuinely need only a small yes/no check.
The lesson is not to replace every boolean with a framework.
The lesson is to avoid compressing a richer governance lifecycle into a boolean when the system needs to preserve multiple states, reasons, routing choices, or evidence.
Completion Criteria
You have completed the lab when you can demonstrate this progression:
Scattered or compressed decision information
↓
Explicit context snapshot
↓
Facts remain separate from rules
↓
Policy interprets facts
↓
Structured outcome
↓
Stable reason code
↓
Intentional precedence
↓
Host can determine the next governed step
You should also be able to explain why these two statements are different:
The operation may not proceed.
and:
The operation may not proceed because it must be deferred,
acknowledged, denied, or escalated.
The second preserves information that the host can use.
Optional Extension — Add Policy Identity to Verification
The sample already carries:
CorrelationId
PolicyVersion
Extend PolicyScenario so it also declares the expected policy version, then verify that the context contains it.
Change one scenario to use a different policy-version string while leaving the policy rules unchanged.
Discuss:
- Why can policy identity matter even when it does not change the immediate outcome?
- Why is a version or hash useful for later audit interpretation?
- What additional evidence would a production system need before claiming that a decision is fully reproducible?
This prepares for the next tutorial, where acknowledgment and audit residue become first-class concerns.
Resetting the Sample
If you created a temporary branch only for the exercise, you can compare your work with the original sample and then discard or keep the branch as desired.
To discard uncommitted changes to the sample:
git restore samples/policy-context-and-explicit-decision-outcomes/PolicyContextAndExplicitDecisionOutcomes/Program.cs
Use git status before restoring anything so that you understand which local changes will be affected.
Related Content
- Policy Context and Explicit Decision Outcomes tutorial — review the architectural reasoning behind the lab.
- Policy Context and Explicit Decision Outcomes sample — return to the executable baseline used by this exercise.
- Decision Before Execution lab — practice the earlier boundary between decision and host-owned execution.
- Acknowledgment and Audit Residue — continue from structured outcomes into acknowledgment, lineage, and governance evidence.
- Foundational Tutorial Index — view the complete foundational learning path.
GovernanceDecisionOutcome— compare the teaching vocabulary with the working framework.GovernanceDecision— inspect the fuller decision model and reason metadata.IAsiBackboneConstraintEvaluationContext— compare the explicit teaching snapshot with the framework context surface.DefaultAsiBackbonePolicyEvaluator— inspect fuller constraint evaluation and decision composition.
Read it. Run it. Question it. Improve it.