Actor boundaries and identity
An actor groups state and the commands that decide changes to it. Start by asking which facts must be considered together for one decision. A registration actor owns a particular registration. A workshop roster actor owns the roster for a particular workshop. These are separate identities even if the UI shows both on one page.
On this page
A useful boundary follows a domain responsibility, rather than a controller method or database table. It should be possible to explain what the actor owns, how it is addressed, and which rules it enforces.
Choose a stable actor key
The workshop sample addresses a registration by a GUID. The same key is used in its domain message so the actor can reject a mismatch.
public static Task RequestAsync(ITroolioClient client, Guid registrationId, Guid workshopId,
string displayName, Guid authenticatedUserId, Guid verifiedDeviceId, Guid correlationId)
{
Metadata headers = MessageContext.AtIngress(correlationId, authenticatedUserId, verifiedDeviceId);
return client.Tell<IRegistrationActor>(registrationId.ToString("D"),
new RequestRegistration(headers, registrationId, workshopId, displayName));
}
The IRegistrationActor contract identifies the actor role, and the key identifies one instance of that role. A user ID, registration ID, workshop ID, and source stream name are separate identifiers. Use explicit names in messages instead of assuming every GUID means the same entity.
Put the rule where its facts live
The registration actor decides whether that registration already exists and whether its owner can cancel it. The roster actor records the registration's participation in a workshop. A read model shows those outcomes efficiently to readers.
If the application introduces a hard capacity rule, a single authoritative actor or commit policy must own the capacity decision. Checking a projected count and then requesting a place can race with another registration. The projection is a convenient view; the capacity decision needs current write-side state and the chosen concurrency contract.
The sample models registration requests and roster bookkeeping. It does not claim to reserve a finite seat across two independent actors. For a multi-participant invariant, review atomic commit and the selected route's full store and authority requirements.
Creation and lifecycle
An actor reference can exist before the domain entity does. Decide how the first command creates state and what other commands do while it is absent. The sample's state starts with status absent; only a valid request transitions it to requested.
[GenerateSerializer]
public record RegistrationState(Guid RegistrationId, Guid WorkshopId,
Guid AttendeeId, string DisplayName, string Status) : IActorState
{
public static RegistrationState Empty => new(Guid.Empty, Guid.Empty,
Guid.Empty, "", "absent");
public RegistrationState Apply(RegistrationRequested e) =>
new(e.RegistrationId, e.WorkshopId, e.AttendeeId, e.DisplayName, "requested");
public RegistrationState Apply(RegistrationCancelled e) =>
new(e.RegistrationId, e.WorkshopId, e.AttendeeId, e.DisplayName, "cancelled");
}
CreatableActor provides an explicit creation lifecycle when that base class suits the model. EventSourcedActor can also implement domain creation rules directly, as this example does. Whichever approach you use, test duplicate creation, cancellation, and reuse of the key after deletion or retirement.
Design for the actor's history
An event-sourced actor loads its own history on activation and applies events to rebuild current state. Keep a boundary narrow enough that one actor's state and history remain manageable. A global actor that accumulates every customer and registration creates a large coordination and replay bottleneck.
For a catalogue across many actors, build a read model from committed events. Avoid activating every registration actor just to render a list. Queries that must examine many records belong to an indexed query model with bounded reads.
Scheduling and concurrency
Orleans controls activation and scheduling, but interleaving attributes change how requests can overlap around asynchronous work. Adding Reentrant to an actor with mutable decision state requires a review of its invariants. The sample leaves the state-owning actors without that attribute.
Even with orderly processing in one actor, calls to different actors are not automatically one database transaction. Stream expected versions and provider fences still matter. See Orleans scheduling for the runtime model, then orchestration workflows for cross-actor work.
Continue with CQRS in practice, replay and live events, and the workshop walkthrough.