Job Context
IJobContext gives your job access to runtime metadata about its own execution — the job ID, which attempt this is, the queue it came from, and the tags attached at enqueue time. You can also use it to report progress to the dashboard and persist checkpoints so long-running jobs can resume after a worker restart. Inject it through the constructor like any other scoped service.
Injecting IJobContext¶
NexJob registers IJobContext as a scoped service tied to each job execution. Declare it in your job's constructor and NexJob resolves it automatically.
public sealed class LongRunningJob : IJob
{
private readonly IJobContext _context;
public LongRunningJob(IJobContext context) => _context = context;
public async Task ExecuteAsync(CancellationToken ct)
{
var jobId = _context.JobId; // unique identifier for this execution
var attempt = _context.Attempt; // 1 on first run, 2 on first retry, etc.
var queue = _context.Queue; // the queue this job was fetched from
var tags = _context.Tags; // tags attached at enqueue time
// your work here
}
}
Available Properties¶
| Property | Type | Description |
|---|---|---|
JobId |
JobId |
Unique identifier for this job execution. Useful for logging and external correlation. |
Attempt |
int |
Current attempt number, 1-based. First execution is 1, first retry is 2. |
MaxAttempts |
int |
Maximum number of attempts configured for this job. |
Queue |
string |
The name of the queue this job was fetched from. |
RecurringJobId |
string? |
The recurring job definition ID, or null if the job was enqueued directly. |
Tags |
IReadOnlyList<string> |
Tags attached to this job at enqueue time. |
Reporting Progress¶
Call ReportProgressAsync to push a progress update to the storage layer. The dashboard reflects updates in real time via SSE.
public sealed class DataImportJob : IJob<ImportInput>
{
private readonly IJobContext _context;
private readonly IDataService _data;
public DataImportJob(IJobContext context, IDataService data)
{
_context = context;
_data = data;
}
public async Task ExecuteAsync(ImportInput input, CancellationToken ct)
{
var records = await _data.FetchAsync(input.Source, ct);
var total = records.Count;
for (var i = 0; i < total; i++)
{
await _data.ProcessAsync(records[i], ct);
var percent = (int)((i + 1) / (double)total * 100);
await _context.ReportProgressAsync(percent, $"Processed {i + 1}/{total}", ct);
}
}
}
Progress Extension Methods¶
NexJob ships convenience extensions that wire up progress reporting to collection iteration automatically.
await foreach (var item in source.WithProgress(_context, ct))
{
await ProcessAsync(item, ct);
}
Reports the percentage of items already yielded. The extension reads the whole source into memory first to determine the total count, so use it only for collections that fit comfortably in memory.
foreach (var item in items.WithProgress(_context))
{
await ProcessAsync(item, ct);
}
Fire-and-forget progress for synchronous collections. The sequence is materialised first to know its size, and the progress call is not awaited — storage errors while reporting are not surfaced to the job.
Progress Checkpoints for Long-Running Jobs¶
For batch processing, data migrations, and multi-step workflows, use checkpoints to persist exactly how far the job has progressed. If the job is interrupted — by a worker restart, a transient crash, or a retry after failure — it resumes from the last checkpoint rather than starting over.
Saving a Checkpoint¶
Call SaveCheckpointAsync<TState> with your state object. NexJob serialises it and persists it atomically alongside the progress update.
public sealed class LargeDataSyncJob : IJob
{
private readonly IJobContext _context;
private readonly IRecordReader _reader;
private readonly IRecordProcessor _processor;
public LargeDataSyncJob(
IJobContext context,
IRecordReader reader,
IRecordProcessor processor)
{
_context = context;
_reader = reader;
_processor = processor;
}
public async Task ExecuteAsync(CancellationToken ct)
{
// Resume from previous checkpoint, or start fresh on first attempt
var state = _context.GetCheckpoint<SyncCheckpoint>()
?? new SyncCheckpoint(LastProcessedId: 0, TotalBatches: 0);
var batch = await _reader.FetchNextBatchAsync(afterId: state.LastProcessedId, ct);
while (batch.Count > 0)
{
await _processor.ProcessBatchAsync(batch, ct);
state = state with
{
LastProcessedId = batch.Last().Id,
TotalBatches = state.TotalBatches + 1
};
await _context.SaveCheckpointAsync(
state: state,
percent: null,
message: $"Processed batch {state.TotalBatches} up to ID {state.LastProcessedId}",
ct: ct);
batch = await _reader.FetchNextBatchAsync(afterId: state.LastProcessedId, ct);
}
}
}
public record SyncCheckpoint(long LastProcessedId, int TotalBatches);
Resuming from a Checkpoint¶
Call GetCheckpoint<TState>() at the start of ExecuteAsync. It returns null on the first attempt (no checkpoint saved yet) and the deserialised state object on subsequent attempts.
var state = _context.GetCheckpoint<SyncCheckpoint>()
?? new SyncCheckpoint(LastProcessedId: 0, TotalBatches: 0);
Checkpoint Lifecycle¶
-
Persists across retries
If the job fails,
CheckpointJsonis preserved in storage. The next attempt callsGetCheckpointand picks up exactly where execution stopped. -
Auto-clears on success
When the job completes successfully, NexJob automatically clears the saved checkpoint to avoid unnecessary storage bloat.
-
Low I/O overhead
Checkpoint payloads are persisted alongside progress updates in a single database operation and do not appear in list views or SSE dashboard feeds.
Detecting Retries¶
Use Attempt to branch logic on retries — for example to skip expensive setup steps you already completed:
public async Task ExecuteAsync(CancellationToken ct)
{
if (_context.Attempt == 1)
{
await _setup.PrepareAsync(ct); // only needed on first attempt
}
await _work.RunAsync(ct);
}
Note
Jobs are transient services, so each attempt creates a new instance. Do not track attempt state in instance fields — use IJobContext.Attempt or a checkpoint instead.
Using IJobContext from Other Services¶
Because IJobContext is scoped, any service resolved inside the same job scope can inject it directly:
public sealed class AuditTrail(IJobContext context)
{
public string CorrelationId => context.JobId.Value.ToString();
}
Warning
Resolving IJobContext outside a job execution — from a web request, a singleton, or a hosted service — throws InvalidOperationException: "IJobContext is only available during job execution". If a service must work both inside and outside jobs, pass the values it needs explicitly, or read them from the structured logging scope NexJob opens around every execution (NexJob.JobId, NexJob.JobType, NexJob.Queue, NexJob.Attempt).
When to Use IJobContext¶
-
Use it when you need to
Access the job ID for logging or external correlation, report progress for long-running jobs, detect retries (
Attempt > 1), or read the queue name or tags from inside the job. -
Skip it when you only need
Your input data — that comes through
IJob<T>.ExecuteAsync(TInput, ...). Storage access — inject your storage service directly.