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:
protectedoverrideasyncTaskExecuteAsync(CancellationTokenstoppingToken){while(!stoppingToken.IsCancellationRequested){try{varresults=await_repository.QueryAsync();foreach(varresultinresults){await_processor.ProcessAsync(result);}}catch(Exceptionex){_logger.LogError(ex,"Worker failed.");}awaitTask.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. |
publicclassMyWorker:IWorker<EmptyTriggerContext>{privatereadonlyILogger<MyWorker>_logger;publicMyWorker(ILogger<MyWorker>logger)=>_logger=logger;publicTaskExecuteAsync(EmptyTriggerContextcontext,CancellationTokencancellationToken){_logger.LogInformation("Running at {timestamp}",context.OccurredAt);returnTask.CompletedTask;}}// Run once on startupbuilder.Services.AddRunOnceWorker<MyWorker>();// Run on a fixed delaybuilder.Services.AddDelayWorker<MyWorker>(TimeSpan.FromSeconds(30));// Run on a cron schedulebuilder.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