NodaTime
Use NodaTime values for scheduling, recurring time zones, job payloads and propagated context.
Immediate.Jobs.NodaTime adds NodaTime scheduling overloads and configures the job serializer for
NodaTime values in payloads and propagated context.
Install and register
dotnet add package Immediate.Jobs.NodaTime --prerelease Register the integration with Jobs:
using Immediate.Jobs.NodaTime;
builder.Services.AddMyAppHandlers();
builder.Services.AddMyAppJobs()
.ConfigureStorage(storage => storage.UseInMemory());
builder.Services.AddImmediateJobsNodaTime();AddImmediateJobsNodaTime() replaces the default IJobSerializer with NodaTimeJobSerializer. By default, that serializer uses JsonSerializerDefaults.Web and DateTimeZoneProviders.Tzdb. Pass an IDateTimeZoneProvider when the application uses a different
provider:
using NodaTime;
IDateTimeZoneProvider timeZoneProvider = DateTimeZoneProviders.Tzdb;
builder.Services.AddSingleton(timeZoneProvider);
builder.Services.AddImmediateJobsNodaTime(timeZoneProvider); Every worker that reads the stored jobs must use a provider that recognizes the same time-zone IDs.
Schedule with Duration and Instant
The generated scheduler remains the service you inject. Import Immediate.Jobs.NodaTime to make
the extension overloads available:
using Immediate.Jobs.NodaTime;
using NodaTime;
[Handler, Job]
public sealed partial class ReconcilePayment(IPaymentProvider payments)
{
public sealed record Payload(Guid OrderId, Instant ExpectedSettlementAt);
private ValueTask HandleAsync(Payload payload, CancellationToken cancellationToken) =>
payments.ReconcileAsync(payload.OrderId, payload.ExpectedSettlementAt, cancellationToken);
}
public sealed class PaymentScheduler(ReconcilePayment.Scheduler reconciliation, IClock clock)
{
public ValueTask<JobHandle> RetryAsync(Guid orderId, CancellationToken cancellationToken)
{
var expectedSettlement = clock.GetCurrentInstant() + Duration.FromHours(2);
return reconciliation.ScheduleAsync(
new(orderId, expectedSettlement),
Duration.FromMinutes(10),
cancellationToken
);
}
public ValueTask<JobHandle> AtSettlementAsync(Guid orderId, CancellationToken cancellationToken)
{
var expectedSettlement = clock.GetCurrentInstant() + Duration.FromHours(2);
return reconciliation.ScheduleAsync(
new(orderId, expectedSettlement),
expectedSettlement,
cancellationToken
);
}
} The package converts Duration to TimeSpan and Instant to DateTimeOffset before calling the
core scheduler. It does not replace Immediate.Jobs’ TimeProvider; inject IClock only when your
application code benefits from a NodaTime clock.
The complete scheduling surface is:
| Operation | NodaTime value | Notes |
|---|---|---|
ScheduleAsync(payload, ...) | Duration, Instant | Delayed or absolute scheduling, including grouped overloads. |
ScheduleAsync(payload, JobDetails, ...) | Duration, Instant | Delayed or absolute work added while a batch member runs. |
ScheduleAfterAsync(payload, ContinuationHandle, ...) | Duration | Delayed continuation after a durable job or batch. |
ScheduleAfterAsync(payload, IReadOnlyList<...>, ...) | Duration | Delayed continuation after every supplied durable job or batch. |
ScheduleAfter(payload, JobDetails, ...) | Duration | Delayed work buffered until the current attempt succeeds. |
Schedule(payload, Batch, ...) | Duration, Instant | Delayed or absolute member of an open batch. |
ScheduleAfter(payload, BatchJobHandle, ...) | Duration | Delayed continuation inside an open batch, including fan-in overloads. |
Batch and continuation overloads otherwise retain the behavior described in Batches and continuations, including trigger and storage-capability requirements.
Use DateTimeZone for dynamic schedules
The recurring overload accepts a DateTimeZone and persists its Id:
using Immediate.Jobs.NodaTime;
using NodaTime;
[Handler, Job(Name = "payment-reconciliation")]
public sealed partial class PaymentReconciliation(IPaymentProvider payments)
{
private ValueTask HandleAsync(
EmptyJobRequest request,
CancellationToken cancellationToken
) => payments.ReconcileOutstandingAsync(cancellationToken);
}
public sealed class PaymentScheduleSetup(PaymentReconciliation.Scheduler reconciliation)
{
public ValueTask ConfigureAsync(CancellationToken cancellationToken)
{
var vienna = DateTimeZoneProviders.Tzdb["Europe/Vienna"];
return reconciliation.AddOrUpdateRecurringAsync(
"daily-payment-reconciliation",
"0 3 * * *",
vienna,
cancellationToken
);
}
} This overload is available on IRecurringJobScheduler, including the generated scheduler for a
payloadless job without a code-defined cron expression. Cron parsing, reconciliation, overlap and
manual-trigger behavior are covered in Recurring jobs.
Serialize NodaTime values
The registration above applies the standard NodaTime.Serialization.SystemTextJson converters to
the serializer used for job payloads and context snapshots. Generated jobs continue to supply
their source-generated JsonTypeInfo<T> metadata, including for payloads such as ReconcilePayment.Payload above.
For application-owned serializer settings, pass your options to NodaTimeJobSerializer; its
constructor adds the NodaTime converters:
using System.Text.Json;
using Immediate.Jobs.NodaTime;
using NodaTime;
var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
WriteIndented = false,
};
IJobSerializer serializer = new NodaTimeJobSerializer(
jsonOptions,
DateTimeZoneProviders.Tzdb
); Call JsonSerializerOptions.UseNodaTime(...) when you need the same converters on options used
outside IJobSerializer. NodaTimeJobSerializer also has parameterless and IDateTimeZoneProvider constructors. If you replace IJobSerializer yourself, register the
configured serializer after Jobs registration.
When a Jobs project references NodaTime without referencing Immediate.Jobs.NodaTime, analyzer
error IJOB0004 is reported at the compilation level. Install the companion package even if the
NodaTime value appears only inside a job context snapshot.