WorkR 0.7.4

dotnet add package WorkR --version 0.7.4
                    
NuGet\Install-Package WorkR -Version 0.7.4
                    
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="WorkR" Version="0.7.4" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="WorkR" Version="0.7.4" />
                    
Directory.Packages.props
<PackageReference Include="WorkR" />
                    
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 WorkR --version 0.7.4
                    
#r "nuget: WorkR, 0.7.4"
                    
#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 WorkR@0.7.4
                    
#: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=WorkR&version=0.7.4
                    
Install as a Cake Addin
#tool nuget:?package=WorkR&version=0.7.4
                    
Install as a Cake Tool

WorkR

.NET NuGet License: MIT

Heads up — WorkR is still in development. Expect breaking API changes before v1.0.

A lightweight .NET background worker framework built on top of IHostedService. Define workers and triggers, compose middleware, and let WorkR manage the execution loop.


Installation

dotnet add package WorkR

Getting Started

Define a Worker

public class MyWorker : IWorker<EmptyTriggerContext>
{
    private readonly ILogger<MyWorker> _logger;

    public MyWorker(ILogger<MyWorker> logger) => _logger = logger;

    public Task ExecuteAsync(EmptyTriggerContext context, CancellationToken cancellationToken)
    {
        _logger.LogInformation("Running at {timestamp}", context.OccurredAt);
        return Task.CompletedTask;
    }
}

Register a Worker

// Fire exactly once when the host starts
builder.Services.AddRunOnceWorker<MyWorker>();

// Full control over the pipeline
builder.Services.AddRunOnceWorker(pipeline =>
    pipeline.AddWorker<MyWorker>());

AddRunOnceWorker

Fires the pipeline exactly once when the host starts. Useful for startup tasks, migrations, or one-off initialisation.

builder.Services.AddRunOnceWorker<MyStartupWorker>();

Each pipeline execution runs inside its own dependency injection scope, created automatically. Scoped services resolved during the execution share that scope and are disposed when the execution completes.


Chained Workers

Workers can be chained using IWorker<TIn, TOut>. Each worker transforms the value and calls next to pass it to the next step.

public class FetchWorker : IWorker<EmptyTriggerContext, IReadOnlyList<Order>>
{
    private readonly IOrderRepository _repo;

    public FetchWorker(IOrderRepository repo) => _repo = repo;

    public async Task ExecuteAsync(
        EmptyTriggerContext context,
        Worker<IReadOnlyList<Order>> next,
        CancellationToken cancellationToken)
    {
        var orders = await _repo.GetPendingAsync(cancellationToken);
        await next(orders, cancellationToken);
    }
}

public class ProcessWorker : IWorker<IReadOnlyList<Order>>
{
    public async Task ExecuteAsync(IReadOnlyList<Order> orders, CancellationToken cancellationToken)
    {
        foreach (var order in orders)
        {
            // process each order
        }
    }
}

Register the chain using AddWorker:

builder.Services.AddRunOnceWorker(pipeline =>
    pipeline
        .AddWorker<FetchWorker, IReadOnlyList<Order>>()
        .AddWorker<ProcessWorker>());

Custom Triggers

Implement ITrigger<TContext> to define a trigger with any execution model, then register it with AddWorker:

public class MyTrigger : ITrigger<EmptyTriggerContext>
{
    public async Task ExecuteAsync(IWorkerPipeline<EmptyTriggerContext> workerPipeline, CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            await workerPipeline.ExecuteAsync(new EmptyTriggerContext(DateTimeOffset.UtcNow), stoppingToken);
            await Task.Delay(TimeSpan.FromSeconds(10), stoppingToken);
        }
    }
}

builder.Services.AddWorker<MyTrigger, EmptyTriggerContext>(
    sp => ActivatorUtilities.CreateInstance<MyTrigger>(sp),
    pipeline => pipeline.AddWorker<MyWorker>());

Worker Lifetime

There are two ways to add a worker to a pipeline.

By type (DI-registered)

The worker type is registered with the DI container automatically and resolved per execution. The default lifetime is Transient:

// Override lifetime
pipeline.AddWorker<MyWorker>(ServiceLifetime.Scoped)

// Skip registration (if already registered elsewhere)
pipeline.AddWorker<MyWorker>(lifetime: null)

By factory (not DI-registered)

Pass a factory to control construction yourself. The worker is not registered with the DI container, and the factory is invoked once per execution — giving you a fresh instance each run by default:

// A new instance is built for every execution
pipeline.AddWorker(sp => new MyWorker(sp.GetRequiredService<IDependency>()))

Because the factory is keyed to this position in the pipeline rather than to the worker type, the same worker type can be added to multiple pipelines (or multiple times in one pipeline) with independent construction. If you want a single instance shared across executions, capture it in the closure and return it:

var shared = new MyWorker();
pipeline.AddWorker(_ => shared) // same instance every execution — must be thread-safe

The factory owns the instance's lifetime — WorkR does not dispose workers it did not create. If your worker is IDisposable/IAsyncDisposable, either resolve it from sp so the container disposes it, or manage disposal yourself.


Middleware

Middleware is configured per worker step using WorkerMiddlewarePipelineBuilder. It wraps worker execution and is applied in registration order (outermost first).

Note: Every pipeline execution already runs inside its own dependency injection scope, created automatically. You do not need UseScope to obtain a scope — scoped services resolved during an execution share that scope and are disposed when the execution completes.

UseScope

Creates a new IServiceScope per execution. Workers downstream of UseScope resolve their dependencies from the scoped container. This is rarely needed now that each execution already runs in its own scope, but remains available for nesting an additional scope.

middleware.UseScope()

UseErrorHandling

Catches and swallows exceptions to prevent a failing execution from crashing the pipeline. An optional predicate filters which exceptions are handled.

middleware.UseErrorHandling<Exception>()
middleware.UseErrorHandling<HttpRequestException>()
middleware.UseErrorHandling<HttpRequestException>(ex => ex.StatusCode == HttpStatusCode.ServiceUnavailable)

UseTimeout

Cancels execution if it exceeds the specified duration.

middleware.UseTimeout(TimeSpan.FromSeconds(30))

Custom Middleware

Implement IWorkerMiddleware to create reusable cross-cutting behaviour:

public class TracingMiddleware : IWorkerMiddleware
{
    public async Task ExecuteAsync(Func<CancellationToken, Task> next, CancellationToken cancellationToken)
    {
        using var activity = Activity.StartActivity("worker.execute");
        await next(cancellationToken);
    }
}

Register it with UseMiddleware:

// Resolved via factory (access to DI)
middleware.UseMiddleware(sp => new TracingMiddleware(sp.GetRequiredService<ITracer>()))

// Pre-constructed instance
middleware.UseMiddleware(new TracingMiddleware())

Middleware Ordering

A typical ordering:

middleware
    .UseErrorHandling<Exception>()         // catch any exceptions from this or subsequent workers
    .UseTimeout(TimeSpan.FromSeconds(30))  // cancel if too slow

Middleware can be configured per step in a chain and effects all subsequent steps:

builder.Services.AddRunOnceWorker(pipeline =>
    pipeline
        .AddWorker<FetchWorker, IReadOnlyList<Order>>(middleware: mw => mw.UseTimeout(TimeSpan.FromSeconds(10)))
        .AddWorker<ProcessWorker>());
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 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 is compatible.  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 (3)

Showing the top 3 NuGet packages that depend on WorkR:

Package Downloads
WorkR.Triggers.Timers

Timer-based triggers for WorkR, supporting delay-based and cron-scheduled background workers.

WorkR.Triggers.AzureServiceBus

Azure Service Bus trigger for WorkR.

WorkR.Triggers.AzureStorageQueues

Azure Storage Queue trigger for WorkR.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.7.4 263 6/18/2026
0.5.8 2,068 6/16/2026
0.5.6 178 6/14/2026