Actors
Build a single-consumer actor that mutates state without locks, then talk to it with fire-and-forget and request/response messages.
What you'll build
A CounterActor (SquidStd.Actors) that processes messages one at a time inside ReceiveAsync,
driven by TellAsync (fire-and-forget) and AskAsync (request/response).
Prerequisites
- .NET 10 SDK
dotnet add package SquidStd.Actors
Steps
1. Define the message contract and the actor
A marker interface groups the messages; Increment is fire-and-forget, GetTotal derives from
ActorRequest<int> to carry a reply. The actor mutates _total without locks because messages
are processed one at a time.
// The message contract: a marker interface, a fire-and-forget message, and an ask request.
internal interface ICounterMessage;
internal sealed record Increment(int By) : ICounterMessage;
internal sealed record GetTotal : ActorRequest<int>, ICounterMessage;
// A single-consumer actor: state is mutated without locks inside ReceiveAsync.
internal sealed class CounterActor : Actor<ICounterMessage>
{
private int _total;
protected override ValueTask ReceiveAsync(ICounterMessage message, CancellationToken cancellationToken)
{
switch (message)
{
case Increment increment:
_total += increment.By;
break;
case GetTotal request:
request.Reply(_total);
break;
}
return ValueTask.CompletedTask;
}
}
2. Send fire-and-forget messages
TellAsync enqueues a message and returns without waiting for it to be handled.
// Fire-and-forget messages: TellAsync enqueues without awaiting a reply.
await counter.TellAsync(new Increment(5));
await counter.TellAsync(new Increment(3));
3. Ask for a reply
AskAsync<TRequest, TReply> enqueues a request and awaits the typed reply the actor sends with
request.Reply(...).
// Request/response: AskAsync enqueues a request and awaits its typed reply.
var total = await counter.AskAsync<GetTotal, int>(new());
Console.WriteLine($"Total: {total}");
Run it
dotnet run --project samples/SquidStd.Samples.Actors
Prints Total: 8.