SquidStd.Actors
Ordered, single-threaded, lock-free per-entity message processing for SquidStd, built on TPL Dataflow
ActionBlock<T>. Each actor owns a mailbox processed by one consumer, so handler state is mutated
without locks. Complements the EventBus (stateless broadcast) and CommandDispatcher (stateless fan-out)
with ordered, stateful, per-instance processing.
Install
dotnet add package SquidStd.Actors
Usage
using SquidStd.Actors;
public interface ISessionMessage { }
public sealed record LineReceived(string Raw) : ISessionMessage;
public sealed record SendText(string Text) : ISessionMessage;
public sealed record GetNick : ActorRequest<string?>, ISessionMessage; // request/response
public sealed class SessionActor : Actor<ISessionMessage>
{
private readonly INetworkConnection _conn;
private string? _nick;
public SessionActor(INetworkConnection conn) { _conn = conn; }
protected override async ValueTask ReceiveAsync(ISessionMessage message, CancellationToken ct)
{
switch (message)
{
case LineReceived(var raw): if (raw.StartsWith("NICK ")) _nick = raw[5..]; break;
case SendText(var text): await _conn.WriteAsync(text, ct); break;
case GetNick q: q.Reply(_nick); break;
}
}
}
await using var session = new SessionActor(conn);
await session.TellAsync(new LineReceived("NICK alice")); // fire-and-forget
string? nick = await session.AskAsync<GetNick, string?>(new GetNick()); // request/response
Use the event bus as an ordered source:
using SquidStd.Actors.Extensions;
using var sub = session.SubscribeToEventBus(eventBus, (UserJoinedEvent e) => new SendText($":{e.Nick} JOIN"));
Key types
| Type | Purpose |
|---|---|
Actor<TMessage> |
Mailbox base class (TellAsync / AskAsync). |
ActorRequest<TReply> |
Base record for request/response messages. |
IActorRequest<TReply> |
Request contract (implement directly if not using the base). |
ActorOptions |
Capacity / overflow / error configuration. |
Options
ActorOptions controls the mailbox:
| Property | Values | Default |
|---|---|---|
Capacity |
bounded mailbox size | 1024 |
OverflowPolicy |
Wait / DropNewest / Unbounded |
Wait |
ErrorPolicy |
Isolate / StopOnError |
Isolate |
ShutdownDrainTimeout |
any TimeSpan |
5s |
- Wait:
TellAsyncawaits until capacity frees (back-pressure). - DropNewest:
TellAsyncreturnsfalsewhen full. - Isolate: a throwing handler is logged and skipped; the actor stays alive.
AskAsyncexceptions always propagate to the caller regardless of policy.
DisposeAsync completes the mailbox and drains queued messages - every Tell runs and every Ask
replies - within ShutdownDrainTimeout. If a handler is still running when that budget elapses, the
actor cancels its handlers and faults any requests that never completed with ObjectDisposedException.
Related
- Tutorial: Actors
- Concept: Messaging models
License
MIT - part of SquidStd.