C# examples
Ordo in C#
Ordo is language-independent. We will start with things you already understand in real life, then show the same ideas in ordinary C#. The first measured engineering examples come from the C# time-entry trial. The later decision, coverage, Capability, and reconciliation examples are explicit C# translations of the current Ordo 1.3 semantics.
You already understand state. You just may not call it that.
State is simply what is true right now. A transition is a change that is allowed from that state.
The 30-second example
A light switch
A normal light is either Off or On. If it is Off, the useful action is Turn On. If it is On, the useful action is Turn Off.
public enum LightState
{
Off,
On
}
public static LightState TurnOn(LightState state) =>
state == LightState.Off
? LightState.On
: throw new InvalidOperationException(
"The light is already on.");
That is the core idea. Ordo takes the same thing you already understand about the light and applies it to business software, where the legal states and transitions are harder to see.
A more useful example
Ordering a cup of coffee
Suppose you order coffee. You can ask for it black, with cream, with sugar, or with both. Those choices describe what you ordered. They are not necessarily the order's state.
The lifecycle is different: Taking Order → Preparing → Ready → Served. That is state because each step changes what can legally happen next.
Cream and sugar are data. Preparing and Served are state.
This distinction matters. If every combination becomes a state, the model explodes into
things like PreparingBlack, PreparingWithCream,
ReadyWithCreamAndSugar, and so on. Ordo models only the distinctions that
change meaning or legal behavior.
public sealed record CoffeeSpec(
bool Cream,
bool Sugar)
{
public static CoffeeSpec Black =>
new(Cream: false, Sugar: false);
public static CoffeeSpec CreamAndSugar =>
new(Cream: true, Sugar: true);
}
public enum OrderStatus
{
TakingOrder,
Preparing,
Ready,
Served,
Cancelled
}
public sealed record CoffeeOrder(
Guid Id,
CoffeeSpec Coffee,
OrderStatus Status);
public static CoffeeOrder AddCream(CoffeeOrder order)
{
if (order.Status != OrderStatus.TakingOrder)
throw new InvalidOperationException(
"The drink can only be changed while taking the order.");
return order with
{
Coffee = order.Coffee with { Cream = true }
};
}
public static CoffeeOrder Submit(CoffeeOrder order)
{
if (order.Status != OrderStatus.TakingOrder)
throw new InvalidOperationException(
"Only an open order can be submitted.");
return order with { Status = OrderStatus.Preparing };
}
public static CoffeeOrder MarkReady(CoffeeOrder order)
{
if (order.Status != OrderStatus.Preparing)
throw new InvalidOperationException(
"Only a drink being prepared can become ready.");
return order with { Status = OrderStatus.Ready };
}
public static CoffeeOrder Serve(CoffeeOrder order)
{
if (order.Status != OrderStatus.Ready)
throw new InvalidOperationException(
"Only a ready drink can be served.");
return order with { Status = OrderStatus.Served };
}
What became explicit?
You cannot serve a drink that is still being prepared. You cannot change cream after preparation starts. You cannot accidentally treat sugar as a lifecycle state.
That is the Ordo move
Put meaning in one authoritative model, expose the legal next actions, and stop every UI, API, engineer, and agent from having to rediscover the rules independently.
The important question is not "is this a state machine?"
Ask: what is true now, and does that change what is legally allowed next? If the answer changes the legal next move, it probably belongs in the semantic state. If it merely describes the thing, like cream or sugar, it is usually data carried by that state.
Name the state instead of reconstructing it.
The measured C# trial used a closed enum for the lifecycle and immutable records for the entity. That is a pragmatic C# representation when the cases do not need different payload shapes.
public enum TimeEntryStatus
{
Recorded,
Voided,
Superseded
}
public sealed record TimeEntry(
EntryId Id,
PersonId PersonId,
DateOnly Date,
TimeOfDay Start,
DurationMinutes Duration,
ProjectId ProjectId,
TimeEntryStatus Status,
long Revision);
C# enums are useful here because a switch expression over an enum can produce CS8509 when a
case is missing. With TreatWarningsAsErrors, adding a lifecycle state can turn
unhandled switches into build failures.
Do not let two different meanings share the same primitive.
A time of day and a duration can both be represented by integers. They are still not the same thing.
public sealed record TimeOfDay
{
public int Hour { get; }
public int Minute { get; }
private TimeOfDay(int hour, int minute)
{
Hour = hour;
Minute = minute;
}
public static TimeOfDay Of(int hour, int minute)
{
if (hour is < 0 or > 23)
throw new ArgumentOutOfRangeException(nameof(hour));
if (minute is < 0 or > 59)
throw new ArgumentOutOfRangeException(nameof(minute));
return new TimeOfDay(hour, minute);
}
public int MinutesSinceMidnight => Hour * 60 + Minute;
}
public sealed record DurationMinutes
{
public int Value { get; }
private DurationMinutes(int value) => Value = value;
public static DurationMinutes Of(int minutes) =>
minutes <= 0
? throw new ArgumentOutOfRangeException(nameof(minutes))
: new DurationMinutes(minutes);
}
What this prevents
A caller cannot accidentally pass a DurationMinutes where a
TimeOfDay is required and still compile.
What this does not prevent
A mathematically wrong calculation can still produce a perfectly valid
DurationMinutes. Types narrow the failure space; they do not prove business
arithmetic.
Change state through named operations.
The write is not "set Status." The write is "Void this Recorded entry at this revision."
public static CommandResult Void(
WeekDocument week,
EntryId entryId,
long expectedRevision,
string? reason)
{
var error = RequireStatus(
week,
entryId,
expectedRevision,
TimeEntryStatus.Recorded,
out var entry);
if (error is not null)
return new CommandResult.Rejected([error]);
var voided = entry! with
{
Status = TimeEntryStatus.Voided,
Revision = entry.Revision + 1,
Provenance = entry.Provenance with
{
CorrectionReason = reason
}
};
var updatedWeek = week with
{
Entries = ReplaceEntry(week.Entries, voided),
DocumentRevision = week.DocumentRevision + 1
};
return new CommandResult.Success(updatedWeek, voided);
}
- 1Current stateThe named state the entity is actually in — not a combination of flags a reader has to infer.
- 2Requested actionA named action, not an arbitrary field write.
- 3Capability checkIs this action one the current state permits at all? If not, the request is refused here.
- 4Evidence checkIs the basis for the transition present and valid?
- 5Policy checkDo the business rules covering this action hold for these particular values?
- 6Legal transitionOnly now does state change, and only to a state the model declares reachable from the current one.
- 7New state, obligations, effectsThe resulting state, what must still happen for the transition to be complete, and the external effects requested as data rather than performed in place.
The important part is not the amount of code. The operation names the precondition, the legal transition, the revision check, and the result. Callers do not get a generic mutation surface.
Derive what can be attempted from the same semantic rules.
The measured C# trial called this projection EntryCapabilities. Current Ordo
terminology reserves Capability for host-established semantic authority,
so think of this older type as an available-action projection.
[Flags]
public enum EntryActions
{
None = 0,
Correct = 1 << 0,
Split = 1 << 1,
Void = 1 << 2,
Restore = 1 << 3,
LinkEvidence = 1 << 4,
UnlinkEvidence = 1 << 5,
Merge = 1 << 6
}
public static class AvailableActions
{
public static EntryActions For(TimeEntry entry) =>
entry.Status switch
{
TimeEntryStatus.Voided =>
EntryActions.Restore,
TimeEntryStatus.Superseded =>
EntryActions.None,
TimeEntryStatus.Recorded =>
ForRecorded(entry)
};
private static EntryActions ForRecorded(TimeEntry entry)
{
var actions =
EntryActions.Correct |
EntryActions.Void |
EntryActions.LinkEvidence |
EntryActions.Merge;
if (entry.Duration.Value >= 2)
actions |= EntryActions.Split;
if (entry.Evidence.Count > 0)
actions |= EntryActions.UnlinkEvidence;
return actions;
}
}
The UI does not own this rule.
A UI, API, or agent may ask the domain which actions are available. It should not recreate the rules independently and hope they stay synchronized with the transition guards.
Separate what you observed from how completely you looked.
This is a C# translation of current Ordo 1.3 semantics. It was not part of the earlier C# effort trial.
public readonly record struct EvidenceId(Guid Value);
public readonly record struct CoverageScope(string Value);
public sealed record Evidence(
EvidenceId Id,
string Source,
DateTimeOffset ObservedAt,
string Basis);
public enum CoverageStatus
{
Complete,
Partial,
Unknown
}
public sealed record ContextCoverageClaim(
CoverageScope Scope,
CoverageStatus Status,
IReadOnlySet<EvidenceId> Provenance);
public sealed record DecisionContract(
IReadOnlySet<CoverageScope> RequiredCompleteScopes);
public static class CoverageGate
{
public static IReadOnlyList<CoverageScope> MissingRequiredCoverage(
DecisionContract contract,
IReadOnlyList<ContextCoverageClaim> claims) =>
contract.RequiredCompleteScopes
.Where(required =>
!claims.Any(c =>
c.Scope == required &&
c.Status == CoverageStatus.Complete))
.ToList();
}
- 1Observation produces EvidenceEvidence carries identity, source, observation time, basis, and typed content. Derived evidence must have a closed, acyclic dependency set.
- 2Evidence supports a Coverage claimA ContextCoverageClaim names a contract/domain-defined scope and says Complete, Partial, or Unknown, citing Evidence as provenance.
- 3Contract declares required Complete scopesCompleteness is required only where the contract says it matters. Other scopes may legitimately remain Partial or Unknown.
- 4Coverage gate runs before provider executionMissing, Partial, or Unknown required coverage becomes InsufficientCoverage. Ordo does not ask the provider to reason through an unestablished context.
- 5Negative knowledge stays scopedA negative observation can support absence only when the method could detect the target and matching coverage is Complete.
- 6Decision proceeds with explicit knowledge limitsThe provider sees coverage as data. Confidence, quantity of context, successful execution, or absence of errors never upgrades coverage implicitly.
A large evidence collection is not automatically Complete coverage. A confident model answer is not Complete coverage. A successful API call is not Complete coverage. The contract names the scopes for which completeness is required.
Authority is supplied by the host, not invented by the decision.
In current Ordo, Capability means a semantic authority prerequisite that the surrounding system has already established.
public readonly record struct CapabilityName(string Value);
public sealed record Capability(
CapabilityName Name,
string EstablishedBy,
DateTimeOffset EstablishedAt);
public static class CapabilityGate
{
private static readonly CapabilityName CorrectTimeEntry =
new("time-entry.correct");
public static bool CanCorrect(IReadOnlySet<Capability> supplied) =>
supplied.Any(c => c.Name == CorrectTimeEntry);
}
Capability is not authentication
It is not a JWT, API key, user identity, signature, or provider confidence score. Those may participate in how the host establishes authority, but the semantic core receives the already-established fact it needs.
The model cannot grant itself authority
A provider response may choose among legal alternatives. It cannot manufacture a missing Capability and thereby make an otherwise-illegal transition legal.
Refuse before model execution when prerequisites are missing.
The decision boundary is where state identity, evidence, coverage, authority, obligations, and the legal choice space come together.
public abstract record DecisionGateResult
{
private DecisionGateResult() { }
public sealed record Allowed(
IReadOnlyList<string> LegalChoices) : DecisionGateResult;
public sealed record InsufficientCoverage(
IReadOnlyList<CoverageScope> Missing) : DecisionGateResult;
public sealed record MissingCapability(
CapabilityName Capability) : DecisionGateResult;
public sealed record BlockedByObligation(
string Obligation) : DecisionGateResult;
}
public static DecisionGateResult EvaluateCorrection(
DecisionContract contract,
IReadOnlyList<ContextCoverageClaim> coverage,
IReadOnlySet<Capability> capabilities,
IReadOnlyList<string> openObligations)
{
var missingCoverage =
CoverageGate.MissingRequiredCoverage(contract, coverage);
if (missingCoverage.Count > 0)
return new DecisionGateResult.InsufficientCoverage(missingCoverage);
var requiredCapability =
new CapabilityName("time-entry.correct");
if (!capabilities.Any(c => c.Name == requiredCapability))
return new DecisionGateResult.MissingCapability(requiredCapability);
if (openObligations.Count > 0)
return new DecisionGateResult.BlockedByObligation(
openObligations[0]);
return new DecisionGateResult.Allowed(
["accept-correction", "reject-correction", "escalate"]);
}
- 1Semantically complete state viewThe domain projects the smallest authoritative view in which no omitted fact could change legality, evidence requirements, coverage, capability applicability, obligations, policy, or transition outcome.
- 2EvidenceTyped evidence identifies what was observed, where it came from, when it was observed, and what it depends on.
- 3Scoped coverageEach contract-defined scope is Complete, Partial, or Unknown. Required non-Complete coverage refuses the decision before provider execution.
- 4Capabilities and obligationsHost-established semantic authority is supplied as capabilities; unresolved required work remains explicit as obligations.
- 5Legal choice spacePolicy, guards, state, evidence, coverage, capabilities, and obligations determine which outcomes are actually legal.
- 6Compute, bounded decide, refuse, or escalateDeterministic work stays deterministic. Judgment happens only inside the legal choice space. Missing prerequisites do not force an answer.
- 7Result, requested effects, and auditThe result carries the state identity and decision facts forward; external work is requested as data and observations are recorded for later verification.
Only the Allowed case reaches a model or human judgment step. Missing coverage,
authority, or unresolved work is a semantic refusal, not another prompt.
Do not turn "we do not know" into failure.
A timeout after a write does not prove the write failed. If repeating the operation may duplicate a real-world effect, the uncertainty must survive.
public enum EffectStatus
{
Succeeded,
Failed,
Unknown
}
public sealed record EffectObservation(
string EffectId,
EffectStatus Status,
string? Detail);
public sealed record ReconciliationObligation(
string EffectId,
string Reason);
public static ReconciliationObligation? Interpret(
EffectObservation observation) =>
observation.Status switch
{
EffectStatus.Succeeded => null,
EffectStatus.Failed => null,
EffectStatus.Unknown =>
new ReconciliationObligation(
observation.EffectId,
"Observe the external system before any dependent transition.")
};
- 1Tier Two requests an effectThe semantic core describes the work as data. It does not perform the I/O.
- 2Tier Three coordinates the boundaryApplication code translates the request and hands it to the host without re-deciding the domain rule.
- 3Tier Four performs or observes the effectThe host returns what it can establish: Succeeded, Failed, or Unknown.
- 4Succeeded or Failed is interpreted normallyThe semantic core applies the domain meaning of the known result.
- 5Unknown creates a reconciliation obligationThe system preserves that the effect may or may not have happened. It does not pretend failure and blindly replay state-changing work.
- 6Tier Three / Four reconcile against realityThe external system is observed using the real contract. Retry is allowed early only when that contract proves retry safety.
- 7Tier Two interprets the reconciled factOnly after reality is re-established may later transitions that depend on the outcome proceed.
Why the extra state matters
If an HTTP request times out after a payment, message, deployment, or database operation, retrying blindly can be more dangerous than refusing to continue. Ordo keeps the unresolved effect visible until reality is re-established.
Make C# help, then verify what C# cannot know.
The compiler should be one member of the verification system, not the definition of correctness.
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
</PropertyGroup>
[Fact]
public void Superseded_entries_have_no_available_actions()
{
var entry = ExampleEntry.Recorded()
with { Status = TimeEntryStatus.Superseded };
var actions = AvailableActions.For(entry);
Assert.Equal(EntryActions.None, actions);
}
| Mechanism | Refuses | Cannot refuse |
|---|---|---|
| Type and exhaustiveness checking | Unrepresentable states, unhandled cases, signature drift | Code that type-checks and does the wrong thing |
| Architecture and dependency checks | Layering violations, inverted dependencies | Correct layering around incorrect behaviour |
| Contract and boundary checks | Representation collapse at a declared boundary | Boundaries nobody declared |
| Behavioural tests | Behaviour the author thought to assert | A check that passes for the wrong reason |
| Integration against a real boundary | Present-but-inert implementations, wire-format mismatches | Conditions the integration environment never produces |
| Independent adversarial review | Declared-but-unreachable outcomes, vacuous assertions | Anything the reviewer also fails to imagine |
Types can reject unrepresentable substitutions and missing enum cases. Architecture checks can reject dependency violations. Tests can verify behavior. Real-boundary integration can catch semantically inert implementations. Independent review can catch assumptions shared by the author and the tests.
C# does not need to become F# for Ordo to work.
Use the strongest mechanisms C# gives you, make their limits explicit, and compensate where the language cannot encode a rule as strongly as the domain requires. Ordo is about semantic authority and bounded reasoning, not loyalty to a syntax.