Skip to content
Tracon

IJobHandler

Namespace Tracon · Assembly Tracon.Abstractions.dll

The extension point that provides job execution logic for one handler key.

public interface IJobHandler

Registered with AddJobHandler<T>(handlerKey): the key lives on the registration, not on the type, so the same type can serve two keys and a duplicate key is caught at startup. The background worker looks the key up by exact, ordinal match, so registration order never decides which handler runs — a consumer handler cannot shadow a built-in one, and a built-in one cannot shadow a consumer’s.

Lifetime: the handler is registered scoped and resolved from a fresh dependency-injection scope per execution. Scoped dependencies are therefore safe to take in the constructor: two jobs running in parallel, and two attempts of the same job, never share an instance.

Tenant: runs under the ambient tenant — the worker opens the job’s own tenant scope before calling IJobHandler.ExecuteAsync, so an ITenantContext the handler resolves reports JobRecord.TenantId, not the process default. The record is also on JobContext.Job for a handler that prefers to read it explicitly.

builder.Services.AddJobHandler<NightlyReportJobHandler>("contoso.nightly-report");

ExecuteAsync(JobContext, CancellationToken)

Section titled “ ExecuteAsync(JobContext, CancellationToken)”

Executes the job.

ValueTask ExecuteAsync(JobContext context, CancellationToken cancellationToken = default)

context JobContext

The job context: record, items, reporting, and cancellation check.

cancellationToken CancellationToken

The cancellation token (triggered when the worker shuts down).

ValueTask

The completion task.

Execution is at-least-once, not exactly-once. The same job — the same JobContext.Job, with the SAME unfiltered JobContext.Items list — can reach IJobHandler.ExecuteAsync more than once: if the handler throws, the job is retried with IJobStore.ReleaseForRetryAsync (if the attempt limit is not exceeded) or marked as JobStatus.Failed; if the process crashes or the lease simply expires before the handler returns, another worker (or the same one) re-leases the SAME job and calls IJobHandler.ExecuteAsync again from scratch. Neither case resets item progress.

Because of this, a handler with side effects (sending an email, charging a payment, calling an external API) MUST be idempotent, or MUST check JobItemRecord.Status itself and skip any item that is not JobItemStatus.Pending — see JobContext.Items. The handlers Tracon ships all do the latter, and the reusable JobHandlerContract in Tracon.Testing.Contracts.Xunit asserts it.