Skip to content

Background services and startup tasks

Use standard .NET hosted services for application initialization and background work. Orleans participates in the same Generic Host, so registration order can ensure Orleans is ready before a hosted service starts.

Register Orleans first, then the BackgroundService:

public static async Task RunBackgroundService(string[] args)
{
var builder = Host.CreateApplicationBuilder(args);
builder.UseOrleans(siloBuilder =>
{
// Configure Orleans.
});
builder.Services.AddHostedService<GrainPingService>();
await builder.Build().RunAsync();
}
public sealed class GrainPingService : BackgroundService
{
private readonly IGrainFactory _grainFactory;
private readonly ILogger<GrainPingService> _logger;
public GrainPingService(
IGrainFactory grainFactory,
ILogger<GrainPingService> logger)
{
_grainFactory = grainFactory;
_logger = logger;
}
protected override async Task ExecuteAsync(
CancellationToken stoppingToken)
{
using var timer = new PeriodicTimer(TimeSpan.FromSeconds(30));
var grain = _grainFactory.GetGrain<IHealthGrain>("background-check");
while (await timer.WaitForNextTickAsync(stoppingToken))
{
try
{
await grain.Ping();
}
catch (Exception exception)
when (exception is not OperationCanceledException)
{
_logger.LogError(exception, "Grain ping failed");
}
}
}
}

Honor stoppingToken so the service doesn’t delay silo shutdown. Use IHostedService directly for one-time startup and shutdown work.

Use AddStartupTask when initialization must complete at an Orleans lifecycle stage before startup can continue:

public static void RegisterStartupTask(ISiloBuilder siloBuilder)
{
siloBuilder.AddStartupTask(
async (services, cancellationToken) =>
{
var grainFactory =
services.GetRequiredService<IGrainFactory>();
var grain =
grainFactory.GetGrain<IInitializerGrain>("application");
await grain.Initialize(cancellationToken);
},
ServiceLifecycleStage.Active);
}

The default stage is Active. An exception fails silo startup. This is appropriate for mandatory validation or initialization, but not for optional work that can retry after the host becomes ready.

For reusable tasks, implement IStartupTask:

public sealed class ValidateDependenciesTask : IStartupTask
{
private readonly IDependencyValidator _validator;
public ValidateDependenciesTask(IDependencyValidator validator)
{
_validator = validator;
}
public Task Execute(CancellationToken cancellationToken) =>
_validator.ValidateAsync(cancellationToken);
}
public static void RegisterValidateDependenciesTask(
ISiloBuilder siloBuilder)
{
siloBuilder.AddStartupTask<ValidateDependenciesTask>(
ServiceLifecycleStage.ApplicationServices);
}
RequirementMechanism
Continuous loop or scheduled application workBackgroundService
One-time host startup and shutdown workIHostedService
Mandatory initialization at a specific Orleans stageAddStartupTask
Start and stop callbacks integrated with an Orleans subsystemILifecycleParticipant<T> with ISiloLifecycle

Don’t use a startup task for long-running loops, database migrations that multiple replicas could race to apply, or work that needs unbounded retries. Coordinate migrations externally or make them safely single-writer.

See Orleans silo lifecycle for lifecycle stage selection.