Akka.NET in ConnectSoft Microservice Template¶
Purpose & Overview¶
Akka.NET is a port of the popular Akka framework for building distributed, concurrent, and resilient applications using the Actor Model. While the ConnectSoft Microservice Template primarily uses Microsoft Orleans as its actor runtime, Akka.NET provides an alternative implementation option for teams requiring more fine-grained control over actor lifecycle, supervision hierarchies, and message routing.
Akka.NET enables:
- Hierarchical Actors: Parent-child actor relationships with supervision strategies
- Explicit Lifecycle Management: Fine-grained control over actor creation, restart, and termination
- Advanced Routing: Complex message routing patterns (round-robin, consistent hashing, broadcast)
- Persistence: Event sourcing and snapshots for stateful actors
- Cluster Sharding: Automatic distribution of actors across cluster nodes
- Location Transparency: Actors can be local or remote with transparent communication
- Fault Tolerance: Supervision hierarchies for automatic error recovery
- Stream Processing: Akka Streams for reactive data processing pipelines
Akka.NET vs Orleans
Akka.NET provides more control and flexibility but requires more configuration and understanding of actor lifecycle. Orleans abstracts away many complexities but provides less fine-grained control. Choose Akka.NET when you need hierarchical supervision, complex routing, or event sourcing patterns. Choose Orleans for simpler virtual actor semantics with automatic lifecycle management.
Architecture Overview¶
Akka.NET in ConnectSoft Architecture¶
Application Layer
├── Processors (Commands/Writes)
└── Retrievers (Queries/Reads)
↓ (Actor Invocation)
Akka.NET ActorSystem
├── ActorSystem (Actor Runtime)
├── Actors (Actor Instances)
│ ├── Domain Actors (BankAccountActor, etc.)
│ ├── Supervisor Actors
│ └── Router Actors
├── Actor Persistence (Event Sourcing)
├── Cluster Sharding (Distributed Actors)
└── Akka Streams (Reactive Processing)
↓
Storage Layer
└── Event Store / Snapshot Store
Key Integration Points¶
| Layer | Component | Responsibility |
|---|---|---|
| ActorModel | IBankAccountActor |
Domain actor interface (framework-agnostic) |
| ActorModel.Akka | BankAccountActor, BankAccountActorRef |
Akka.NET-specific actor implementation |
| ApplicationModel | AkkaExtensions | ActorSystem configuration, clustering, persistence |
| DomainModel | Domain Logic | Business rules executed within actors |
| PersistenceModel | Persistence Store | Event sourcing and snapshot storage |
Comparison: Akka.NET vs Orleans¶
Key Differences¶
| Feature | Akka.NET | Orleans |
|---|---|---|
| Actor Model | Hierarchical actors with explicit lifecycle | Virtual actors with automatic lifecycle |
| Lifecycle Management | Manual creation, supervision, termination | Automatic activation/deactivation |
| State Persistence | Event sourcing with snapshots (manual) | Built-in state storage (automatic) |
| Clustering | Akka Cluster with manual sharding | Automatic grain placement |
| Supervision | Explicit supervision hierarchies | Automatic reactivation |
| Message Routing | Advanced routing (routers, sharding) | Simple grain invocation |
| Fault Tolerance | Supervision strategies | Automatic failover |
| Complexity | Higher (more control) | Lower (more abstraction) |
| Learning Curve | Steeper | Gentler |
| Use Cases | Complex routing, event sourcing, streaming | Virtual actors, stateful services |
When to Choose Akka.NET¶
Choose Akka.NET when: - You need hierarchical actor supervision - You require event sourcing patterns - You need complex message routing (routers, sharding) - You want fine-grained control over actor lifecycle - You're building streaming/reactive pipelines - You need Akka Streams for data processing
Choose Orleans when: - You want virtual actors with automatic lifecycle - You prefer simpler state management - You need built-in clustering and failover - You want less boilerplate code - You're building traditional stateful services
Core Concepts¶
Actor System¶
The ActorSystem is the root of the actor hierarchy and manages actor creation, supervision, and configuration:
// AkkaExtensions.cs
public static class AkkaExtensions
{
public static IServiceCollection AddMicroserviceAkka(
this IServiceCollection services,
IConfiguration configuration)
{
var akkaConfig = configuration.GetSection("Akka");
// Create ActorSystem
var actorSystem = ActorSystem.Create(
"MicroserviceActorSystem",
LoadAkkaConfiguration(akkaConfig));
services.AddSingleton(actorSystem);
// Register actor props
services.AddScoped<BankAccountActorProps>();
return services;
}
private static Config LoadAkkaConfiguration(IConfigurationSection akkaConfig)
{
var configBuilder = new StringBuilder();
configBuilder.AppendLine("akka {");
configBuilder.AppendLine(" actor {");
configBuilder.AppendLine(" provider = cluster");
configBuilder.AppendLine(" }");
configBuilder.AppendLine(" cluster {");
configBuilder.AppendLine($" seed-nodes = [{akkaConfig["SeedNodes"]}]");
configBuilder.AppendLine(" }");
configBuilder.AppendLine("}");
return ConfigurationFactory.ParseString(configBuilder.ToString());
}
}
Actor Interface (Framework-Agnostic)¶
Actors are defined with framework-agnostic interfaces in the ActorModel project:
// IBankAccountActor.cs (ActorModel project)
namespace ConnectSoft.MicroserviceTemplate.ActorModel
{
using System.Threading.Tasks;
/// <summary>
/// Bank account actor interface.
/// </summary>
public interface IBankAccountActor
{
/// <summary>
/// Withdraw the given amount from the bank account.
/// </summary>
/// <param name="input">Withdraw input.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task<WithdrawOutput> Withdraw(WithdrawInput input);
/// <summary>
/// Deposit the given amount to the bank account.
/// </summary>
/// <param name="input">Deposit input.</param>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task<DepositOutput> Deposit(DepositInput input);
/// <summary>
/// Get the current balance.
/// </summary>
/// <returns>A <see cref="Task"/> representing the asynchronous operation.</returns>
Task<GetBalanceOutput> GetBalance();
}
}
Akka.NET Actor Implementation¶
Akka.NET actors inherit from ReceiveActor and implement message handlers:
// BankAccountActor.cs (ActorModel.Akka project)
namespace ConnectSoft.MicroserviceTemplate.ActorModel.Akka
{
using System;
using Akka.Actor;
using ConnectSoft.MicroserviceTemplate.ActorModel;
using ConnectSoft.MicroserviceTemplate.DomainModel;
using Microsoft.Extensions.Logging;
/// <summary>
/// Bank account actor implementation using Akka.NET.
/// </summary>
public sealed class BankAccountActor : ReceiveActor, IBankAccountActor
{
private readonly IActorRef retrieverActor;
private readonly IActorRef processorActor;
private readonly ILogger<BankAccountActor> logger;
private decimal balance;
public BankAccountActor(
IActorRef retrieverActor,
IActorRef processorActor,
ILogger<BankAccountActor> logger)
{
this.retrieverActor = retrieverActor;
this.processorActor = processorActor;
this.logger = logger;
// Register message handlers
Receive<WithdrawInput>(HandleWithdraw);
Receive<DepositInput>(HandleDeposit);
Receive<GetBalanceRequest>(HandleGetBalance);
// Initialize state
this.balance = 0;
}
private void HandleWithdraw(WithdrawInput input)
{
try
{
if (this.balance < input.Amount)
{
Sender.Tell(new WithdrawOutput
{
Success = false,
ErrorMessage = "Insufficient funds"
});
return;
}
this.balance -= input.Amount;
this.logger.LogInformation(
"Withdrawn {Amount} from account. New balance: {Balance}",
input.Amount,
this.balance);
Sender.Tell(new WithdrawOutput { Success = true });
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error withdrawing from account");
Sender.Tell(new WithdrawOutput
{
Success = false,
ErrorMessage = ex.Message
});
}
}
private void HandleDeposit(DepositInput input)
{
try
{
this.balance += input.Amount;
this.logger.LogInformation(
"Deposited {Amount} to account. New balance: {Balance}",
input.Amount,
this.balance);
Sender.Tell(new DepositOutput { Success = true });
}
catch (Exception ex)
{
this.logger.LogError(ex, "Error depositing to account");
Sender.Tell(new DepositOutput
{
Success = false,
ErrorMessage = ex.Message
});
}
}
private void HandleGetBalance(GetBalanceRequest request)
{
Sender.Tell(new GetBalanceOutput { Balance = this.balance });
}
protected override void PreStart()
{
this.logger.LogInformation("BankAccountActor started");
base.PreStart();
}
protected override void PostStop()
{
this.logger.LogInformation("BankAccountActor stopped");
base.PostStop();
}
}
}
Actor Factory/Props¶
Akka.NET uses Props to create actors:
// BankAccountActorProps.cs
namespace ConnectSoft.MicroserviceTemplate.ActorModel.Akka
{
using Akka.Actor;
using Microsoft.Extensions.Logging;
/// <summary>
/// Factory for creating BankAccountActor instances.
/// </summary>
public sealed class BankAccountActorProps
{
private readonly IActorRefFactory actorSystem;
private readonly ILoggerFactory loggerFactory;
public BankAccountActorProps(
IActorRefFactory actorSystem,
ILoggerFactory loggerFactory)
{
this.actorSystem = actorSystem;
this.loggerFactory = loggerFactory;
}
public IActorRef Create(string accountId)
{
var props = Props.Create(() =>
{
var retrieverActor = this.actorSystem.ActorOf(
Props.Create<RetrieverActor>(),
$"retriever-{accountId}");
var processorActor = this.actorSystem.ActorOf(
Props.Create<ProcessorActor>(),
$"processor-{accountId}");
return new BankAccountActor(
retrieverActor,
processorActor,
this.loggerFactory.CreateLogger<BankAccountActor>());
});
return this.actorSystem.ActorOf(props, $"bank-account-{accountId}");
}
}
}
Actor Reference Wrapper¶
Wrap IActorRef to implement the framework-agnostic interface:
// BankAccountActorRef.cs
namespace ConnectSoft.MicroserviceTemplate.ActorModel.Akka
{
using System;
using System.Threading.Tasks;
using Akka.Actor;
using ConnectSoft.MicroserviceTemplate.ActorModel;
/// <summary>
/// Akka.NET implementation of IBankAccountActor.
/// </summary>
public sealed class BankAccountActorRef : IBankAccountActor
{
private readonly IActorRef actorRef;
private readonly TimeSpan timeout;
public BankAccountActorRef(IActorRef actorRef, TimeSpan timeout)
{
this.actorRef = actorRef;
this.timeout = timeout;
}
public async Task<WithdrawOutput> Withdraw(WithdrawInput input)
{
return await this.actorRef.Ask<WithdrawOutput>(input, this.timeout);
}
public async Task<DepositOutput> Deposit(DepositInput input)
{
return await this.actorRef.Ask<DepositOutput>(input, this.timeout);
}
public async Task<GetBalanceOutput> GetBalance()
{
return await this.actorRef.Ask<GetBalanceOutput>(
new GetBalanceRequest(),
this.timeout);
}
}
}
Actor Lifecycle¶
Lifecycle Hooks¶
Akka.NET actors have explicit lifecycle hooks:
public class MyActor : ReceiveActor
{
protected override void PreStart()
{
// Called before actor starts processing messages
// Initialize resources, connect to external services
}
protected override void PreRestart(Exception reason, object message)
{
// Called before actor restarts after failure
// Clean up resources
}
protected override void PostRestart(Exception reason)
{
// Called after actor restarts
// Reinitialize resources
}
protected override void PostStop()
{
// Called after actor stops
// Clean up resources, disconnect from services
}
}
Supervision¶
Akka.NET uses supervision hierarchies for fault tolerance:
// Supervisor actor
public class SupervisorActor : ReceiveActor
{
private IActorRef childActor;
public SupervisorActor()
{
// Create child actor with supervision strategy
this.childActor = Context.ActorOf(
Props.Create<BankAccountActor>(),
"bank-account");
// Define supervision strategy
Context.SupervisorStrategy = new OneForOneStrategy(
maxNrOfRetries: 10,
withinTimeRange: TimeSpan.FromMinutes(1),
localOnlyDecider: ex =>
{
switch (ex)
{
case ArgumentException _:
return Directive.Stop; // Stop on invalid arguments
case InvalidOperationException _:
return Directive.Restart; // Restart on operation errors
default:
return Directive.Escalate; // Escalate to parent
}
});
}
}
Actor Persistence (Event Sourcing)¶
Persistent Actor¶
Akka.NET supports event sourcing for stateful actors:
// PersistentBankAccountActor.cs
namespace ConnectSoft.MicroserviceTemplate.ActorModel.Akka
{
using Akka.Actor;
using Akka.Persistence;
/// <summary>
/// Persistent bank account actor using event sourcing.
/// </summary>
public sealed class PersistentBankAccountActor : ReceivePersistentActor
{
public override string PersistenceId { get; }
private decimal balance;
public PersistentBankAccountActor(string accountId)
{
this.PersistenceId = $"bank-account-{accountId}";
// Command handlers
Command<WithdrawInput>(cmd => HandleWithdraw(cmd));
Command<DepositInput>(cmd => HandleDeposit(cmd));
// Event handlers
Recover<DepositedEvent>(evt => HandleDeposited(evt));
Recover<WithdrawnEvent>(evt => HandleWithdrawn(evt));
Recover<SnapshotOffer>(offer => HandleSnapshot(offer));
}
private void HandleWithdraw(WithdrawInput input)
{
if (this.balance < input.Amount)
{
Sender.Tell(new WithdrawOutput
{
Success = false,
ErrorMessage = "Insufficient funds"
});
return;
}
var evt = new WithdrawnEvent(input.Amount, DateTime.UtcNow);
Persist(evt, persisted =>
{
HandleWithdrawn(persisted);
Sender.Tell(new WithdrawOutput { Success = true });
});
}
private void HandleDeposit(DepositInput input)
{
var evt = new DepositedEvent(input.Amount, DateTime.UtcNow);
Persist(evt, persisted =>
{
HandleDeposited(persisted);
Sender.Tell(new DepositOutput { Success = true });
});
}
private void HandleDeposited(DepositedEvent evt)
{
this.balance += evt.Amount;
}
private void HandleWithdrawn(WithdrawnEvent evt)
{
this.balance -= evt.Amount;
}
private void HandleSnapshot(SnapshotOffer offer)
{
if (offer.Snapshot is BankAccountSnapshot snapshot)
{
this.balance = snapshot.Balance;
}
}
}
}
Persistence Configuration¶
Configure persistence plugins:
// AkkaExtensions.cs
private static Config LoadAkkaConfiguration(IConfigurationSection akkaConfig)
{
var configBuilder = new StringBuilder();
configBuilder.AppendLine("akka {");
configBuilder.AppendLine(" persistence {");
configBuilder.AppendLine(" journal {");
configBuilder.AppendLine(" plugin = \"akka.persistence.journal.sql-server\"");
configBuilder.AppendLine(" sql-server {");
configBuilder.AppendLine($" connection-string = \"{akkaConfig["ConnectionString"]}\"");
configBuilder.AppendLine(" }");
configBuilder.AppendLine(" }");
configBuilder.AppendLine(" snapshot-store {");
configBuilder.AppendLine(" plugin = \"akka.persistence.snapshot-store.sql-server\"");
configBuilder.AppendLine(" sql-server {");
configBuilder.AppendLine($" connection-string = \"{akkaConfig["ConnectionString"]}\"");
configBuilder.AppendLine(" }");
configBuilder.AppendLine(" }");
configBuilder.AppendLine(" }");
configBuilder.AppendLine("}");
return ConfigurationFactory.ParseString(configBuilder.ToString());
}
Clustering and Sharding¶
Cluster Configuration¶
Configure Akka Cluster for distributed actors:
// AkkaExtensions.cs
private static Config LoadAkkaConfiguration(IConfigurationSection akkaConfig)
{
var seedNodes = akkaConfig.GetSection("SeedNodes")
.GetChildren()
.Select(node => $"\"akka.tcp://MicroserviceActorSystem@{node.Value}\"")
.ToList();
var configBuilder = new StringBuilder();
configBuilder.AppendLine("akka {");
configBuilder.AppendLine(" actor {");
configBuilder.AppendLine(" provider = cluster");
configBuilder.AppendLine(" }");
configBuilder.AppendLine(" remote {");
configBuilder.AppendLine(" dot-netty.tcp {");
configBuilder.AppendLine($" hostname = {akkaConfig["Hostname"]}");
configBuilder.AppendLine($" port = {akkaConfig["Port"]}");
configBuilder.AppendLine(" }");
configBuilder.AppendLine(" }");
configBuilder.AppendLine(" cluster {");
configBuilder.AppendLine($" seed-nodes = [{string.Join(", ", seedNodes)}]");
configBuilder.AppendLine(" }");
configBuilder.AppendLine("}");
return ConfigurationFactory.ParseString(configBuilder.ToString());
}
Cluster Sharding¶
Use cluster sharding for automatic actor distribution:
// ShardedBankAccountActor.cs
public class ShardedBankAccountActor : PersistentBankAccountActor
{
public ShardedBankAccountActor() : base(EntityId)
{
}
private static string EntityId =>
Context.Self.Path.Name;
protected override void PreStart()
{
base.PreStart();
Context.Become(Active);
}
private void Active()
{
Command<ShardRegion.StartEntity>(start =>
Sender.Tell(start.EntityId));
Command<WithdrawInput>(cmd => HandleWithdraw(cmd));
Command<DepositInput>(cmd => HandleDeposit(cmd));
}
}
// Sharding configuration
var sharding = ClusterSharding.Get(actorSystem);
var shardRegion = await sharding.StartAsync(
typeName: "BankAccount",
entityProps: Props.Create<ShardedBankAccountActor>(),
settings: ClusterShardingSettings.Create(actorSystem),
messageExtractor: new BankAccountMessageExtractor());
Routing¶
Router Actors¶
Use routers for load balancing and message distribution:
// Round-robin router
var router = actorSystem.ActorOf(
Props.Create<BankAccountActor>()
.WithRouter(new RoundRobinPool(5)));
// Consistent hashing router
var hashRouter = actorSystem.ActorOf(
Props.Create<BankAccountActor>()
.WithRouter(new ConsistentHashingPool(5)));
// Broadcast router
var broadcastRouter = actorSystem.ActorOf(
Props.Create<BankAccountActor>()
.WithRouter(new BroadcastPool(5)));
Custom Router¶
Create custom routing logic:
public class AccountIdRouter : CustomRouterConfig
{
public override Router CreateRouter(ActorSystem system)
{
return new Router(new AccountIdRoutingLogic());
}
private class AccountIdRoutingLogic : RoutingLogic
{
public override Routee Select(object message, Routee[] routees)
{
if (message is IHasAccountId hasAccountId)
{
var index = hasAccountId.AccountId.GetHashCode() % routees.Length;
return routees[Math.Abs(index)];
}
return routees[0];
}
}
}
Integration with Clean Architecture¶
Domain Model Integration¶
Actors call domain services:
public class BankAccountActor : ReceiveActor
{
private readonly IMicroserviceAggregateRootsRetriever retriever;
private readonly IMicroserviceAggregateRootsProcessor processor;
public BankAccountActor(
IMicroserviceAggregateRootsRetriever retriever,
IMicroserviceAggregateRootsProcessor processor)
{
this.retriever = retriever;
this.processor = processor;
Receive<WithdrawInput>(async input =>
{
// Use domain services
var result = await this.processor.ProcessAsync(
new ProcessInput { /* ... */ });
Sender.Tell(new WithdrawOutput { Success = result.Success });
});
}
}
Service Registration¶
Register actors in dependency injection:
// AkkaExtensions.cs
public static IServiceCollection AddMicroserviceAkka(
this IServiceCollection services,
IConfiguration configuration)
{
// Create ActorSystem
var actorSystem = ActorSystem.Create(
"MicroserviceActorSystem",
LoadAkkaConfiguration(configuration));
services.AddSingleton(actorSystem);
// Register actor factories
services.AddScoped<BankAccountActorProps>();
// Register actor reference factory
services.AddScoped<Func<string, IBankAccountActor>>(sp =>
{
var props = sp.GetRequiredService<BankAccountActorProps>();
var actorSystem = sp.GetRequiredService<ActorSystem>();
var timeout = TimeSpan.FromSeconds(30);
return accountId =>
{
var actorRef = props.Create(accountId);
return new BankAccountActorRef(actorRef, timeout);
};
});
return services;
}
Configuration¶
appsettings.json¶
{
"Akka": {
"Hostname": "localhost",
"Port": 8080,
"SeedNodes": [
"localhost:8080",
"localhost:8081"
],
"ConnectionString": "Server=localhost;Database=AkkaPersistence;...",
"Persistence": {
"Journal": {
"Plugin": "akka.persistence.journal.sql-server"
},
"SnapshotStore": {
"Plugin": "akka.persistence.snapshot-store.sql-server"
}
}
}
}
Testing¶
TestKit¶
Use Akka.TestKit for testing:
// BankAccountActorTests.cs
[TestClass]
public class BankAccountActorTests : TestKit
{
[TestMethod]
public void Withdraw_WithSufficientBalance_ShouldSucceed()
{
// Arrange
var actor = Sys.ActorOf(Props.Create<BankAccountActor>());
// Act
actor.Tell(new DepositInput { Amount = 100 });
var depositResult = ExpectMsg<DepositOutput>();
actor.Tell(new WithdrawInput { Amount = 50 });
var withdrawResult = ExpectMsg<WithdrawOutput>();
// Assert
Assert.IsTrue(depositResult.Success);
Assert.IsTrue(withdrawResult.Success);
}
}
Best Practices¶
Do's¶
-
Use Supervision Hierarchies
-
Handle Actor Lifecycle
-
Use Persistent Actors for State
-
Use Routers for Load Distribution
Don'ts¶
-
Don't Block in Message Handlers
-
Don't Share Mutable State
-
Don't Create Too Many Top-Level Actors
Troubleshooting¶
Issue: Actor Not Responding¶
Symptoms: Actor doesn't respond to messages.
Solutions:
- Check actor is started (PreStart called)
- Verify actor reference is correct
- Check supervision strategy isn't stopping actor
- Review logs for exceptions
Issue: Messages Lost¶
Symptoms: Messages not received by actor.
Solutions:
- Use persistent actors for guaranteed delivery
- Check actor lifecycle (stopped/restarted)
- Verify message type matches Receive handler
- Check dead letter queue
Issue: Cluster Not Forming¶
Symptoms: Actors can't communicate across nodes.
Solutions:
- Verify seed nodes are reachable
- Check network configuration
- Verify cluster configuration in appsettings.json
- Check firewall rules
Summary¶
Akka.NET in the ConnectSoft Microservice Template provides:
- ✅ Hierarchical Actors: Parent-child relationships with supervision
- ✅ Explicit Lifecycle: Fine-grained control over actor creation and termination
- ✅ Event Sourcing: Persistent actors with event journaling
- ✅ Advanced Routing: Complex message routing patterns
- ✅ Cluster Sharding: Automatic actor distribution across nodes
- ✅ Fault Tolerance: Supervision strategies for error recovery
- ✅ Stream Processing: Akka Streams for reactive pipelines
- ✅ Clean Architecture: Framework-agnostic interfaces with Akka.NET implementation
Akka.NET is ideal for teams requiring: - More control over actor lifecycle - Event sourcing patterns - Complex routing and supervision hierarchies - Fine-grained fault tolerance strategies
While Akka.NET provides more flexibility than Orleans, it requires more configuration and understanding of actor lifecycle management. Choose Akka.NET when you need hierarchical supervision, event sourcing, or complex routing patterns that aren't available in Orleans.