Version v1.0 · dotnet
Job Model
Full job model explanation including interfaces, attributes, context, and registration behavior.
Job Model
In DurableStack, a job is a class that implements a supported job contract and is registered in the runtime job registry.
Each execution of that job is a run with its own lifecycle, attempt count, and status transitions.
Core job contracts
Use IDurableJob when a job has no payload:
public interface IDurableJob
{
Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default);
}
Use IDurableJob<TArgs> when a job needs typed input:
public interface IDurableJob<TArgs>
{
Task ExecuteAsync(TArgs args, JobContext context, CancellationToken cancellationToken = default);
}
What JobContext gives you
JobContext includes:
RunId: unique identifier for the specific run instance.JobName: logical job name from registration.Attempt: current attempt number for this run.ScheduledForUtc: the intended schedule/enqueue time for this run.Services: scoped service provider for resolving dependencies during execution.
Recommended model: attribute-based job metadata
DurableStack discovers public job classes automatically when options.JobRegistration.AutoDiscoverJobsFromAssembly is true (default).
Use attributes for execution and schedule metadata:
using DurableStack.Core;
using DurableStack.Core.Abstractions;
using DurableStack.Core.Models;
using DurableStack.Hosting.DependencyInjection;
[DurableJob(
Name = "invoice-sync",
MaxAttempts = 5,
RetryBehavior = RetryBehavior.Backoff,
RetryInitialDelaySeconds = 10)]
[RecurringJob(
"*/5 * * * *",
TimeZone = "UTC",
Enabled = true,
AllowConcurrentRuns = false)]
public sealed class InvoiceSyncJob : IDurableJob
{
public Task ExecuteAsync(JobContext context, CancellationToken cancellationToken = default)
{
return Task.CompletedTask;
}
}
Full attribute options and defaults
DurableJobAttribute
Name(string?): optional stable job name. Default is class name.MaxAttempts(int): total attempts including initial attempt. Default3.RetryBehavior(RetryBehavior):FixedDelayorBackoff. DefaultFixedDelay.RetryInitialDelaySeconds(int): per-job initial retry delay. Default0meaning unset, which falls back to runtimeoptions.RetryDelay.
RecurringJobAttribute
Cron(string, constructor): required cron expression.TimeZone(string): IANA time zone ID. DefaultUTC.Enabled(bool): whether recurring schedule starts enabled. Defaulttrue.AllowConcurrentRuns(bool): whether overlapping recurring runs are allowed. Defaultfalse.
One job, two execution paths
A job with only DurableJob is enqueue-only.
A job with both DurableJob and RecurringJob supports:
- automatic recurring materialization from cron
- manual enqueue/run-now using
IDurableStackClient
var runId = await durableClient.EnqueueAsync<InvoiceSyncJob>(cancellationToken: cancellationToken);
Runtime registration behavior
During startup, DurableStack builds DurableJobRegistration entries from discovered job classes.
Key registration rules:
- Duplicate job names are rejected.
- Duplicate job types are rejected.
- Invalid
MaxAttempts(<= 0) fails startup. - Invalid cron or invalid time zone fails startup.
Activation model and dependency resolution
Jobs are resolved from DI using one of two activation modes:
ScopedPerExecution(default): creates a fresh scope per run.RootProvider: resolves from root provider (scoped dependencies are not supported).
In most cases, keep ScopedPerExecution.
Explicit registration for advanced scenarios
You can explicitly register jobs instead of auto-discovery:
builder.Services.AddDurableJob<InvoiceSyncJob>(
name: "invoice-sync",
cronExpression: "*/5 * * * *",
timeZone: "UTC",
maxAttempts: 5);
Or with options:
builder.Services.AddDurableJob<InvoiceSyncJob>("invoice-sync", options =>
{
options.WithMaxAttempts(5)
.WithRetryBehavior(RetryBehavior.Backoff)
.WithRetryInitialDelaySeconds(10)
.RunOnCron("*/5 * * * *", timeZone: "UTC")
.WithAllowConcurrentRuns(false);
});
Practical guidance
- Keep job names stable once production data exists.
- Keep handlers idempotent so retries are safe.
- Use typed jobs (
IDurableJob<TArgs>) when payload shape matters. - Keep job classes focused on orchestration and call domain services for business logic.