From dotnet-skills
Provides Akka.NET best practices for EventStream vs DistributedPubSub, supervision strategies, error handling, Props vs DependencyResolver, work distribution, and cluster/local testability abstractions.
npx claudepluginhub aaronontheweb/dotnet-skills --plugin dotnet-skillsThis skill uses the workspace's default tool permissions.
Use this skill when:
Creates isolated Git worktrees for feature branches with prioritized directory selection, gitignore safety checks, auto project setup for Node/Python/Rust/Go, and baseline verification.
Executes implementation plans in current session by dispatching fresh subagents per independent task, with two-stage reviews: spec compliance then code quality.
Dispatches parallel agents to independently tackle 2+ tasks like separate test failures or subsystems without shared state or dependencies.
Use this skill when:
Context.System.EventStream is local to a single ActorSystem process. It does NOT work across cluster nodes.
// BAD: This only works on a single server
// When you add a second server, subscribers on server 2 won't receive events from server 1
Context.System.EventStream.Subscribe(Self, typeof(PostCreated));
Context.System.EventStream.Publish(new PostCreated(postId, authorId));
When EventStream is appropriate:
For events that must reach actors across multiple cluster nodes, use Akka.Cluster.Tools.PublishSubscribe:
using Akka.Cluster.Tools.PublishSubscribe;
public class TimelineUpdatePublisher : ReceiveActor
{
private readonly IActorRef _mediator;
public TimelineUpdatePublisher()
{
// Get the DistributedPubSub mediator
_mediator = DistributedPubSub.Get(Context.System).Mediator;
Receive<PublishTimelineUpdate>(msg =>
{
// Publish to a topic - reaches all subscribers across all nodes
_mediator.Tell(new Publish($"timeline:{msg.UserId}", msg.Update));
});
}
}
builder.WithDistributedPubSub(role: null); // Available on all roles, or specify a role
| Pattern | Topic Format | Use Case |
|---|---|---|
| Per-user | timeline:{userId} | Timeline updates, notifications |
| Per-entity | post:{postId} | Post engagement updates |
| Broadcast | system:announcements | System-wide notifications |
| Role-based | workers:rss-poller | Work distribution |
A supervision strategy defined on an actor dictates how that actor supervises its children, NOT how the actor itself is supervised.
public class ParentActor : ReceiveActor
{
// This strategy applies to children of ParentActor, NOT to ParentActor itself
protected override SupervisorStrategy SupervisorStrategy()
{
return new OneForOneStrategy(
maxNrOfRetries: 10,
withinTimeRange: TimeSpan.FromSeconds(30),
decider: ex => ex switch
{
ArithmeticException => Directive.Resume,
NullReferenceException => Directive.Restart,
ArgumentException => Directive.Stop,
_ => Directive.Escalate
});
}
}
The default OneForOneStrategy already includes rate limiting:
You rarely need a custom strategy unless you have specific requirements.
Good reasons:
AllForOneStrategyBad reasons:
Use try-catch when:
public class RssFeedPollerActor : ReceiveActor
{
public RssFeedPollerActor()
{
ReceiveAsync<PollFeed>(async msg =>
{
try
{
var feed = await _httpClient.GetStringAsync(msg.FeedUrl);
var items = ParseFeed(feed);
// Process items...
}
catch (HttpRequestException ex)
{
// Expected failure - log and schedule retry
_log.Warning("Feed {Url} unavailable: {Error}", msg.FeedUrl, ex.Message);
Context.System.Scheduler.ScheduleTellOnce(
TimeSpan.FromMinutes(5), Self, msg, Self);
}
catch (XmlException ex)
{
// Invalid feed format - log and mark as bad
_log.Error("Feed {Url} has invalid format: {Error}", msg.FeedUrl, ex.Message);
Sender.Tell(new FeedPollResult.InvalidFormat(msg.FeedUrl));
}
});
}
}
Let exceptions propagate (trigger supervision) when:
// BAD: Swallowing exceptions hides problems
catch (Exception ex)
{
_log.Error(ex, "Error processing work");
// Actor continues with potentially corrupt state
}
// GOOD: Handle known exceptions, let unknown ones propagate
catch (HttpRequestException ex)
{
// Known, expected failure - handle gracefully
_log.Warning("HTTP request failed: {Error}", ex.Message);
Sender.Tell(new WorkResult.TransientFailure());
}
// Unknown exceptions propagate to supervision
Use Props.Create() when:
IServiceProvider or IRequiredActor<T>// Simple actor with no DI needs
public static Props Props(PostId postId, IPostWriteStore store)
=> Akka.Actor.Props.Create(() => new PostEngagementActor(postId, store));
Use resolver.Props<T>() when:
IServiceProvider to create scoped servicesIRequiredActor<T> to get references to other actors// Registration with DI
builder.WithActors((system, registry, resolver) =>
{
var actor = system.ActorOf(resolver.Props<OrderProcessorActor>(), "order-processor");
registry.Register<OrderProcessorActor>(actor);
});
You almost never need remote deployment. If you're not doing remote deployment (and you probably aren't):
Props.Create(() => new Actor(...)) with closures is fineFor most applications, use cluster sharding instead of remote deployment - it handles distribution automatically.
When you have many background jobs (RSS feeds, email sending, etc.), don't process them all at once - this causes thundering herd problems.
Three patterns to solve this:
FOR UPDATE SKIP LOCKED for natural cross-node distributionSee work-distribution-patterns.md for full code samples.
| Mistake | Why It's Wrong | Fix |
|---|---|---|
| Using EventStream for cross-node pub/sub | EventStream is local only | Use DistributedPubSub |
| Defining supervision to "protect" an actor | Supervision protects children | Understand the hierarchy |
| Catching all exceptions | Hides bugs, corrupts state | Only catch expected errors |
| Always using DependencyResolver | Adds unnecessary complexity | Use plain Props when possible |
| Processing all background jobs at once | Thundering herd, resource exhaustion | Use database queue + rate limiting |
| Throwing exceptions for expected failures | Triggers unnecessary restarts | Return result types, use messaging |
Need to communicate between actors?
├── Same process only? -> EventStream is fine
├── Across cluster nodes?
│ ├── Point-to-point? -> Use ActorSelection or known IActorRef
│ └── Pub/sub? -> Use DistributedPubSub
└── Fire-and-forget to external system? -> Consider outbox pattern
Exception occurred in actor?
├── Expected failure (HTTP timeout, invalid input)?
│ └── Try-catch, handle gracefully, continue
├── State might be corrupt?
│ └── Let supervision restart
├── Unknown cause?
│ └── Let supervision restart
└── Programming error (null ref, bad logic)?
└── Let supervision restart, fix the bug
Creating actor Props?
├── Actor needs IServiceProvider?
│ └── Use resolver.Props<T>()
├── Actor needs IRequiredActor<T>?
│ └── Use resolver.Props<T>()
├── Simple actor with constructor params?
│ └── Use Props.Create(() => new Actor(...))
└── Remote deployment needed?
└── Probably not - use cluster sharding instead
For applications that need to run both in clustered production and local/test environments, use abstraction patterns to toggle between implementations:
AkkaExecutionMode enum - Controls which implementations are used (LocalTest vs Clustered)GenericChildPerEntityParent - Mimics sharding behavior locally using the same IMessageExtractorIPubSubMediator - Abstracts DistributedPubSub for swappable local/cluster implementationsSee cluster-local-abstractions.md for complete implementation code.
In actors, use ILoggingAdapter from Context.GetLogger() instead of DI-injected ILogger<T>:
public class MyActor : ReceiveActor
{
private readonly ILoggingAdapter _log = Context.GetLogger();
public MyActor()
{
Receive<MyMessage>(msg =>
{
_log.Info("Processing message for user {UserId}", msg.UserId);
_log.Error(ex, "Failed to process {MessageType}", msg.GetType().Name);
});
}
}
Why ILoggingAdapter:
Info(), Debug(), Warning(), Error() (not Log* variants)Don't inject ILogger into actors - it bypasses Akka's logging infrastructure.
// Named placeholders for better log aggregation and querying
_log.Info("Order {OrderId} processed for customer {CustomerId}", order.Id, order.CustomerId);
// Prefer named placeholders over positional
// Good: {OrderId}, {CustomerId}
// Avoid: {0}, {1}
When actors launch async operations via PipeTo, those operations can outlive the actor if not properly managed. Key practices:
PostStop()See async-cancellation-patterns.md for complete implementation code.