Elarion
ConceptsActors

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:

  1. 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.
  2. 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.
  3. 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.
SymptomReach for
Two users race to update a recordOptimistic concurrency
A webhook can arrive twice[Idempotent] or the consumer inbox
A follow-up must survive the requestOutbox integration event
One import per tenantScheduled job claims
One ordered TCP/API session per keyActor owning the resource
Live, disposable per-key readingsActor with no snapshot
Act once after a sequence of eventsActor + 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.

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.ActorsOrleans
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.

On this page