Dloizides.BackgroundWork 1.0.0

dotnet add package Dloizides.BackgroundWork --version 1.0.0
                    
NuGet\Install-Package Dloizides.BackgroundWork -Version 1.0.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Dloizides.BackgroundWork" Version="1.0.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Dloizides.BackgroundWork" Version="1.0.0" />
                    
Directory.Packages.props
<PackageReference Include="Dloizides.BackgroundWork" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Dloizides.BackgroundWork --version 1.0.0
                    
#r "nuget: Dloizides.BackgroundWork, 1.0.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Dloizides.BackgroundWork@1.0.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Dloizides.BackgroundWork&version=1.0.0
                    
Install as a Cake Addin
#tool nuget:?package=Dloizides.BackgroundWork&version=1.0.0
                    
Install as a Cake Tool

Dloizides.BackgroundWork

Run singleton background work safely alongside a horizontally-scaled HTTP tier.

A typical .NET service does two jobs in one process: it answers HTTP requests, and it runs scheduled sweeps. Only the first is safe to duplicate. Set replicas: 2 for availability and the sweeps run twice — sending duplicate email, deleting the same rows twice, expiring the same records twice.

This package separates those jobs and guards the second one.

// Program.cs
builder.Services.AddBackgroundWork(builder.Configuration);

// A sweep that must never run twice
builder.Services.AddLeaderElectedHostedService(
    "welcome-email", sp => sp.GetRequiredService<WelcomeEmailWorker>());

// Background work that is genuinely safe to run concurrently
builder.Services.AddRoleAwareHostedService(sp => sp.GetRequiredService<CacheWarmer>());
// appsettings.json — the default changes nothing
{
  "BackgroundWork": {
    "Role": "All",                 // Api | Worker | All  (default All)
    "KeyPrefix": "kefi",
    "ConnectionString": "Host=saas-db;Database=kefi;Username=kefi;Password=..."
  }
}

Deploy the same image twice, with BackgroundWork__Role=Api on the scalable HTTP tier and BackgroundWork__Role=Worker on a single-replica worker.

Why replicas: 1 is not enough on its own

A rolling update starts the new pod before terminating the old one, so a 1-replica Deployment runs two copies for ~20–30s on every deploy. Without a lease, every deploy is a window in which two copies of a delete sweep run concurrently. The lease makes correctness independent of the replica count.

Why Postgres advisory locks

The failure being designed against is an OOM SIGKILL. A session-level advisory lock is released by Postgres the moment the TCP session ends, so a killed pod frees its lease immediately — no TTL to tune, no clock-skew window. A Redis lease would linger for its full TTL after the same death, and tuning that TTL down trades stuck locks for false expiries under GC pause.

It also needs no schema, no migration and no writes — the lock is a session primitive, so adopting this package cannot touch application data.

Fails closed

Situation Behaviour
Lease held by another instance guarded work does not start
Database unreachable treated as not held — work does not start
Lease session dies while running work is stopped within CheckInterval
Role=Api inner service is never even constructed
Role=Worker / All with no connection string startup throws

The one detail that matters

The lock must be held on a dedicated, non-pooled connection. An advisory lock lives on a session: taken on an EF-pooled connection it is silently released the moment that connection returns to the pool — the code reads as correct, IsHeld still says true, and two workers run anyway. LeaseConnectionString.ForDedicatedSession forces Pooling=false, and it is asserted by test rather than left as a comment.

Lease ids are derived by SHA-256 from {KeyPrefix}.{name}. Advisory lock ids are global per database, so KeyPrefix is what stops two services sharing one Postgres from colliding. string.GetHashCode() is randomised per process and would let two replicas compute different ids for the same lease — both would acquire, both would run.

Observability

LeaseStateRegistry exposes, per worker: whether the lease is held, whether the guarded work is running, and when that last changed. A worker that silently stops sweeping is otherwise invisible — nothing errors, nothing restarts, the emails just stop.

API

Member Purpose
AddBackgroundWork(configuration, configure?) Binds options, registers shared infrastructure. Call once.
AddLeaderElectedHostedService(name, factory) Role-aware and lease-guarded. Use for anything that sends, deletes, expires, or advances a state machine.
AddRoleAwareHostedService(factory) Role-aware only, no lease. Concurrency-safe work only.
LeaseStateRegistry.Snapshot() Current held/running state of every guarded worker.
WorkerRole Api | Worker | All (default All).

Registration is opt-in per worker, never a blanket filter over every IHostedService — some hosted services (request-driven helpers, the MassTransit bus) must keep running in the Api role.

License

MIT

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
1.0.0 209 8/11/2026