Kind and Outcome
- What it provides
- Applied, no-change, admission rejection, domain rejection, conflict, invalid definition, or infrastructure failure with a typed outcome when available.
Cohesive.Transitions Guide
Define a business entity and its invariant, author a typed Transition in C#, and evaluate one decision before connecting storage or messaging.
This guide models one AssignCarrier operation. A Load has a status and optional carrier. The Transition admits only draft Loads, rejects an empty carrier identity, updates the two relevant fields, and checks the resulting invariant.
The example stops after the business decision, which makes the first program useful in a test without requiring a database or message broker. Later sections show where persistence, compilation, and the external commit boundary enter.
Add the current prerelease package to a .NET 10 project:
dotnet add package Cohesive.Transitions --prereleaseImport the namespaces used by this walkthrough:
using Cohesive.Execution;
using Cohesive.Model;
using Cohesive.Model.Serialization;
using Cohesive.Transitions.Authoring;
using Cohesive.Transitions.Compilation;
using Cohesive.Transitions.Execution;
using Cohesive.Transitions.IR;The package is evolving through prereleases. Keep the authoring, persistence, and execution packages on the same published version.
Start with the domain types used by the Transition:
public enum LoadStatus
{
Draft,
Assigned
}
public enum AssignCarrierOutcome
{
Assigned,
NotDraft,
InvalidCarrier
}
public sealed record AssignCarrierInput(string CarrierId);Declare the entity fields and the rule that every valid Load must satisfy. An entity can own named Transition properties as shown on the overview page; this walkthrough authors the Transition separately so each setup step remains visible.
public sealed class Load : Entity<Load>
{
public Load()
{
Status = Field(nameof(Status), LoadStatus.Draft);
CarrierId = Field<string?>(
nameof(CarrierId),
initialValue: null,
configure: field => field.Optional());
Invariant(
"AssignedLoadsHaveACarrier",
load => load.Status != LoadStatus.Assigned ||
load.CarrierId != null);
}
public Field<LoadStatus> Status { get; }
public Field<string?> CarrierId { get; }
}The entity definition now contains the Load's shape and its always-true rule. Load.Define().Shape supplies the typed state model used by Transition authoring.
Give the Transition a stable definition identity, semantic revision, root-body identity, and provenance:
var metadata = new TransitionAuthoringMetadata(
new("transition/load/assign-carrier"),
new("revision/1"),
new("assign-carrier/body"),
new ExecutionProvenance(
new(TransitionAuthoring.Producer),
new("src/domain/Load.cs"),
DocumentOrigin.User),
displayName: "Assign carrier");Now author the legal change with typed expressions:
var authored = TransitionAuthoring.Create<
Load, AssignCarrierInput, AssignCarrierOutcome>(
Load.Define().Shape,
metadata,
transition =>
{
transition.Requires(
new("assign-carrier/admit/draft"),
(load, _) => load.Status == LoadStatus.Draft,
(_, _) => AssignCarrierOutcome.NotDraft);
transition.Choose(new("assign-carrier/validate"), choice => choice
.Case(
new("assign-carrier/valid"),
(_, input) => input.CarrierId != "",
valid => valid
.Set(
new("assign-carrier/set-carrier"),
load => load.CarrierId,
(_, input) => input.CarrierId)
.Set(
new("assign-carrier/set-status"),
load => load.Status,
LoadStatus.Assigned)
.Return(
new("assign-carrier/assigned"),
TransitionOutcomeDisposition.Applied,
AssignCarrierOutcome.Assigned))
.Fallback(
new("assign-carrier/invalid"),
invalid => invalid.Return(
new("assign-carrier/rejected"),
TransitionOutcomeDisposition.DomainRejected,
AssignCarrierOutcome.InvalidCarrier)));
transition.Invariant(
new("assign-carrier/invariant/carrier-required"),
load => load.Status != LoadStatus.Assigned ||
load.CarrierId != null);
});The callback is construction-time syntax. Its lambdas must lower into the portable expression language; arbitrary method calls, hidden I/O, captured runtime state, loops, and mutation are rejected during authoring.
Check structured validation before using the definition:
if (!authored.IsValid)
{
foreach (var diagnostic in authored.Validation.Diagnostics)
Console.Error.WriteLine(
$"{diagnostic.Code}: {diagnostic.Message}");
return;
}The typed handle exposes the canonical document and its exact reference:
var document = authored.Document;
var reference = authored.Reference;
Console.WriteLine(reference.DefinitionId);
Console.WriteLine(reference.RevisionId);
Console.WriteLine(reference.Fingerprint.Value);Persist the document rather than the builder callback or a compiled plan:
var json = ExecutionDefinitionJsonSerializer.Serialize(document);Strict restoration checks schema compatibility, the closed node model, canonical ordering, and the semantic fingerprint. A consumer can restore this JSON without loading the assembly that authored it.
Static compilation validates the document and derives an immutable executable plan:
var compilation = TransitionStaticCompiler.Compile(document);
if (!compilation.IsSuccessful)
{
foreach (var diagnostic in compilation.Validation.Diagnostics)
Console.Error.WriteLine(
$"{diagnostic.Code}: {diagnostic.Message}");
return;
}
var plan = compilation.Plan!;The plan is fingerprint-affine. It indexes the canonical program and its requirements, but it does not replace the document as semantic authority or select a storage engine.
Construct typed portable input and one coherent aggregate observation:
var input = PortableValue.Concrete(
plan.Definition.Input,
ObservationValue.FromObject(
new AssignCarrierInput("carrier-7")));
var state = PortableValue.Concrete(
plan.Definition.Observation,
ObservationValue.FromObject(new
{
Status = LoadStatus.Draft,
CarrierId = (string?)null
}));Run the deterministic reference interpreter:
var decision = TransitionReferenceInterpreter.DecideFullState(
plan,
new("assign-carrier/example-1"),
input,
state);
Console.WriteLine(decision.Kind);
// Applied
foreach (var patch in decision.Patch)
Console.WriteLine($"{patch.Path}: {patch.After.Value}");The same execution core accepts sparse observation entries when an adapter has acquired only the demanded fields. Sparse evaluation preserves the difference between an unobserved path and an explicit absent, null, unknown, failed, or concrete value.
The returned TransitionDecision keeps the semantic and execution evidence separate from infrastructure action:
The reference interpreter has not updated a database or published a message. A Storage or Process integration must:
Keeping that boundary visible prevents an in-memory callback from becoming accidental persistence authority.