From Akka.NET
Akka.NET is the .NET port of Akka — runs on .NET / .NET Framework, with the same actor model, clustering, and persistence story.
For migration, Akka.NET ≈ Akka (JVM) — the from-akka-jvm guide covers the conceptual mapping. This page focuses on C# specifics and what changes in TypeScript.
Concept mapping
Section titled “Concept mapping”// Akka.NET (C#):public class Counter : ReceiveActor { private int _count; public Counter() { Receive<string>(cmd => { if (cmd == "inc") _count++; else if (cmd == "get") Sender.Tell(_count); }); }}
var system = ActorSystem.Create("MySystem");var counter = system.ActorOf<Counter>("counter");counter.Tell("inc");// actor-ts:import { Actor, ActorSystem, Props, type ActorRef } from 'actor-ts';
type Cmd = { kind: 'inc' } | { kind: 'get'; replyTo: ActorRef<number> };
class Counter extends Actor<Cmd> { private count = 0; override onReceive(cmd: Cmd): void { if (cmd.kind === 'inc') this.count++; else if (cmd.kind === 'get') cmd.replyTo.tell(this.count); }}
const system = ActorSystem.create('MySystem');const counter = system.actorOf(Props.create(() => new Counter()), 'counter');counter.tell({ kind: 'inc' });Differences from Akka.NET:
- No
ReceiveActorbase +Receive<T>registrations — useonReceive(msg)with a switch / pattern-match. - Message types are explicit —
Cmdunion vs Akka.NET’s often-untypedReceive<object>. - Lambda-style behavior definition isn’t TypeScript-friendly
— class with
onReceiveis the idiom.
Sender / reply
Section titled “Sender / reply”// Akka.NET:Receive<GetState>(_ => Sender.Tell(_state));// actor-ts — option 1: replyTo in the messagetype Cmd = { kind: 'get-state'; replyTo: ActorRef<State> };
class MyActor extends Actor<Cmd> { onReceive(cmd: Cmd): void { if (cmd.kind === 'get-state') cmd.replyTo.tell(this.state); }}
// actor-ts — option 2: this.sender (Option type)class MyActor extends Actor<Cmd> { onReceive(cmd: Cmd): void { if (cmd.kind === 'get-state') { this.sender.forEach(s => s.tell(this.state)); } }}Prefer option 1 (explicit replyTo) — type-safe and clear
about the contract.
Supervisor strategy
Section titled “Supervisor strategy”// Akka.NET:protected override SupervisorStrategy SupervisorStrategy() => new OneForOneStrategy(maxNrOfRetries: 5, withinTimeRange: TimeSpan.FromMinutes(1), decider: ex => ex switch { ArithmeticException _ => Directive.Resume, _ => Directive.Restart, });// actor-ts:override supervisorStrategy = new OneForOneStrategy( decideBy([ { match: TypeError, then: Directive.Resume }, ], Directive.Restart), { maxRetries: 5, withinTimeRangeMs: 60_000 },);Same shape — decider returns directives based on exception type.
Cluster
Section titled “Cluster”// Akka.NET:var cluster = Cluster.Get(system);cluster.Join(Address.Parse("akka.tcp://MySystem@host:port"));// actor-ts:const cluster = await Cluster.join(system, { host: 'host', port: 2552, seeds: ['host:2552', ...],});Same join semantics; actor-ts uses URL-like seeds vs Akka.NET’s explicit Address.
Sharding
Section titled “Sharding”// Akka.NET:var region = ClusterSharding.Get(system).Start( typeName: "Counter", entityProps: Props.Create<CounterActor>(), settings: ClusterShardingSettings.Create(system), messageExtractor: new MessageExtractor());// actor-ts:const region = ClusterSharding.get(system, cluster).start<Cmd>({ typeName: 'Counter', entityProps: Props.create(() => new CounterActor()), extractEntityId: (cmd) => cmd.entityId, numShards: 100,});Drop-in conceptually. MessageExtractor becomes
extractEntityId; shard ID is auto-derived.
Persistence
Section titled “Persistence”// Akka.NET:public class Account : ReceivePersistentActor { public override string PersistenceId => "account-" + Id; private decimal _balance;
public Account() { Recover<Deposited>(e => _balance += e.Amount);
Command<Deposit>(c => Persist(new Deposited(c.Amount), e => _balance += e.Amount)); }}// actor-ts:class Account extends PersistentActor<Cmd, Event, State> { constructor(public readonly id: string) { super(); } readonly persistenceId = `account-${this.id}`;
initialState(): State { return { balance: 0 }; }
onEvent(state: State, event: Event): State { if (event.kind === 'deposited') return { balance: state.balance + event.amount }; return state; }
onCommand(state: State, cmd: Cmd): void { if (cmd.kind === 'deposit') { this.persist({ kind: 'deposited', amount: cmd.amount }, () => {}); } }}Single onEvent instead of split Recover<T> / Command<T>.
Configuration
Section titled “Configuration”Akka.NET uses HOCON for configuration — same as Akka JVM. actor-ts also uses HOCON (Configuration).
The key prefixes differ:
# Akka.NET:akka { cluster { seed-nodes = [...] failure-detector { ... } }}
# actor-ts:actor-ts { cluster { gossip-interval = 1s failure-detector { ... } }}Different root key (akka vs actor-ts), different specific
keys, but the HOCON format and conventions are the same.
What you give up
Section titled “What you give up”- C# language features — LINQ, async/await with structured concurrency, etc. TypeScript has async/await but the ecosystem is different.
- .NET ecosystem — Akka.NET integrates with .NET’s auth, ORM, etc. ecosystems. actor-ts uses Node / Bun’s.
- Some Akka.NET-specific features — DotNetty transport, Hyperion serialization, etc. don’t have direct equivalents.
What you gain
Section titled “What you gain”- TypeScript types — message types checked at the boundary.
- Faster startup — Bun starts in sub-100ms vs .NET’s ~500ms-1s.
- Container-native — smaller images, less memory baseline.
Migration strategies
Section titled “Migration strategies”Same as Akka JVM:
- Don’t rewrite everything at once. Run actor-ts in a new service that fronts the Akka.NET system.
- One bounded context at a time.
- Migrate persistence carefully — Akka.NET’s JSON.NET serialized events can be read by actor-ts with an EventAdapter.
Where to next
Section titled “Where to next”- from-akka-jvm — the main reference (Akka.NET mirrors JVM Akka).
- Migration overview — cross-framework comparison.
- Quickstart — actor-ts hello world.