This is the multi-page printable view of this section. Click here to print.

Return to the regular view of this page.

Dapr Jobs .NET SDK

Overview of the Dapr Jobs building block for .NET

Dapr.Jobs provides a dedicated client for interacting with the Dapr Job APIs from a .NET application. With DaprJobsClient you can schedule future operations to run according to a predefined schedule with an optional payload. Three scheduling modes are supported: one-time jobs that fire at a specific point in time, interval-based jobs that recur on a fixed TimeSpan, and Cron-based jobs that use a Cron expression for calendar-aware scheduling. The SDK also provides minimal-API endpoint mapping so your application receives trigger invocations when a job fires.

The client is registered with dependency injection via AddDaprJobsClient() and can be configured from IConfiguration, environment variables, or a DaprJobsClientBuilder for advanced scenarios.

Core concepts

  • How to: Author and manage Dapr Jobs in the .NET SDK: client registration with dependency injection, IConfiguration, and manual instantiation; mapping job trigger endpoints in minimal APIs; scheduling one-time, interval-based, and Cron-based jobs; retrieving and deleting jobs.
  • DaprJobsClient usage: lifetime management, builder configuration, environment variables, gRPC channel options, cancellation, the three dependency injection overloads, payload serialization with JSON helper extensions, and error handling.

Next steps

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.

ArgumentMeaning
jobName, scheduleRequired name and DaprJobSchedule.
payloadOptional ReadOnlyMemory<byte> delivered unchanged to the callback.
startingFromOptional DateTimeOffset controlling the first due time.
repeatsOptional repeat count.
ttlOptional absolute DateTimeOffset expiry, later than startingFrom when both are supplied.
overwriteDefaults to false; opt in to replacing an existing same-name registration.
failurePolicyOptionsDrop or constant-interval retry policy for failed triggers.
cancellationTokenCancels 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.

Configure trigger failure policy

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.com
  • DAPR_GRPC_ENDPOINT: used to find the gRPC endpoint of the Dapr sidecar, example: https://dapr-grpc-api.mycompany.com
  • DAPR_HTTP_PORT: if DAPR_HTTP_ENDPOINT is not set, this is used to find the HTTP local endpoint of the Dapr sidecar
  • DAPR_GRPC_PORT: if DAPR_GRPC_ENDPOINT is not set, this is used to find the gRPC local endpoint of the Dapr sidecar
  • DAPR_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.