Important
Heads up — WorkR is still in development. Expect breaking API changes before v1.0.
WorkR is a lightweight, extensible .NET library for building composable background worker pipelines on top of BackgroundService. It replaces deeply nested loops and ad-hoc polling logic with a clean, testable, and DI-friendly abstraction.
Building background workers in .NET typically results in boilerplate-heavy BackgroundService implementations with nested loops, scattered error handling, and logic that is difficult to test in isolation:
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
{
while (!stoppingToken.IsCancellationRequested)
{
try
{
var results = await _repository.QueryAsync();
foreach (var result in results)
{
await _processor.ProcessAsync(result);
}
}
catch (Exception ex)
{
_logger.LogError(ex, "Worker failed.");
}
await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
}
}WorkR solves this by separating concerns into discrete, composable pieces — triggers, workers, and middleware — each with a single responsibility.
| Concept | Role |
|---|---|
| Trigger | Owns the execution loop. Fires the worker pipeline on a timer, queue message, or any signal. |
| Worker | Receives a value from the trigger (or a previous worker) and performs work. |
| Middleware | Wraps worker execution with cross-cutting concerns (error handling, timeouts, scoping). |
| TriggerContext | The typed payload passed from trigger to worker chain, carrying metadata like ExecutionId and OccurredAt. |
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;
}
}
// Run once on startup
builder.Services.AddRunOnceWorker<MyWorker>();
// Run on a fixed delay
builder.Services.AddDelayWorker<MyWorker>(TimeSpan.FromSeconds(30));
// Run on a cron schedule
builder.Services.AddScheduledWorker<MyWorker>("*/5 * * * *");WorkR emits one OpenTelemetry-compatible span per worker pipeline execution from an
ActivitySource named "WorkR", regardless of trigger type. Enable it by adding the
source to your OpenTelemetry TracerProviderBuilder:
builder.Services.AddOpenTelemetry()
.WithTracing(tracing => tracing.AddSource("WorkR"));Spans are named EXECUTE <pipeline>, where <pipeline> is the worker chain joined
with -> (for example EXECUTE ValidateOrder -> ShipOrder), and carry the
following tags:
| Tag | Description |
|---|---|
workr.version |
Version of the WorkR assembly |
workr.service.id |
Stable identifier for the worker service instance |
workr.trigger |
Trigger type name |
workr.trigger.version |
Version of the trigger's assembly |
workr.pipeline |
Worker chain, joined with -> |
workr.execution.id |
Identifier for this individual execution |
When a span is already active (e.g. a messaging SDK's process span), the WorkR span becomes its child; otherwise it starts a new trace. Failed executions are marked with an error status and the exception is recorded. When no listener is subscribed, tracing has no overhead.
The source name "WorkR" is a stable public contract.
| Package | Description |
|---|---|
WorkR.Abstractions |
Core interfaces: ITrigger<T>, IWorker<T>, IWorkerMiddleware, TriggerContext. Reference this from libraries that define reusable workers, triggers, or middleware. |
WorkR |
Core implementation: pipeline builder, built-in middleware, AddWorker, AddRunOnceWorker. |
WorkR.Triggers.Timers |
Delay and cron-scheduled triggers: AddDelayWorker, AddScheduledWorker. |
WorkR.Triggers.AzureStorageQueues |
Azure Storage Queue trigger: AddStorageQueueWorker. |
WorkR.Triggers.AzureServiceBus |
Azure Service Bus trigger: AddServiceBusWorker. |
MIT