![]() |
VOOZH | about |
dotnet add package MLambda.Actors.Abstraction --version 2.2.4
NuGet\Install-Package MLambda.Actors.Abstraction -Version 2.2.4
<PackageReference Include="MLambda.Actors.Abstraction" Version="2.2.4" />
<PackageVersion Include="MLambda.Actors.Abstraction" Version="2.2.4" />Directory.Packages.props
<PackageReference Include="MLambda.Actors.Abstraction" />Project file
paket add MLambda.Actors.Abstraction --version 2.2.4
#r "nuget: MLambda.Actors.Abstraction, 2.2.4"
#:package MLambda.Actors.Abstraction@2.2.4
#addin nuget:?package=MLambda.Actors.Abstraction&version=2.2.4Install as a Cake Addin
#tool nuget:?package=MLambda.Actors.Abstraction&version=2.2.4Install as a Cake Tool
👁 NuGet
👁 Build and Test
👁 codecov
MLambda is a Reactive Actor Model framework for .NET with built-in clustering, gossip protocol, and mTLS security. It provides a lightweight actor system with guardian hierarchy supervision, reactive message passing via System.Reactive, and a clean API built on C# pattern matching.
dotnet add package MLambda.Actors
IObservable<T> via System.ReactiveMicrosoft.Extensions.DependencyInjectionAdd the MLambda.Actors packages to your project.
var services = new ServiceCollection();
services.AddActor(); // Core actor system
services.AddActor<MyActor>(); // Register your actors
Inherit from Actor and override Receive using C# pattern matching:
[Route("/counter")]
public class CounterActor : Actor
{
private int count;
protected override Behavior Receive(object data) =>
data switch
{
Increment _ => Actor.Behavior<int>(this.HandleIncrement),
GetCount _ => Actor.Behavior<int>(() => Observable.Return(this.count)),
_ => Actor.Ignore,
};
private IObservable<int> HandleIncrement()
{
this.count++;
return Observable.Return(this.count);
}
}
// Inject IUserContext via DI
var address = await userContext.Spawn<CounterActor>();
// Synchronous send (request-response)
int count = await address.Send<Increment, int>(new Increment());
// Fire-and-forget
address.Send(new Increment()).Subscribe();
The Receive method returns a Behavior delegate (IObservable<object> Behavior(IContext context)). Use Actor.Behavior(...) factory methods to create behaviors from your handler methods:
protected override Behavior Receive(object data) =>
data switch
{
string message => Actor.Behavior(this.Print, message), // with parameter
int value => Actor.Behavior(this.Process, value), // typed parameter
_ => Actor.Ignore, // unhandled
};
Handler methods can optionally accept IContext as the first parameter to access the actor context (Self, Spawn, Watch, etc.).
Actors can switch their message handling behavior at runtime:
public class BecomeActor : Actor
{
protected override Behavior Receive(object data) =>
data switch
{
SetMood m when m.Mood == "happy" => Actor.Behavior(this.SwitchToHappy),
AskMood _ => Actor.Behavior<string>(() => Observable.Return("normal")),
_ => Actor.Ignore,
};
private IObservable<string> SwitchToHappy(IContext ctx)
{
this.Become(this.HappyBehavior); // Switch behavior
return Observable.Return("now happy");
}
private Behavior HappyBehavior(object data) =>
data switch
{
AskMood _ => Actor.Behavior<string>(() => Observable.Return("happy")),
SetMood m when m.Mood == "normal" => Actor.Behavior(this.RevertToNormal),
_ => Actor.Ignore,
};
private IObservable<string> RevertToNormal(IContext ctx)
{
this.Unbecome(); // Revert to default Receive
return Observable.Return("back to normal");
}
}
Actors can stash messages they cannot yet handle and replay them later. This is commonly combined with Become for initialization patterns:
public class StashActor : Actor
{
private readonly List<string> processed = new();
protected override Behavior Receive(object data) =>
data switch
{
Initialize _ => Actor.Behavior(this.HandleInit),
string _ => Actor.Behavior(this.StashIt), // Not ready yet
_ => Actor.Ignore,
};
private IObservable<string> HandleInit(IContext ctx)
{
this.Become(this.ReadyBehavior);
this.UnstashAll(); // Replay all stashed messages
return Observable.Return("initialized");
}
private IObservable<string> StashIt(IContext ctx)
{
this.Stash?.Stash(); // Buffer current message
return Observable.Return("stashed");
}
private Behavior ReadyBehavior(object data) =>
data switch
{
string msg => Actor.Behavior(this.Process, msg),
_ => Actor.Ignore,
};
private IObservable<string> Process(string msg)
{
this.processed.Add(msg);
return Observable.Return($"processed: {msg}");
}
}
Define how parent actors handle child failures:
OneForOne -- Only the failing child is affected:
public class MyActor : Actor
{
public override ISupervisor Supervisor => Strategy.OneForOne(
decider => decider
.When<InvalidOperationException>(Directive.Resume)
.When<InvalidCastException>(Directive.Restart)
.Default(Directive.Escalate));
}
AllForOne -- All sibling children are affected when one fails:
public class ParentActor : Actor
{
private readonly IBucket bucket;
public ParentActor(IBucket bucket) => this.bucket = bucket;
public override ISupervisor Supervisor => Strategy.AllForOne(
decider => decider
.When<InvalidOperationException>(Directive.Resume)
.When<InvalidCastException>(Directive.Restart)
.Default(Directive.Escalate),
this.bucket);
}
Directives: Resume (ignore error), Restart (recreate actor), Stop (terminate), Escalate (pass to parent).
Monitor another actor's lifecycle:
protected override Behavior Receive(object data) =>
data switch
{
WatchTarget t => Actor.Behavior(this.StartWatching, t),
Terminated t => Actor.Behavior(this.OnTerminated, t),
_ => Actor.Ignore,
};
private IObservable<string> StartWatching(IContext ctx, WatchTarget target)
{
ctx.Watch(target.Address); // Register for Terminated notifications
return Observable.Return("watching");
}
private IObservable<string> OnTerminated(Terminated t)
{
// The watched actor has stopped
return Observable.Return("target terminated");
}
Override lifecycle methods to execute code at specific points:
public class MyActor : Actor
{
public override void PreStart() { /* Before first message */ }
public override void PostStop() { /* After actor stops */ }
public override void PreRestart(Exception reason) { /* Before restart */ }
public override void PostRestart(Exception reason) { /* After restart */ }
protected override Behavior Receive(object data) => Actor.Ignore;
}
| Package | Description |
|---|---|
MLambda.Actors.Abstraction |
Core abstractions and interfaces |
MLambda.Actors |
Actor system implementation |
MLambda.Actors.Core |
DI registration and ActorHost |
MLambda.Actors.Surround |
Route resolution, StandaloneStrategy |
MLambda.Actors.Network |
TCP transport layer |
MLambda.Actors.Gossip |
Gossip protocol for cluster membership |
MLambda.Actors.Gossip.Data |
CRDT data structures with replication |
MLambda.Actors.Cluster |
Cluster routing, delivery, strategies |
MLambda.Actors.Fortress |
mTLS security with auto-rotating certs |
MLambda.Actors.Monitoring |
OpenTelemetry metrics and tracing |
MLambda.Saga |
Distributed transaction sagas |
All artifacts share the same version, published on every v* tag.
docker pull ghcr.io/mlambda-net/mlambda-cluster:2.2.3
| Tag | Description |
|---|---|
ghcr.io/mlambda-net/mlambda-cluster:2.2.3 |
Specific version |
ghcr.io/mlambda-net/mlambda-cluster:latest |
Latest release |
# Cluster chart
helm pull oci://ghcr.io/mlambda-net/charts/mlambda-cluster --version 2.2.3
# Service template chart
helm pull oci://ghcr.io/mlambda-net/charts/mlambda-my-service --version 2.2.3
# Demo chart
helm pull oci://ghcr.io/mlambda-net/charts/mlambda-demo --version 2.2.3
Bash / Linux / macOS:
curl -sSL https://mlambda-net.github.io/mlambda/scripts/install-cluster.sh | bash
With options:
curl -sSL https://mlambda-net.github.io/mlambda/scripts/install-cluster.sh | bash -s -- --version 2.2.3 --replicas 4
PowerShell / Windows:
iwr -useb https://mlambda-net.github.io/mlambda/scripts/install-cluster.ps1 | iex
With options:
$env:MLAMBDA_VERSION="2.2.3"; $env:MLAMBDA_REPLICAS="4"; iwr -useb https://mlambda-net.github.io/mlambda/scripts/install-cluster.ps1 | iex
# Dev: build locally and deploy
skaffold dev -p dev
# Release: deploy from published GHCR artifacts
skaffold deploy -p release
Client ───TCP──► Cluster ◄──Gossip──► Cluster
│ │
Hybrid Hybrid
(Actors) (Actors)
Three topologies:
Actors use context.Ref<T>() to address other actors -- the topology determines how the reference is resolved (locally or via cluster routing). Business actors are topology-agnostic.
src/
MLambda.Actors.Abstraction/ # Interfaces: IContext, IAddressStrategy, Topology
MLambda.Actors/ # Core: Bucket, Process, Address, Context
MLambda.Actors.Core/ # DI registration, ActorHost, ActorHostConfig
MLambda.Actors.Surround/ # Route resolution, StandaloneStrategy, ActorHost
MLambda.Actors.Network/ # TCP transport
MLambda.Actors.Gossip/ # Gossip protocol
MLambda.Actors.Cluster/ # ClusterStrategy, HybridStrategy, RouteAddress
MLambda.Actors.Fortress/ # mTLS security layer
MLambda.Actors.Monitoring/ # OpenTelemetry integration
test/
MLambda.Actors.Test/ # Reqnroll BDD tests
Tests use SpecFlow (BDD) with Gherkin feature files:
dotnet test
Example scenario:
Feature: Message stashing with Become
Scenario: Messages sent before initialization are stashed and processed after
Given a stash actor
When the messages "alpha", "beta", "gamma" are sent
And the actor is initialized
And the processed messages are queried
Then the processed messages should be "alpha", "beta", "gamma"
Create a local Kind cluster for development:
kind create cluster --name mlambda
Build the Docker image and load it into Kind. Always use unique tags — Kind caches :latest aggressively and may not pick up new builds:
# Build with a unique tag
docker build -f cmd/MLambda.Actors.Host/Dockerfile -t mlambda/actors-host:v1 .
# Load into Kind (required — Kind can't pull from local Docker)
kind load docker-image mlambda/actors-host:v1 --name mlambda
# Update the running deployment
kubectl set image statefulset/mlambda-cluster mlambda-cluster=mlambda/actors-host:v1 -n mlambda-system
kubectl rollout status statefulset/mlambda-cluster -n mlambda-system
For subsequent builds, increment the tag (v2, v3, etc.) to force Kind to use the new image.
To connect a local Asteroid client to cluster pods inside Kind:
# Forward two cluster pods (run each in a separate terminal or background)
kubectl port-forward -n mlambda-system mlambda-cluster-0 15001:5000
kubectl port-forward -n mlambda-system mlambda-cluster-1 15002:5000
Then set CLUSTER_NODES for the demo client:
CLUSTER_NODES="localhost,15001;localhost,15002" dotnet run --project demo/MLambda.Actors.Demo.Player
Note: The CLUSTER_NODES format is host,port pairs separated by ; (not host:port).
Windows caveat: Background port-forward processes can die silently. If connections fail, kill stale port-forward processes and restart them.
# Scale up
kubectl scale deployment mlambda-satellite -n mlambda-system --replicas=3
# Scale down
kubectl scale deployment mlambda-satellite -n mlambda-system --replicas=1
After scaling, wait ~10 seconds for heartbeats to propagate. The cluster automatically purges stale satellite entries and re-routes actors to live satellites.
# All cluster pod logs
kubectl logs -n mlambda-system -l app=mlambda-cluster --tail=50
# Follow a specific pod
kubectl logs -n mlambda-system mlambda-cluster-0 -f
# Satellite logs
kubectl logs -n mlambda-system -l app=mlambda-satellite --tail=50
The cluster uses layered supervision to handle failures in distributed actor creation and message delivery:
Transport-level timeouts (TcpTransport.Send) — Every TCP send has a 5-second timeout via CancellationTokenSource. If a node is unreachable, the send fails fast instead of hanging indefinitely.
Actor creation supervision (CreatorActor) — When a satellite is asked to create an actor, a 10-second timer starts. If the satellite doesn't respond in time, CreatorActor sends a failed CreateActorResponse back to RouteActor, which resets the route to Dead for retry on another satellite.
Stale satellite purging (RouteActor.PurgeStaleSatellites) — Satellites have a LastSeen timestamp updated on every heartbeat. Entries older than 15 seconds are removed, and their routes are marked Dead so actors get recreated on live satellites.
Route validation on dispatch (RouteActor.HandleDispatchWork) — Before dispatching to a satellite, the route actor verifies the target satellite is still in the live satellites list. If the satellite is gone (e.g., after scale-down), the route is marked Dead and the actor is recreated elsewhere.
Periodic re-registration (NodeLifecycleService.SendHeartbeats) — Satellites re-send their registration message alongside every heartbeat (every 5 seconds). This ensures all cluster nodes eventually learn about all satellites, even if the initial registration was lost.
| Symptom | Cause | Fix |
|---|---|---|
Send() hangs forever |
No transport timeout | Transport has 5s timeout; check if CancellationTokenSource is wired |
| Actor creation times out | Satellite unreachable or slow | CreatorActor has 10s timeout; check satellite logs |
| Actors not found after scale-down | Routes point to dead satellites | PurgeStaleSatellites runs on every dispatch; wait ~15s |
| Satellite not registered on all clusters | Initial registration lost | Heartbeats re-send registration every 5s; wait for convergence |
| Port-forward connections fail silently | Stale process on Windows | Kill and restart port-forward processes |
| Kind ignores new Docker image | Cached :latest tag |
Use unique tags (v1, v2) and kind load docker-image |
Console.WriteLine at key points: RouteActor.HandleDispatchWork, CreatorActor.HandleCreateActorRequest, NodeLifecycleService.SendHeartbeats.Running, Waiting, Dead) transitions to identify stuck or stale entries.Console.WriteLine calls after fixing.This project is licensed under the MIT License. See for details.
<p align="center"> <a href="https://www.buymeacoffee.com/yordivad" target="_blank"> <img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" width="217px" height="51px"> </a> </p>
| Product | Versions Compatible and additional computed target framework versions. |
|---|---|
| .NET | net10.0 net10.0 is compatible. net10.0-android net10.0-android was computed. net10.0-browser net10.0-browser was computed. net10.0-ios net10.0-ios was computed. net10.0-maccatalyst net10.0-maccatalyst was computed. net10.0-macos net10.0-macos was computed. net10.0-tvos net10.0-tvos was computed. net10.0-windows net10.0-windows was computed. |
Showing the top 5 NuGet packages that depend on MLambda.Actors.Abstraction:
| Package | Downloads |
|---|---|
|
MLambda.Actors
Reactive actor model implementation for .NET |
|
|
MLambda.Actors.Network
TCP transport layer for MLambda actor communication |
|
|
MLambda.Actors.Core
Dependency injection registration for MLambda actors |
|
|
MLambda.Actors.Gossip
Gossip protocol for cluster membership and failure detection |
|
|
MLambda.Actors.Gossip.Data
CRDT data structures with gossip-based replication |
This package is not used by any popular GitHub repositories.
| Version | Downloads | Last Updated |
|---|---|---|
| 2.2.4 | 301 | 5/1/2026 |
| 2.2.4-rc.f671 | 94 | 5/1/2026 |
| 2.2.4-rc.68e7 | 101 | 5/7/2026 |
| 2.2.3 | 211 | 4/27/2026 |
| 2.2.3-rc.d6bd | 76 | 4/27/2026 |
| 2.2.2 | 191 | 4/27/2026 |
| 2.2.2-rc.8f6d | 87 | 4/27/2026 |
| 2.2.2-rc.86d6 | 67 | 4/27/2026 |
| 2.2.1 | 279 | 4/27/2026 |
| 2.2.1-rc.95d5 | 75 | 4/27/2026 |
| 2.2.1-rc.3fea | 73 | 4/27/2026 |
| 2.2.0 | 282 | 4/25/2026 |
| 2.2.0-rc.2eee | 79 | 4/25/2026 |
| 2.1.0 | 310 | 4/23/2026 |
| 2.0.0-rc.f358 | 84 | 3/25/2026 |
| 2.0.0-rc.cdc2 | 90 | 3/25/2026 |
| 2.0.0-rc.a4f6 | 84 | 3/25/2026 |
| 2.0.0-rc.883c | 80 | 3/25/2026 |
| 2.0.0-rc.33b9 | 76 | 3/25/2026 |
| 2.0.0-rc.7452 | 73 | 3/23/2026 |