Skip to content

Cadenza Worker

Jung Hyun, Nam edited this page May 28, 2026 · 2 revisions

Cadenza.Worker

Background services and daemons in a single file. Run hosts a BackgroundService on the .NET Generic Host and respects Ctrl+C and SIGTERM out of the box.

#:sdk Cadenza.Worker@1.0.15
Base SDK Microsoft.NET.Sdk.Worker
Output Long-running console host
AOT-clean Yes
Default deploy Self-contained binary, or container (Deployment Container)

Minimal example

#!/usr/bin/env dotnet run
#:sdk Cadenza.Worker@1.0.15

await Run(async (ct) =>
{
    while (!ct.IsCancellationRequested)
    {
        Log.Info($"Heartbeat at {DateTime.UtcNow:O}");
        await Task.Delay(TimeSpan.FromSeconds(30), ct);
    }
});

How it works:

  • Run builds the Generic Host and registers your lambda as a BackgroundService.
  • The CancellationToken passed to the lambda fires on graceful shutdown (SIGTERM on Linux/macOS, Ctrl+C everywhere). Honor it for prompt shutdown.
  • Log.Info / Log.Warn / Log.Error / Log.Debug route through ILogger, so any structured-logging provider Just Works.

Tier 1 surface

await Run(async (CancellationToken ct) => {});
await Run(async (IServiceProvider sp, CancellationToken ct) => {});   // DI overload

Worker.Config<T>(key);     // typed configuration — call inside Run
Worker.Service<T>();       // resolve a DI service — call inside Run

Log.Info / Log.Warn / Log.Error / Log.Debug

Plus the shared modules (Fs, Sh, Http, Env, Json, Prompt) — see Tier 1 API.

Configuration

Worker.Config<T>(key) reads from the host's IConfiguration — env vars, command-line arguments, appsettings.json (if you drop one next to the script).

Important: call Config<T> and Service<T> inside the Run callback. They need the host to be built; calling them at the top of the script before Run will throw Worker host has not been started.

#:sdk Cadenza.Worker@1.0.15

await Run(async (ct) =>
{
    var endpoint = Worker.Config<string>("Api:Endpoint");
    var interval = Worker.Config<int>("Api:IntervalSeconds");

    Log.Info($"Polling {endpoint} every {interval}s");

    while (!ct.IsCancellationRequested)
    {
        try
        {
            var probe = await Http.GetJson<Probe>($"{endpoint}/health", Ctx.Default, ct);
            Log.Info($"status: {probe.status}");
        }
        catch (OperationCanceledException) { throw; }
        catch (Exception ex)
        {
            Log.Warn($"probe failed: {ex.Message}");
        }
        await Task.Delay(TimeSpan.FromSeconds(interval), ct);
    }
});

record Probe(string status);

[System.Text.Json.Serialization.JsonSerializable(typeof(Probe))]
partial class Ctx : System.Text.Json.Serialization.JsonSerializerContext { }

Config sources (precedence, highest first)

  1. Command-line arguments — dotnet run app.cs -- Api:Endpoint=https://…
  2. Environment variables — Api__Endpoint=https://… (double underscore for nested keys)
  3. appsettings.json next to the script (if present)
  4. Defaults baked into the script

Resolving DI services

await Run(async (sp, ct) =>
{
    var db = sp.GetRequiredService<MyDbClient>();
    // …
});

Or with the no-arg overload:

await Run(async (ct) =>
{
    var db = Worker.Service<MyDbClient>();
    // …
});

To register services up front, use the DI overload of Run — or, if you need to add services before the host starts, drop down to the Generic Host directly (Microsoft.Extensions.Hosting.Host.CreateApplicationBuilder()).

Logging

Log.Info("started");
Log.Debug("verbose detail");
Log.Warn("retrying after transient error");
Log.Error("fatal", exception);

Log.* routes through ILogger<Program>. Levels honor Logging:LogLevel:Default from configuration. JSON logs, OpenTelemetry, Seq, Application Insights — all the usual providers attach via standard IServiceCollection extensions on the underlying host. For provider setup, drop down to the Generic Host directly.

Graceful shutdown

On Ctrl+C or SIGTERM:

  1. The host signals the cancellation token.
  2. Your lambda should observe ct.IsCancellationRequested (and pass ct to Task.Delay, Http.GetJson, etc.).
  3. Once the lambda returns, the host exits.

A misbehaving lambda that ignores ct will block shutdown for up to the host's stop timeout (5 seconds by default).

When to use it

  • Polling jobs, queue consumers, schedulers
  • systemd / Windows Service / container daemons
  • Side processes that need lifecycle + DI + config but no HTTP endpoint

When not to use it

Samples

  • worker-heartbeat.cs — minimal Run(async ct => …) loop
  • worker-polling.cs — config + HTTP + level-correct logging

See Samples for the full annotated list.

Shipping

# Single binary
dotnet publish app.cs -c Release --self-contained -r linux-x64

# Container (no Dockerfile)
dotnet publish app.cs --os linux --arch x64 /t:PublishContainer

See Deployment Single Binary and Deployment Container.

See also

Clone this wiki locally