From Microsoft Orleans
Este conteúdo não está disponível em sua língua ainda.
Microsoft Orleans is a .NET grain-based actor framework. Grains are virtual — addressed by ID, auto-spawned on first message, auto-deactivated when idle. Sharded entities in actor-ts are the closest analog.
The grain ↔ sharded entity mapping
Section titled “The grain ↔ sharded entity mapping”Orleans: actor-ts: IGreeter grain interface → TypeScript message types (Command union) GreeterGrain : Grain → Actor extending PersistentActor (or plain Actor) IGrainKey → string entity ID client.GetGrain<IGreeter>(id)→ reference + tell via sharding regionBefore/after
Section titled “Before/after”Grain definition
Section titled “Grain definition”// Orleans (C#):public interface IUserGrain : IGrainWithStringKey { Task<UserProfile> GetProfile(); Task UpdateName(string name);}
public class UserGrain : Grain, IUserGrain { private UserProfile _profile = new();
public Task<UserProfile> GetProfile() => Task.FromResult(_profile); public Task UpdateName(string name) { _profile = _profile with { Name = name }; return Task.CompletedTask; }}// actor-ts:import { match } from 'ts-pattern';import { Actor, type ActorRef } from 'actor-ts';
type Command = | { entityId: string; kind: 'get-profile'; replyTo: ActorRef<UserProfile> } | { entityId: string; kind: 'update-name'; name: string };
class UserActor extends Actor<Command> { private profile: UserProfile = { name: '', email: '' };
override onReceive(command: Command): void { match(command) .with({ kind: 'get-profile' }, (c) => this.onGetProfile(c)) .with({ kind: 'update-name' }, (c) => this.onUpdateName(c)) .exhaustive(); }
private onGetProfile(command: GetProfileCommand): void { command.replyTo.tell(this.profile); }
private onUpdateName(command: UpdateNameCommand): void { this.profile = { ...this.profile, name: command.name }; }}Differences:
- Method calls become message kinds + handlers.
Task<T>returns become explicitreplyTorefs.IGrainWithStringKeyis implicit —extractEntityId(command)pulls the ID at the sharding layer.
Grain client
Section titled “Grain client”// Orleans:var grain = grainFactory.GetGrain<IUserGrain>("user-42");var profile = await grain.GetProfile();await grain.UpdateName("Alice");// actor-ts:import { ClusterSharding } from 'actor-ts';
const region = cluster.sharding.start<Command>({ typeName: 'user', entityActor: UserActor, extractEntityId: (command) => command.entityId,});
// Get profile via ask:const profile = await region.ask({ entityId: 'user-42', kind: 'get-profile', replyTo: undefined as any,}, 5_000);
// Update via tell:region.tell({ entityId: 'user-42', kind: 'update-name', name: 'Alice' });The region is a single ActorRef — messages routed by
extractEntityId. Each unique ID has one actor, spawned on its first
message. Unlike an Orleans grain it does not deactivate on its own:
there is no default idle window, so an entity stays resident until
something passivates it (see below).
Activation / deactivation
Section titled “Activation / deactivation”// Orleans (lifecycle methods):public override Task OnActivateAsync(CancellationToken token) { // Load state from persistence return base.OnActivateAsync(token);}
public override Task OnDeactivateAsync(DeactivationReason reason, CancellationToken token) { // Cleanup return base.OnDeactivateAsync(reason, token);}// actor-ts:class UserActor extends Actor<Command> { override preStart(): void { // Equivalent of OnActivateAsync } override postStop(): void { // Equivalent of OnDeactivateAsync }}The framework’s preStart + postStop map directly.
For automatic passivation on idle:
sharding.start({ // ... passivationIdleMs: 30_000, // passivate after 30s idle — like Orleans's default});Persistence
Section titled “Persistence”// Orleans Event Sourcing:[LogConsistencyProvider(ProviderName = "EventStore")]public class AccountGrain : JournaledGrain<AccountState, AccountEvent>, IAccount { public Task Deposit(decimal amount) { RaiseEvent(new Deposited(amount)); return ConfirmEvents(); }}// actor-ts:class Account extends PersistentActor<Command, Event, State> { readonly persistenceId = `account-${this.entityId}`;
initialState(): State { return { balance: 0 }; }
onEvent(state: State, event: Event): State { return match(event) .with({ kind: 'deposited' }, (e) => ({ balance: state.balance + e.amount })) .exhaustive(); }
onCommand(state: State, command: Command): void { match(command) .with({ kind: 'deposit' }, (c) => this.onDeposit(c)) .exhaustive(); }
private onDeposit(command: DepositCommand): void { this.persist({ kind: 'deposited', amount: command.amount }, () => {}); }}Very similar pattern. Orleans uses RaiseEvent + ConfirmEvents; actor-ts uses persist with callback.
Conceptual differences
Section titled “Conceptual differences”| Orleans | actor-ts |
|---|---|
| Virtual actors — always-existing, spawned on demand. | Sharded entities are similar; need to be started as a sharded type. |
| Directory-based placement — silos look up grain locations. | Hash-mod-region placement by default; coordinator manages. |
| Streams — push-based data flow. | DistributedPubSub for similar fan-out semantics. |
| Reminders — durable timers. | context.timers + persistence — manually combine. |
| Transactions across grains — built-in. | NOT built-in — use sagas or two-phase commit patterns. |
Reminders
Section titled “Reminders”// Orleans:await this.RegisterOrUpdateReminder("daily-check", TimeSpan.FromDays(1), TimeSpan.FromDays(1));
public Task ReceiveReminder(string reminderName, TickStatus status) { // handle}// actor-ts — no direct equivalent, build with persistence + timers:class MyActor extends PersistentActor<Command, Event, State> { override preStart(): void { super.preStart(); this.context.timers.startTimerWithFixedDelay( 'daily-check', { kind: 'daily-check' }, 24 * 60 * 60_000, ); }}Timers in actor-ts don’t survive restart — for true
“durable reminders,” you’d persist the schedule + re-arm in
onRecoveryComplete.
Streams
Section titled “Streams”// Orleans Streams:var stream = streamProvider.GetStream<Order>(streamId, "orders");await stream.OnNextAsync(order);await stream.SubscribeAsync((order, _) => ...);// actor-ts equivalent — DistributedPubSub:const ps = system.extension(DistributedPubSubId);ps.start(cluster);
ps.mediator.tell(new Publish('orders', order));ps.mediator.tell(new Subscribe('orders', subscriberRef));Cluster-wide pub/sub. No grain involvement; topic-based fan-out.
Cross-grain transactions
Section titled “Cross-grain transactions”// Orleans Transactions (.NET 8+):[Transaction(TransactionOption.Required)]public async Task Transfer(...) { await fromAccount.Withdraw(amount); await toAccount.Deposit(amount);}actor-ts doesn’t have built-in transactions across actors. Build with sagas:
class TransferSaga extends PersistentFSM<...> { // Step 1: withdraw from source. // Step 2: deposit to destination. // On failure: compensate (refund source).}See PersistentFSM for the saga pattern.
Cluster setup
Section titled “Cluster setup”// Orleans (with K8s):var host = Host.CreateDefaultBuilder() .UseOrleans(siloBuilder => siloBuilder .UseKubernetesHosting() .ConfigureEndpoints(siloPort: 11111, gatewayPort: 30000) ) .Build();// actor-ts:import { KubernetesApiSeedProvider, KubernetesApiSeedProviderOptions } from 'actor-ts';
const kubernetesApiSeedProviderOptions = KubernetesApiSeedProviderOptions.create() .withNamespace(process.env.K8S_NAMESPACE!) .withServiceName('my-app') .withSystemName('my-system') .withPort(2552);const nodes = await new KubernetesApiSeedProvider( kubernetesApiSeedProviderOptions,).lookup();const seeds = nodes.map(String); // 'system@host:port'
const clusterOptions = ClusterOptions.create() .withHost(process.env.POD_IP!) .withPort(2552) .withSeeds(seeds);const cluster = await Cluster.join(system, clusterOptions);Different API shape, same idea. K8s pod discovery → cluster join.
What you give up
Section titled “What you give up”- Method-call semantics — Orleans grain calls are typed method invocations; actor-ts has message types + handlers. More verbose; more explicit.
- Distributed transactions across grains — not built-in.
- Reminders as a first-class primitive — combine timers + persistence.
- .NET ecosystem — switching to TS / JS runtime.
What you gain
Section titled “What you gain”- TypeScript for end-to-end typing.
- Bun’s fast startup vs .NET’s warm-up.
- Open-source ecosystem for runtime + persistence backends.
Where to next
Section titled “Where to next”- Quickstart — actor-ts hello world.
- Sharding overview — the closest Orleans-grain analog.
- PersistentActor — for event-sourced grains.
- PersistentFSM — saga pattern for cross-grain workflows.
