1 - How to: Author and manage Dapr Jobs in the .NET SDK
Schedule jobs, receive HTTP or gRPC callbacks, and manage job registrations in .NET
Use Dapr.Jobs version 1.19.0 or later with a running Dapr sidecar and Scheduler. The complete
management surface described here requires a runtime that supports the corresponding operations; use
Dapr 1.19 for the 1.19 features. All client operations use version-aware stable/alpha API fallback, but that
does not make newer management operations available on an older runtime.
dotnet add package Dapr.Jobs
Jobs are scoped to the scheduling application’s Dapr app ID. The client does not accept a different target
app ID when scheduling a trigger. See the
Jobs sample.
Register the client and callback handler
Register the client before building the host, and map one handler for dynamically named jobs:
using Dapr.Jobs;
using Dapr.Jobs.Extensions;
using Dapr.Jobs.Models;
using System.Text;
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprJobsClient();
await using var app = builder.Build();
app.MapDaprScheduledJobHandler(
async (string jobName, ReadOnlyMemory<byte> payload, CancellationToken cancellationToken) =>
{
app.Logger.LogInformation("Received job {JobName}", jobName);
var text = Encoding.UTF8.GetString(payload.Span);
await HandleJobAsync(jobName, text, cancellationToken);
},
timeout: TimeSpan.FromSeconds(30));
await app.RunAsync();
HandleJobAsync represents your application logic. The callback receives the job name and original payload
bytes; additional services can be resolved from request DI. Its cancellation token includes the configured
handler timeout. Handlers must observe that token themselves; a timeout does not forcibly stop arbitrary code.
Return a Task for asynchronous work so the callback waits for it before acknowledging the trigger.
For HTTP callbacks, the mapper registers POST /job/{jobName}. Start the application with a matching app port:
dapr run --app-id jobsapp --app-port 5000 -- dotnet run --urls http://localhost:5000
gRPC callbacks
AddDaprJobsClient and MapDaprScheduledJobHandler also register the gRPC callback path. Choose it with
--app-protocol grpc. For plaintext gRPC, configure a dedicated HTTP/2 listener before builder.Build():
using Microsoft.AspNetCore.Server.Kestrel.Core;
builder.WebHost.ConfigureKestrel(options =>
{
options.ListenLocalhost(5000, listen => listen.Protocols = HttpProtocols.Http2);
});
An HTTP/1-only listener, or plaintext Http1AndHttp2 without protocol negotiation, cannot serve this gRPC
callback. Use separate listeners if the same app must also accept HTTP/1 traffic.
Register the job
Resolve DaprJobsClient and use ScheduleJobAsync for every schedule type:
var jobsClient = app.Services.GetRequiredService<DaprJobsClient>();
var schedule = DaprJobSchedule.FromDateTime(DateTimeOffset.UtcNow.AddMinutes(5));
await jobsClient.ScheduleJobAsync(
"send-receipt", schedule,
payload: Encoding.UTF8.GetBytes("order-42"),
overwrite: false,
cancellationToken: cancellationToken);
Schedule only after the callback is ready, or use a future due time. Registering a job does not by itself
start an application listener.
| Argument | Meaning |
|---|
jobName, schedule | Required name and DaprJobSchedule. |
payload | Optional ReadOnlyMemory<byte> delivered unchanged to the callback. |
startingFrom | Optional DateTimeOffset controlling the first due time. |
repeats | Optional repeat count. |
ttl | Optional absolute DateTimeOffset expiry, later than startingFrom when both are supplied. |
overwrite | Defaults to false; opt in to replacing an existing same-name registration. |
failurePolicyOptions | Drop or constant-interval retry policy for failed triggers. |
cancellationToken | Cancels waiting for the client operation; it is not the callback token. |
One-time, interval, and predefined schedules
var oneTime = DaprJobSchedule.FromDateTime(DateTimeOffset.UtcNow.AddHours(1));
var interval = DaprJobSchedule.FromDuration(TimeSpan.FromMinutes(15));
var daily = DaprJobSchedule.Daily;
await jobsClient.ScheduleJobAsync("refresh-cache", interval, repeats: 5,
cancellationToken: cancellationToken);
Predefined schedules also include Hourly, Midnight, Weekly, Monthly, and Yearly.
Cron-based job and time zone
Use FromExpression for a string expression, or FromCronExpression for a CronExpressionBuilder:
var schedule = DaprJobSchedule.FromExpression("CRON_TZ=America/New_York 0 9 * * *");
await jobsClient.ScheduleJobAsync("morning-report", schedule,
cancellationToken: cancellationToken);
The builder also supports WithTimeZone(string) and WithTimeZone(TimeZoneInfo). DaprJobSchedule.TimeZone exposes an embedded CRON_TZ
identifier. Use valid IANA time-zone IDs and account for daylight-saving changes when choosing calendar schedules.
JSON and string payload helpers
ScheduleJobWithPayloadAsync from Dapr.Jobs.Extensions accepts the same schedule:
await jobsClient.ScheduleJobWithPayloadAsync(
"create-report", DaprJobSchedule.Daily,
new { AccountId = "42", Format = "pdf" },
cancellationToken: cancellationToken);
The object overload JSON-serializes with web defaults unless you pass jsonSerializerOptions. The string
overload encodes UTF-8 text without JSON quoting. Deserialize callback bytes with the matching convention.
There are no separate ScheduleCronJobAsync or ScheduleOneTimeJobWithPayloadAsync helpers in this release.
await jobsClient.ScheduleJobAsync(
"refresh-catalog", DaprJobSchedule.Hourly,
failurePolicyOptions: new JobFailurePolicyConstantOptions(TimeSpan.FromSeconds(10))
{
MaxRetries = 5
},
cancellationToken: cancellationToken);
JobFailurePolicyConstantOptions retries at the specified interval; an unset MaxRetries allows indefinite
retries. JobFailurePolicyDropOptions drops a failed trigger. An omitted policy leaves runtime defaults in
effect. This policy is separate from the job’s recurring schedule and repeat count. Make callback side effects
idempotent because triggers can be delivered more than once.
Get details of already-scheduled job
var details = await jobsClient.GetJobAsync("refresh-catalog", cancellationToken);
DaprJobDetails exposes Name, Schedule, DueTime, Ttl, RepeatCount, Payload, FailurePolicy, and Overwrite.
Name can be null for manually constructed or older-runtime results. Inspect
details.Schedule.ExpressionValue and schedule classification properties such as IsCronExpression and
IsPointInTimeExpression. The response does not expose top-level Interval or CronExpression properties.
List all registered jobs
var registered = await jobsClient.ListJobsAsync(cancellationToken);
foreach (var job in registered)
{
Console.WriteLine($"{job.Name}: {job.Schedule.ExpressionValue}");
}
The result is an IReadOnlyList<DaprJobDetails> scoped to the current app ID. An empty list normally means
there are no jobs; also consider the release-specific cancellation behavior described below.
Delete a scheduled job
await jobsClient.DeleteJobAsync("refresh-catalog", cancellationToken);
Delete multiple jobs by name prefix
await jobsClient.DeleteJobsByPrefixAsync("report-", cancellationToken);
The prefix match is case-sensitive. A null, empty, or whitespace prefix deletes all jobs for this app ID.
Validate a user-supplied prefix before calling this operation.
Purge all jobs
await jobsClient.PurgeAllJobsAsync(cancellationToken);
This explicitly requests deletion of every job associated with the current app ID, not every job in the cluster.
Errors and cancellation
Runtime failures are wrapped in DaprException; inspect InnerException. Invalid arguments can throw standard
argument exceptions. In 1.19.0, several Jobs methods catch caller cancellation instead of throwing
OperationCanceledException: writes/deletes can return without confirmation and listing can return empty.
GetJobAsync reports a missing expected result as DaprException after cancellation. See
DaprJobsClient usage for client configuration and payload conventions.
Analyzer
The Dapr.Jobs.Analyzers package ships diagnostic DAPR1501 (warning, category Usage). It fires when
ScheduleJobAsync is called with a literal job name and no MapDaprScheduledJobHandler call exists in the same
source file, with the message “Job invocations require the MapDaprScheduledJobHandler be set and configured for
job name ‘{name}’ on IEndpointRouteBuilder”. Add the handler mapping, or suppress the diagnostic with
#pragma warning disable DAPR1501 or an .editorconfig rule if the handler is intentionally mapped elsewhere.
2 - DaprJobsClient usage
Essential tips and advice for using DaprJobsClient
Lifetime management
A DaprJobsClient is a version of the Dapr client that is dedicated to interacting with the Dapr Jobs API. It can be
registered alongside a DaprClient and other Dapr clients without issue.
It maintains access to networking resources in the form of TCP sockets used to communicate with the Dapr sidecar and
implements IDisposable to support the eager cleanup of resources.
For best performance, create a single long-lived instance of DaprJobsClient and provide access to that shared instance
throughout your application. DaprJobsClient instances are thread-safe and intended to be shared.
This can be aided by utilizing the dependency injection functionality. The registration method supports registration using
as a singleton, a scoped instance or as transient (meaning it’s recreated every time it’s injected), but also enables
registration to utilize values from an IConfiguration or other injected service in a way that’s impractical when
creating the client from scratch in each of your classes.
Avoid creating a DaprJobsClient for each operation and disposing it when the operation is complete.
Configuring DaprJobsClient via the DaprJobsClientBuilder
A DaprJobsClient can be configured by invoking methods on the DaprJobsClientBuilder class before calling .Build()
to create the client itself. The settings for each DaprJobsClient are separate
and cannot be changed after calling .Build().
var daprJobsClient = new DaprJobsClientBuilder()
.UseDaprApiToken("abc123") // Specify the API token used to authenticate to other Dapr sidecars
.Build();
The DaprJobsClientBuilder contains settings for:
- The HTTP endpoint of the Dapr sidecar
- The gRPC endpoint of the Dapr sidecar
- The
JsonSerializerOptions object used to configure JSON serialization - The
GrpcChannelOptions object used to configure gRPC - The API token used to authenticate requests to the sidecar
- The factory method used to create the
HttpClient instance used by the SDK - The timeout used for the
HttpClient instance when making requests to the sidecar
The SDK will read the following environment variables to configure the default values:
DAPR_HTTP_ENDPOINT: used to find the HTTP endpoint of the Dapr sidecar, example: https://dapr-api.mycompany.comDAPR_GRPC_ENDPOINT: used to find the gRPC endpoint of the Dapr sidecar, example: https://dapr-grpc-api.mycompany.comDAPR_HTTP_PORT: if DAPR_HTTP_ENDPOINT is not set, this is used to find the HTTP local endpoint of the Dapr sidecarDAPR_GRPC_PORT: if DAPR_GRPC_ENDPOINT is not set, this is used to find the gRPC local endpoint of the Dapr sidecarDAPR_API_TOKEN: used to set the API token
Configuring gRPC channel options
Dapr’s use of CancellationToken for cancellation relies on the configuration of the gRPC channel options. If you need
to configure these options yourself, make sure to enable the ThrowOperationCanceledOnCancellation setting.
var daprJobsClient = new DaprJobsClientBuilder()
.UseGrpcChannelOptions(new GrpcChannelOptions { ThrowOperationCanceledOnCancellation = true })
.Build();
Using cancellation with DaprJobsClient
The APIs on DaprJobsClient perform asynchronous operations and accept an optional CancellationToken parameter. This
follows a standard .NET practice for cancellable operations. Note that when cancellation occurs, there is no guarantee that
the remote endpoint stops processing the request, only that the client has stopped waiting for completion.
Pass cancellation tokens to bound calls. In SDK 1.19.0, scheduling, deletion, and listing catch
caller-initiated cancellation: write methods can complete without confirming a change and listing can return
an empty list. GetJobAsync can surface a DaprException after cancellation. Do not rely on an
OperationCanceledException being thrown by every Jobs operation, or interpret a canceled call as confirmation
that a job was created/deleted. Check the token and reconcile with the runtime when the outcome matters.
Configuring DaprJobsClient via dependency injection
Using the built-in extension methods for registering the DaprJobsClient in a dependency injection container can
provide the benefit of registering the long-lived service a single time, centralize complex configuration and improve
performance by ensuring similarly long-lived resources are re-purposed when possible (e.g. HttpClient instances).
There are three overloads available to give the developer the greatest flexibility in configuring the client for their
scenario. Each of these will register the IHttpClientFactory on your behalf if not already registered, and configure
the DaprJobsClientBuilder to use it when creating the HttpClient instance in order to re-use the same instance as
much as possible and avoid socket exhaustion and other issues.
In the first approach, there’s no configuration done by the developer and the DaprJobsClient is configured with the
default settings.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprJobsClient(); //Registers the `DaprJobsClient` to be injected as needed
var app = builder.Build();
AddDaprJobsClient() also registers the gRPC AppCallbackAlpha callback service and configures Kestrel for HTTP/1 + HTTP/2, enabling the sidecar to deliver job triggers over either HTTP or gRPC without any additional setup.
Sometimes the developer will need to configure the created client using the various configuration options detailed
above. This is done through an overload that passes in the DaprJobsClientBuilder and exposes methods for configuring
the necessary options.
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprJobsClient((_, daprJobsClientBuilder) => {
//Set the API token
daprJobsClientBuilder.UseDaprApiToken("abc123");
//Specify a non-standard HTTP endpoint
daprJobsClientBuilder.UseHttpEndpoint("http://dapr.my-company.com");
});
var app = builder.Build();
Finally, it’s possible that the developer may need to retrieve information from another service in order to populate
these configuration values. That value may be provided from a DaprClient instance, a vendor-specific SDK or some
local service, but as long as it’s also registered in DI, it can be injected into this configuration operation via the
last overload:
var builder = WebApplication.CreateBuilder(args);
//Register a fictional service that retrieves secrets from somewhere
builder.Services.AddSingleton<SecretService>();
builder.Services.AddDaprJobsClient((serviceProvider, daprJobsClientBuilder) => {
//Retrieve an instance of the `SecretService` from the service provider
var secretService = serviceProvider.GetRequiredService<SecretService>();
var daprApiToken = secretService.GetSecret("DaprApiToken").Value;
//Configure the `DaprJobsClientBuilder`
daprJobsClientBuilder.UseDaprApiToken(daprApiToken);
});
var app = builder.Build();
Understanding payload serialization on DaprJobsClient
While there are many methods on the DaprClient that automatically serialize and deserialize data using the
System.Text.Json serializer, this SDK takes a different philosophy. Instead, the relevant methods accept an optional
payload of ReadOnlyMemory<byte> meaning that serialization is an exercise left to the developer and is not
generally handled by the SDK.
For JSON-serializable values, use ScheduleJobWithPayloadAsync from Dapr.Jobs.Extensions. It accepts a
DaprJobSchedule, an object payload, and optional JsonSerializerOptions.
This will convert the value to UTF-8 encoded bytes for you as a convenience. Here’s an example of what this might
look like when scheduling a Cron expression:
public sealed record Doodad (string Name, int Value);
//...
var doodad = new Doodad("Thing", 100);
await daprJobsClient.ScheduleJobWithPayloadAsync(
"myJob", DaprJobSchedule.FromExpression("5 * * * *"), doodad);
In the same vein, if you have a plain string value, you can use an overload of the same method to serialize a
string-typed payload and the JSON serialization step will be skipped and it’ll only be encoded to an array of
UTF-8 encoded bytes. Here’s an example of what this might look like when scheduling a one-time job:
var now = DateTimeOffset.UtcNow;
var oneWeekFromNow = now.AddDays(7);
await daprJobsClient.ScheduleJobWithPayloadAsync(
"myOtherJob", DaprJobSchedule.FromDateTime(oneWeekFromNow), "This is a test!");
The delegate handling the job invocation expects at least two arguments to be present:
- A
string that is populated with the jobName, providing the name of the invoked job - A
ReadOnlyMemory<byte> that is populated with the bytes originally provided during the job registration.
Deserialize with the same format used when scheduling. For JSON, use
JsonSerializer.Deserialize<T>(jobPayload.Span, options) with matching serializer options; for UTF-8 text,
use Encoding.UTF8.GetString. The client does not infer the payload’s type or encoding.
To deserialize the bytes to a string, the following helper method can be used:
var payloadAsString = Encoding.UTF8.GetString(jobPayload.Span); //If successful, returns a string with the value
Error handling
DaprJobsClient wraps runtime/gRPC failures in DaprException. Invalid argument values raise standard
exceptions such as ArgumentOutOfRangeException or ArgumentNullException.
The most common cases of failure will be related to:
- Incorrect argument formatting while engaging with the Jobs API
- Transient failures such as a networking problem
- Invalid data, such as a failure to deserialize a value into a type it wasn’t originally serialized from
In any of these cases, you can examine more exception details through the .InnerException property.