Decode.Hosting 2.1.0

There is a newer version of this package available.
See the version list below for details.
dotnet add package Decode.Hosting --version 2.1.0
                    
NuGet\Install-Package Decode.Hosting -Version 2.1.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="Decode.Hosting" Version="2.1.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Decode.Hosting" Version="2.1.0" />
                    
Directory.Packages.props
<PackageReference Include="Decode.Hosting" />
                    
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 Decode.Hosting --version 2.1.0
                    
#r "nuget: Decode.Hosting, 2.1.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 Decode.Hosting@2.1.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=Decode.Hosting&version=2.1.0
                    
Install as a Cake Addin
#tool nuget:?package=Decode.Hosting&version=2.1.0
                    
Install as a Cake Tool

Decode.Hosting

Resilient periodic background workers for .NET, each cycle running inside its own dependency injection scope and its own trace span.

🚀 Features

  • Scope per cycle: every iteration gets a fresh IServiceScope, so scoped services — a DbSession, a unit of work, an HttpClient with scoped handlers — behave exactly as they do in a web request instead of being captured for the lifetime of the process.
  • Survives failures: an unhandled exception in a cycle is logged, backed off, and the worker continues. Opt out with SuppressExceptions = false.
  • Does not block startup: a RunOnStartup cycle runs after the host has finished starting, not during it.
  • Instrumented: one span per cycle on the Decode.Hosting activity source, tagged with the worker name and execution id.
  • Clear failure on misconfiguration: a non-positive Interval fails with a message naming the property, instead of throwing from inside Task.Delay.

📦 Installation

dotnet add package Decode.Hosting

🛠️ Setup

1. Write the worker

Derive from ScopedPeriodicWorker and override ExecuteScopedAsync. The constructor must accept IServiceScopeFactory, ILogger and WorkerOptions to pass to the base class — the registration supplies all three. Any additional constructor parameters are resolved from the container.

using Decode.Hosting;
using Decode.Hosting.Abstractions;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;

public class ExpireTokensWorker(
    IServiceScopeFactory scopeFactory,
    ILogger<ExpireTokensWorker> logger,
    WorkerOptions options)
    : ScopedPeriodicWorker(scopeFactory, logger, options)
{
    protected override async Task ExecuteScopedAsync(
        IServiceProvider serviceProvider,
        WorkerContext context)
    {
        // Resolve from the cycle's scope, never from a captured root provider.
        ITokenRepository repository = serviceProvider.GetRequiredService<ITokenRepository>();

        int removed = await repository.DeleteExpiredAsync(context.CancellationToken);

        logger.LogInformation(
            "Cycle {ExecutionId} removed {Count} expired tokens.",
            context.ExecutionId,
            removed);
    }
}

Resolve scoped dependencies from the serviceProvider argument, not from the constructor. The worker itself is registered as a singleton, so a scoped service taken through its constructor either throws InvalidOperationException — "Cannot resolve scoped service from root provider", which is what scope validation reports, on by default in Development — or, with validation off, silently resolves once and stays captured for the lifetime of the process. IServiceScopeFactory exists to avoid both.

2. Register it

using Decode.Hosting;

builder.Services.AddDecodeBackgroundWorker<ExpireTokensWorker>(options =>
{
    options.Interval = TimeSpan.FromMinutes(15);
    options.RunOnStartup = true;
    options.ErrorBackoffDelay = TimeSpan.FromSeconds(30);
    options.SuppressExceptions = true;
});

The using Decode.Hosting; matters. AddDecodeBackgroundWorker lives in the Decode.Hosting namespace rather than Microsoft.Extensions.DependencyInjection, so it will not appear on IServiceCollection without it.

Called without the delegate, the worker runs with the WorkerOptions defaults: every minute, starting immediately, surviving errors with a 5 second backoff.

⏱️ Timing

Situation Time until the next cycle starts
Normal cycle Interval + how long the cycle took
Failed cycle, SuppressExceptions = true ErrorBackoffDelay + Interval
Host shutting down no next cycle; the loop exits

The delay is between cycles, so cycle duration is additive — this is a periodic worker, not a fixed-rate scheduler. If you need a cycle to start at an exact wall-clock time, this is the wrong tool; use a scheduler such as Quartz or Hangfire.

RunOnStartup runs its cycle after the host has started. BackgroundService executes ExecuteAsync synchronously up to its first incomplete await and IHost.StartAsync awaits that, so without care a first cycle taking a minute would delay the whole application by a minute. The worker yields before doing anything for exactly this reason.

🔥 Failure behaviour

options.SuppressExceptions = true;   // default

An unhandled exception is logged at Error with the execution id, the cycle's span is marked Error with the exception message, the worker waits ErrorBackoffDelay and carries on. This is the right default for a worker whose job will probably succeed next time — a transient database timeout, a rate-limited API.

options.SuppressExceptions = false;

The exception is logged at Critical and rethrown. That faults the BackgroundService, and since .NET 6 the default BackgroundServiceExceptionBehavior is StopHost — the application shuts down. Choose this when a failing cycle means the process is no longer trustworthy and you would rather have the orchestrator restart it.

Two things are never treated as failures: an OperationCanceledException raised because the host is shutting down is logged at Information, and a cycle interrupted by shutdown does not trigger the error backoff.

A non-positive Interval throws InvalidOperationException naming WorkerOptions.Interval. Because the check runs on the worker's background task, it surfaces either from StartAsync or from the faulted execute task depending on scheduling — either way the host does not keep running with a worker that would spin.

📊 Telemetry

Each cycle opens a span named {WorkerTypeName}.Execute on the activity source Decode.Hosting:

Tag Value
worker.name the worker's type name
worker.execution_id the cycle's WorkerContext.ExecutionId

The span status is set to Error with the exception message when a cycle throws, whether or not the failure is suppressed.

Nothing is exported until the source is registered. With Decode.Telemetry:

builder.Services.AddOpenTelemetry()
    .WithTracing(tracing => tracing
        .AddDecodeInstrumentation()   // includes Decode.Hosting
        .AddOtlpExporter());

Or directly, without taking the dependency:

tracing.AddSource("Decode.Hosting");

This package emits no metrics.

⚠️ One worker type, one registration

AddDecodeBackgroundWorker<TWorker> registers through AddHostedService<TWorker>, which de-duplicates by service and implementation type. Calling it twice for the same TWorker keeps the first registration and silently discards the second:

// Does not do what it looks like — the second call is ignored.
builder.Services.AddDecodeBackgroundWorker<SyncWorker>(o => o.Interval = TimeSpan.FromMinutes(5));
builder.Services.AddDecodeBackgroundWorker<SyncWorker>(o => o.Interval = TimeSpan.FromHours(1));

To run the same logic on two schedules, declare two types — a shared base class with two thin subclasses is enough.

📖 Configuration from appsettings.json

AddDecodeBackgroundWorker takes options in code. To bind them from configuration, register the worker yourself and use the IOptions<WorkerOptions> constructor that ScopedPeriodicWorker also provides:

builder.Services.Configure<WorkerOptions>(builder.Configuration.GetSection("ExpireTokensWorker"));
builder.Services.AddHostedService<ExpireTokensWorker>();
public class ExpireTokensWorker(
    IServiceScopeFactory scopeFactory,
    ILogger<ExpireTokensWorker> logger,
    IOptions<WorkerOptions> options)
    : ScopedPeriodicWorker(scopeFactory, logger, options)
{
    // ...
}

Note that WorkerOptions is a single type, so one Configure<WorkerOptions> call covers every worker bound this way. Use named options if two workers need different values from configuration.

📄 License

MIT License.

Product Compatible and additional computed target framework versions.
.NET net5.0 was computed.  net5.0-windows was computed.  net6.0 was computed.  net6.0-android was computed.  net6.0-ios was computed.  net6.0-maccatalyst was computed.  net6.0-macos was computed.  net6.0-tvos was computed.  net6.0-windows was computed.  net7.0 was computed.  net7.0-android was computed.  net7.0-ios was computed.  net7.0-maccatalyst was computed.  net7.0-macos was computed.  net7.0-tvos was computed.  net7.0-windows was computed.  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 is compatible.  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. 
.NET Core netcoreapp3.0 was computed.  netcoreapp3.1 was computed. 
.NET Standard netstandard2.1 is compatible. 
MonoAndroid monoandroid was computed. 
MonoMac monomac was computed. 
MonoTouch monotouch was computed. 
Tizen tizen60 was computed. 
Xamarin.iOS xamarinios was computed. 
Xamarin.Mac xamarinmac was computed. 
Xamarin.TVOS xamarintvos was computed. 
Xamarin.WatchOS xamarinwatchos 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
3.0.0 80 9/14/2026
2.1.0 84 9/13/2026
2.0.2 83 9/13/2026
2.0.1 85 9/13/2026
2.0.0 180 7/28/2026
1.0.3 114 7/22/2026