Grain activation and lifecycle
Orleans activates grains on demand and deactivates idle activations to reclaim resources. Activation is an implementation detail of a grain’s stable logical identity: callers continue using the same grain reference across activation changes.
Activation
Section titled “Activation”Orleans creates grain classes through dependency injection, establishes their grain context, loads configured persistent state, and then calls OnActivateAsync.
Override the cancellation-token overload:
public sealed class DeviceGrain( IDeviceConnectionFactory connectionFactory) : Grain, IDeviceGrain{ private IDeviceConnection? _connection;
public override async Task OnActivateAsync( CancellationToken cancellationToken) { _connection = await connectionFactory.ConnectAsync( this.GetPrimaryKeyString(), cancellationToken);
await base.OnActivateAsync(cancellationToken); }}OnActivateAsync accepts a CancellationToken; there is no parameterless overload. If activation fails, Orleans doesn’t make that activation available for calls.
Avoid doing unnecessary work during activation. Activations can be recreated after collection, migration, silo restart, or failure.
Deactivation
Section titled “Deactivation”Orleans can deactivate an activation because it has been idle, the silo is stopping, the application requested deactivation, migration is occurring, or an error made the activation invalid.
public override async Task OnDeactivateAsync( DeactivationReason reason, CancellationToken cancellationToken){ if (_connection is not null) { await _connection.DisposeAsync(); }
await base.OnDeactivateAsync(reason, cancellationToken);}Deactivation is best effort. OnDeactivateAsync doesn’t run if the process terminates abruptly or in some failure cases. Persist important state as part of the operation that changes it, not only during deactivation.
Influence activation lifetime
Section titled “Influence activation lifetime”Call DeactivateOnIdle to ask Orleans to deactivate the grain after the current request and queued work complete:
public Task Close(){ DeactivateOnIdle(); return Task.CompletedTask;}Call DelayDeactivation to keep an otherwise idle activation eligible for a specified period. This is a hint, not a durability guarantee; failures and shutdown can still remove the activation.
Grain timers don’t keep an activation alive by default. Set GrainTimerCreationOptions.KeepAlive only when timer activity should extend the activation lifetime.
Lifecycle stages and participants
Section titled “Lifecycle stages and participants”The grain lifecycle exposes ordered stages:
| Stage | Purpose |
|---|---|
| GrainLifecycleStage.First | Earliest subscription point. |
| GrainLifecycleStage.SetupState | State setup and loading. |
| GrainLifecycleStage.Activate | Grain activation and deactivation callbacks. |
| GrainLifecycleStage.Last | Latest subscription point. |
Components that need ordered activation-scoped behavior can implement ILifecycleParticipant<T> for IGrainLifecycle and subscribe through ObservableLifecycle. Use distinct stages when one component’s startup depends on another completing. Callbacks within a stage can execute concurrently.
public sealed class CacheParticipant : ILifecycleParticipant<IGrainLifecycle>{ public void Participate(IGrainLifecycle lifecycle) { lifecycle.Subscribe<CacheParticipant>( GrainLifecycleStage.SetupState, OnStart, OnStop); }
private Task OnStart(CancellationToken cancellationToken) => Task.CompletedTask;
private Task OnStop(CancellationToken cancellationToken) => Task.CompletedTask;}The runtime calls Participate on a grain object which implements the participant interface. Services use an explicit enrollment owner, such as a facet factory or the shared activation setup described below.
Shared activation setup
Section titled “Shared activation setup”Use IConfigureGrainTypeComponents to select features for a grain implementation class and register reusable setup actions with AddActivationSetup. This example selects classes implementing an application-owned ICachedGrain marker:
public interface ICachedGrain : IGrain;
public sealed class CacheSetupConfigurator(GrainClassMap grainClasses) : IConfigureGrainTypeComponents{ public void Configure( GrainType grainType, GrainProperties properties, GrainTypeSharedContext shared) { if (grainClasses.TryGetGrainClass(grainType, out Type? grainClass) && typeof(ICachedGrain).IsAssignableFrom(grainClass)) { shared.AddActivationSetup(static context => { CacheParticipant cache = context.ActivationServices .GetRequiredService<CacheParticipant>(); cache.Participate(context.ObservableLifecycle); }); } }}Register the configurator as a singleton and the feature state as scoped:
siloBuilder.ConfigureServices(services =>{ services.AddScoped<CacheParticipant>(); services.AddSingleton<IConfigureGrainTypeComponents, CacheSetupConfigurator>();});Orleans caches the selected setup actions in the shared grain type context. Each activation runs those actions in registration order after its grain constructor completes and GrainInstance is assigned. All setup actions finish before the runtime calls the grain object’s Participate method and starts lifecycle callbacks. Each stateless worker activation runs the same shared setup with its own context.
The shared action resolves CacheParticipant only for selected grains. Resolution uses the activation scope, so constructor injection of CacheParticipant and setup share the same scoped service. For interface injection, register an alias factory which resolves that concrete service. Ordinary concrete, keyed, and participant-interface DI registrations retain their explicit enrollment behavior.
Setup actions can run concurrently for different activations. Keep shared actions stateless, or make captured shared data safe for concurrent access; keep activation-specific state in the activation scope. Add actions during shared type configuration. Use synchronous setup to enroll services and lifecycle callbacks for asynchronous initialization and shutdown. Assign one enrollment owner to each feature so that subscriptions are established once.
A setup exception fails the activation, skips remaining setup actions and lifecycle startup, and triggers grain and activation-scope disposal. A fresh activation resolves fresh scoped state and runs the cached setup again.
For the runtime lifecycle model shared by silos and grain activations, see Orleans runtime lifecycle.
Grain migration
Section titled “Grain migration”Migration moves an activation to another silo while preserving migration-participating in-memory state. Call MigrateOnIdle to request migration after the activation finishes its current work:
public Task RequestMigration(){ MigrateOnIdle(); return Task.CompletedTask;}The request is advisory. Migration occurs only if placement selects another compatible silo. Orleans carries the current RequestContext into the placement decision.
Implement IGrainMigrationParticipant for custom activation state that must survive migration:
public sealed class SessionGrain : Grain, ISessionGrain, IGrainMigrationParticipant{ private int _sequence;
public void OnDehydrate(IDehydrationContext context) { context.TryAddValue("sequence", _sequence); }
public void OnRehydrate(IRehydrationContext context) { context.TryGetValue("sequence", out _sequence); }}Persistent-state components supplied by Orleans participate automatically. Migration isn’t a replacement for durable storage: migrated state is still lost if the source process fails before transfer completes.
Automatic activation repartitioning and rebalancing use migration to improve locality or cluster balance. Both are experimental. See Grain placement for their status and configuration.
Use ImmovableAttribute to exclude a grain type from automatic migration. It doesn’t block an explicit MigrateOnIdle request.
For the runtime protocols behind activation, collection, deactivation, and migration, see Activation lifecycle and migration. Application code should continue to rely on the public lifecycle APIs described here rather than runtime internals.
