Entity
- Question it answers
- What business state do we own?
- Load example
- Status and CarrierId belong to a Load.
Building Blocks
Define business entities, invariants, and the transitions that change them using familiar, typed C#.
An entity defines authoritative business state: the fields it contains and the invariants every valid state must satisfy. Its Transitions define the business decisions that can change that state.
Cohesive.Transitions lets you describe both in typed C#. A Transition receives the entity's current state and a typed input, checks the relevant rules, and returns a decision. That decision can include state changes, a typed outcome, and effects such as domain events or requests for other work.
Here is a Load with one useful operation. Assigning a carrier is allowed only while the Load is still a draft. A successful decision updates the state and emits a LoadAssigned domain event.
public enum LoadStatus { Draft, Assigned }
public enum AssignCarrierOutcome { Assigned, NotDraft, InvalidCarrier }
public sealed record AssignCarrierInput(string CarrierId);
public sealed record LoadAssigned(string CarrierId);
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("AssignedLoadsHaveCarrier",
load => load.Status != LoadStatus.Assigned || load.CarrierId != null
);
AssignCarrier = Transition<AssignCarrierInput, AssignCarrierOutcome>(
LoadSemantics.AssignCarrier,
transition => transition
.Requires(
(load, _) => load.Status == LoadStatus.Draft,
(_, _) => AssignCarrierOutcome.NotDraft
)
.Requires(
(_, input) => input.CarrierId != "",
(_, _) => AssignCarrierOutcome.InvalidCarrier
)
.Set(
load => load.CarrierId,
(_, input) => input.CarrierId
)
.Set(
load => load.Status,
LoadStatus.Assigned
)
.Emit(
LoadInteractions.LoadAssigned,
(_, input) => new LoadAssigned(input.CarrierId)
)
.Return(
TransitionOutcomeDisposition.Applied,
AssignCarrierOutcome.Assigned
));
}
public Field<LoadStatus> Status { get; }
public Field<string?> CarrierId { get; }
public Transition<Load, AssignCarrierInput, AssignCarrierOutcome> AssignCarrier { get; }
}LoadSemantics.AssignCarrier supplies stable metadata for the Transition, while LoadInteractions.LoadAssigned identifies the event contract. Those declarations are setup rather than business logic, so the example leaves them out. The Getting Started guide builds the complete version.
The guided walkthrough installs the package and builds the complete AssignCarrier example. It starts with the entity and its business rule, then shows how to evaluate one decision before connecting storage or messaging.
Open the Getting Started guide →
The model follows the way C# developers already tend to discuss domain behavior:
The expressions receive load and input directly. Reading load.Status gives the Transition the current business context it needs. No repository, service provider, or ambient request object is hidden inside the rule.
Ordinary entity methods are easy to start with. Over time, their rules often spread into handlers, persistence hooks, message publishers, API endpoints, and workflow code. Cohesive asks you to describe each change more explicitly so more of the surrounding system can work from the same definition.
A Transition decides; it does not save an entity or publish a message while the rule is running. Given the same state and input, it reaches the same result.
For AssignCarrier, a decision can say that the Load was assigned and include the two proposed field changes plus the event to emit. It can instead return NotDraft or InvalidCarrier with no accepted state change. The caller receives an explicit result in either case.
This separation keeps business meaning easy to test. Infrastructure can later apply an accepted decision with the transaction, concurrency, and delivery guarantees required by the application.
In an application, an entity Transition runs inside an execution environment. An API operation, command handler, or Cohesive Process supplies the typed input. The environment obtains the current entity state, evaluates the Transition, and receives its decision.
If the decision is accepted, the environment asks the configured storage integration to validate concurrency and commit the field changes. Its effect integration then records and delivers any emitted events or requests through the selected event store, outbox, or message queue. When an integration cannot provide the guarantees the decision requires, execution stops with capability diagnostics.
The entity remains pure because repositories, transactions, clocks, message publishers, and retry policies belong to this surrounding environment. The Internals guide describes the execution and commit boundaries in detail.
An entity invariant describes a rule that must hold regardless of which Transition produced the state. In the example, AssignedLoadsHaveACarrier belongs to the Load because it defines what a valid Load is. Entity validation uses that rule whenever it checks or constructs Load state.
A Transition can also add a candidate-state invariant to its own decision definition. Cohesive evaluates that local check before returning an accepted change. This is useful when the Transition must carry the complete protection needed at its execution boundary.
Admission rules answer a different question. requires-draft explains when AssignCarrier may begin, so a non-draft Load returns the typed NotDraft outcome instead of becoming an invariant failure.
Transitions can emit effects as part of an accepted decision. The referenced interaction contract says which kind of effect it is:
LoadAssigned.SchedulePickup. Its contract defines the expected response or terminal outcome, which lets a Process coordinate the obligation explicitly.Emitting an effect does not call a handler from inside the Transition. The decision identifies what should happen and carries the typed payload; the execution environment makes the state change and its outgoing interactions durable under the guarantees the application chooses.
A Load can expose transitions such as AssignCarrier, SchedulePickup, Depart, and Deliver, each with the state and input it needs.
Cohesive.Processes invokes Transitions, waits for requested work, and carries durable outcomes forward through a longer-running flow.Cohesive.Relations describes the facts and derived views used before or around a business decision.Cohesive.Storage reads current state and commits accepted changes and outgoing interactions with the required guarantees.Cohesive.Api and Cohesive.Presentation can project the same typed inputs and outcomes into operations and user experiences.The C# surface is only the entry point. Cohesive also needs to preserve a Transition across processes and versions, determine exactly which state it reads, validate references, detect conflicts, and hand accepted effects to storage and delivery infrastructure.
Those concerns introduce the canonical Transition document, expression lowering, compilation, sparse observations, semantic identity, execution evidence, and the boundary between a decision and its externalities. They matter once you are evaluating architecture or building adapters, but they are not prerequisites for understanding the domain model.