Skip to content

Spindle.Hosting Setup

BL19 edited this page Aug 25, 2026 · 1 revision

Spindle.Hosting setup

This page describes the initial setup of Spindle in a .NET application. It covers both kinds of service commonly used with Spindle:

  • A worker service that advances durable flow instances in the background.
  • A non-worker service, such as an API or message-ingress service, that starts flows while a separate worker advances them.

EF Core provider setup and migration management are covered in Spindle EF Core persistence.

Packages

Install Spindle.Hosting and one persistence implementation. For an EF Core application, install the provider package for the database as described in the persistence page.

dotnet add package Spindle.Hosting
dotnet add package Spindle.Persistence.EFCore.PostgreSQL

Spindle targets .NET 8 and .NET 10 in this repository. Keep the Spindle package version and the EF Core provider major version aligned with the target framework used by the application.

Configuration

The application should provide a connection string and, for PostgreSQL or SQL Server, may provide a schema:

{
  "ConnectionStrings": {
    "Spindle": "Host=localhost;Database=spindle;Username=spindle;Password=change-me"
  },
  "Spindle": {
    "Schema": "spindle"
  }
}

Do not commit production credentials. Use the application's normal secret or environment configuration. The environment-variable form of the schema setting is Spindle__Schema.

SQLite and MySQL do not support the schema registration parameter. Leave Spindle:Schema unset when using those providers.

Register a worker service

A worker service registers the persistence provider, flow definitions, optional step handlers, and AddSpindleWorker. AddSpindleWorker includes the runtime and starts the hosted background pump.

using Spindle.Abstractions.Core;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Spindle.Hosting;
using Spindle.Persistence.EFCore.PostgreSQL;

var builder = Host.CreateApplicationBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("Spindle")
    ?? throw new InvalidOperationException("The Spindle connection string is required.");
var schema = builder.Configuration["Spindle:Schema"];

builder.Services.AddSpindlePostgreSql(
    connectionString,
    schema: schema);

builder.Services.AddSpindleFlow<MyFlow, MyRequest, MyResult>(
    new FlowName("my-flow"));

builder.Services.AddSpindleStepHandler<
    MyStepHandler,
    MyStepRequest,
    MyStepResult>(
    new StepHandlerId("my-step"));

builder.Services.AddSpindleWorker(options =>
{
    options.WorkerId = "orders-worker";
    options.PollInterval = TimeSpan.FromMilliseconds(250);
    options.MaxConcurrentFlowInstances = 4;
});

var app = builder.Build();
await app.RunAsync();

The worker reads runnable flow instances from the configured store, executes ready local steps, fires due timers, and records progress. Multiple worker processes can use the same store; configure migrations separately so that only one controlled process applies database changes at a time.

Worker options

AddSpindleWorker accepts an optional Action<SpindleHostOptions>:

Option Default Purpose
PollInterval 250ms Delay when a worker tick makes no progress.
MaxFlowInstancesPerTick 100 Maximum runnable instances read per tick.
MaxStepsPerFlowPerTick 1 Maximum ready local steps run for one flow per tick.
MaxConcurrentFlowInstances CPU count Maximum flow instances advanced concurrently.
LeaseDuration 30s Duration of local step leases.
WorkerId Machine-based Worker identity recorded on attempts and leases.

Register a non-worker service

A non-worker service registers AddSpindleRuntime instead of AddSpindleWorker. This is useful for an API, job producer, or message-ingress process that starts flows but should not run the background worker loop.

using Spindle.Abstractions.Core;
using Spindle.Abstractions.Flows;
using Microsoft.Extensions.DependencyInjection;
using Spindle.Hosting;
using Spindle.Persistence.EFCore.PostgreSQL;

var builder = WebApplication.CreateBuilder(args);

var connectionString = builder.Configuration.GetConnectionString("Spindle")
    ?? throw new InvalidOperationException("The Spindle connection string is required.");

builder.Services.AddSpindlePostgreSql(
    connectionString,
    schema: builder.Configuration["Spindle:Schema"]);

builder.Services.AddSpindleFlow<MyFlow, MyRequest, MyResult>(
    new FlowName("my-flow"));

builder.Services.AddSpindleRuntime();

var app = builder.Build();

app.MapPost("/flows", async (
    StartFlowRequest request,
    ISpindleRuntime runtime,
    CancellationToken cancellationToken) =>
{
    await runtime.StartAsync<MyRequest, MyResult>(
        new FlowName("my-flow"),
        new MyRequest(request.Value),
        new StartFlowOptions { IdempotencyKey = request.IdempotencyKey },
        cancellationToken);

    return Results.Accepted();
});

await app.RunAsync();

AddSpindleRuntime registers ISpindleRuntime and the flow/handler registries, but it does not start SpindleWorkerHostedService. Durable work that needs background advancement requires a worker service connected to the same persistence store.

Do not register both AddSpindleRuntime and AddSpindleWorker; AddSpindleWorker already registers the runtime.

Flows and step handlers

Register every flow that the process may start or execute:

builder.Services.AddSpindleFlow<MyFlow, MyRequest, MyResult>(
    new FlowName("my-flow"));

Register handlers used by ctx.StepHandler(...):

builder.Services.AddSpindleStepHandler<
    MyStepHandler,
    MyStepRequest,
    MyStepResult>(
    new StepHandlerId("my-step"));

Flow names and step-handler IDs are part of the persisted flow contract. Keep them stable when deploying a new application version.

Database initialization

Choose one of the migration workflows described in Spindle EF Core persistence. In development and test environments, applying migrations during startup can be convenient. In production, prefer a separately controlled migration job or reviewed SQL script.

Clone this wiki locally