Jobs and handlers
Define a job contract with a stable name, write the handler that runs it, and register both.
On this page
A job is a small data record that describes work to do. A handler is the class that does the work. Runwick stores the job, and a worker later decodes it and calls the handler.
The job contract
A job contract is a concrete class or record that implements IQueueJob. IQueueJob is an empty marker interface. Payloads are data only: they carry no behaviour, and attributes on them cannot resolve services or do I/O.
Every contract needs a stable name. The name is what Runwick stores, so you can rename, move or re-namespace the class without breaking queued jobs. The CLR type name is never stored.
You can use both. Identity settings ([JobName] and Named, [PayloadVersion] and AtVersion) must agree if both are present, or startup fails with identity_conflict. For queue, priority and MaxExecutions the fluent value wins over the attribute. The full order is in Configuration.
Attributes are not inherited. A derived class never picks up its base class's name.
Hangfire in the same file
Hangfire also has a QueueAttribute. If a file imports both, alias Runwick's: using JobQueue = Runwick.QueueAttribute; and write [JobQueue("emails")].
Naming rules
Names are case-sensitive and never trimmed. A dotted style such as email.welcome or billing.invoice.send reads well in the dashboard. Two contracts with the same name fail startup with duplicate_job_name.
The handler
A handler implements IJobHandler<TJob> with one method, HandleAsync(TJob job, CancellationToken cancellationToken).
- Returning normally means success.
- Throwing an exception means a retryable failure, unless it is a
PermanentJobFailureException. See Retries and timeouts. - The token is cancelled on a user cancel, a timeout, a lost lease or shutdown. Pass it to every awaited call. See Cancellation.
Handlers are registered as scoped services. Runwick creates a fresh DI scope for every attempt, so a scoped DbContext belongs to that one attempt and is disposed afterwards. Inject anything you need into the constructor, including IJobContext (see Job context).
Handlers can run more than once
Runwick is at-least-once. A crash after the work but before the result is saved runs the handler again. Make the effects safe to repeat. See Delivery guarantees.
There is one handler per job name per worker. Binding two handlers to one job fails with duplicate_handler.
Registration
Registration happens inside AddRunwick. Nothing is scanned automatically.
Each scan only looks at the one assembly you point it at, and runs in a fixed order.
Configuration is checked once, at startup. Runwick collects every error and throws one RunwickConfigurationException that lists them all, so a typo in a queue name stops the app before it does any work.
Payload serialization
Payloads are stored as UTF-8 JSON using Runwick's own fixed profile, runwick.json version 1. It does not use your ASP.NET JSON settings, so changing those can never change what is in the queue.
The profile is strict:
- Property names are camelCase. Unknown properties and duplicate properties are rejected when reading.
- Numbers must be real JSON numbers. Enums are written as their member names.
DateTimevalues must be UTC. PreferDateTimeOffsetfor instants.- Members typed
object,[Flags]enums,[JsonConverter]and attributes that loosen the rules are rejected at startup. UseJsonElementfor free-form data. - Polymorphism works only for closed hierarchies declared with
[JsonPolymorphic]and[JsonDerivedType].
You can supply source-generated metadata with runwick.AddJsonTypeInfoResolver(MyJsonContext.Default).
Payload limits
These limits are fixed. Dispatch fails before anything is written if a payload breaks them.
Keep payloads small. Store a file or a large document elsewhere and put its ID in the job.
Changing a payload's shape
The worker decodes the stored bytes fresh on every run. If you change the record, raise the version and register an upgrade from the old shape:
Each step goes up by exactly one version. Upgrades run on the worker, after the run is authorized, and must be pure functions. A worker only picks up jobs whose version it can decode, so during a rolling deploy old workers leave version 2 jobs alone. More in Upgrades.
Runwick.Contracts for shared job types
Runwick.Contracts holds IQueueJob, the handler interface, the attributes and the dispatch types. It has no dependencies.
Put your job records in a class library that references only Runwick.Contracts. Producers and workers both reference that library, and the library never pulls in database drivers or handler code.
The app that actually dispatches still needs Runwick and a store package, because IJobDispatcher is implemented there. A producer-only app simply leaves out AddWorker and does not need the handlers.
Next
- Dispatching: create jobs.
- Queues and priorities: route them.
- Retries and timeouts: what happens when they fail.