Actors
Decide whether a live in-memory consistency unit deserves an actor, then choose the runtime, durability, placement, and integration model.
Elarion.Actors turns a plain [Actor] class into a keyed, mailbox-protected state machine with a
source-generated typed facade. It is deliberately a small-to-mid application primitive: virtual
activation, sequential execution, idle passivation, and optional PostgreSQL snapshots, single-homing, and
fixed virtual-shard placement, without a clustered actor runtime.
Default to the database
Most applications need zero actors. If the solution sketches cleanly as a table with a version column, use optimistic concurrency, constraints, idempotency, jobs, and the outbox. An actor is justified only when the consistency unit is a live in-memory thing.
When (not) to use an actor
An actor earns its place in exactly three shapes:
- It owns a stateful external resource — a TCP session, device command channel, IMAP connection, or per-tenant API client whose calls must be ordered. You cannot row-lock a socket.
- Its state is hot, ephemeral, and loss-tolerant — live telemetry, presence, progress, or a write-behind buffer. Restart loss is acceptable or naturally repaired by the next reading.
- It is an event-driven decide-once coordinator — a small state machine that reacts over time and must act once. This is the only shape that normally combines snapshots and single-homing, and a versioned state row may still be simpler.
| Symptom | Reach for |
|---|---|
| Two users race to update a record | Optimistic concurrency |
| A webhook can arrive twice | [Idempotent] or the consumer inbox |
| A follow-up must survive the request | Outbox integration event |
| One import per tenant | Scheduled job claims |
| One ordered TCP/API session per key | Actor owning the resource |
| Live, disposable per-key readings | Actor with no snapshot |
| Act once after a sequence of events | Actor + snapshot, or a versioned state row |
For common loss-tolerant batching and latest-wins publishing, prefer the ready-made data-rate shaping helpers before writing an actor.
The basic shape
[Actor]
public sealed class DeviceSessionActor(
IActorContext<Guid> context,
IDeviceConnection connection)
{
public Task<DeviceReply> Send(DeviceCommand command, CancellationToken ct) =>
connection.SendAsync(context.Key, command, ct);
}The generator emits IDeviceSession : IActorFacade<Guid>. Callers keep a cheap address, not a live
activation handle:
var device = actors.Get<IDeviceSession>(deviceId);
var reply = await device.Send(command, ct);Get never activates anything. The first call activates the actor, calls are serialized through its
mailbox, and a cold activation passivates after five minutes by default. A facade remains valid across
passivation, and actor method renames fail at compile time at every call site.
Runtime and facades
Generated contracts, keying, mailboxes, reentrancy, lifecycle, configuration, and worker pools.
State and placement
PostgreSQL snapshots, query semantics, conflict retry, multi-instance topology, and the actor home.
Events, streams, and telemetry
Generated event relays, ordered facade streams, activation retention, traces, and metrics.
The migration seam
The generated facade is intentionally the same kind of contract as an Orleans grain interface: a
key-addressed interface of Task-shaped methods resolved through a factory.
| Elarion.Actors | Orleans |
|---|---|
IActorFacade<Guid> | IGrainWithGuidKey |
actors.Get<IMyActor>(id) | grainFactory.GetGrain<IMyActor>(id) |
[Reentrant] | [Reentrant] |
IActorState<TState> | [PersistentState] IPersistentState<TState> |
Migration is still real work: arguments and results must become serializable, the actor class becomes a grain implementation, custom keys need mapping, and placement moves to Orleans. Serializable DTO-shaped method signatures keep that seam honest.
Cross-module communication
Direct, synchronous module-to-module calls go through a published [ModuleContract]; an analyzer keeps modules honest, and an optional generated typed in-process API lets a module call its own handlers by name.
Actor runtime and facades
Generated facades, keying, activation, mailboxes, lifecycle, reentrancy, configuration, wiring, and worker-pool patterns.