1 - How to: Author and manage Dapr Jobs in the .NET SDK
Learn how to author and manage Dapr Jobs using the .NET SDK
Let’s create an endpoint that will be invoked by Dapr Jobs when it triggers, then schedule the job in the same app. We’ll use the simple example provided here, for the following demonstration and walk through it as an explainer of how you can schedule one-time or recurring jobs using either an interval or Cron expression yourself. In this guide,
you will:
- Deploy a .NET Web API application (JobsSample)
- Utilize the Dapr .NET Jobs SDK to schedule a job invocation and set up the endpoint to be triggered
In the .NET example project:
- The main
Program.cs
file comprises the entirety of this demonstration.
Prerequisites
Note
Note that while .NET 6 is the minimum support version of .NET in Dapr v1.15, only .NET 8 and .NET 9 will continue to be supported by Dapr in v1.16 and later.
Set up the environment
Clone the .NET SDK repo.
git clone https://github.com/dapr/dotnet-sdk.git
From the .NET SDK root directory, navigate to the Dapr Jobs example.
Run the application locally
To run the Dapr application, you need to start the .NET program and a Dapr sidecar. Navigate to the JobsSample
directory.
We’ll run a command that starts both the Dapr sidecar and the .NET program at the same time.
dapr run --app-id jobsapp --dapr-grpc-port 4001 --dapr-http-port 3500 -- dotnet run
Dapr listens for HTTP requests at http://localhost:3500
and internal Jobs gRPC requests at http://localhost:4001
.
Register the Dapr Jobs client with dependency injection
The Dapr Jobs SDK provides an extension method to simplify the registration of the Dapr Jobs client. Before completing
the dependency injection registration in Program.cs
, add the following line:
var builder = WebApplication.CreateBuilder(args);
//Add anywhere between these two lines
builder.Services.AddDaprJobsClient();
var app = builder.Build();
Note that in today’s implementation of the Jobs API, the app that schedules the job will also be the app that receives the trigger notification. In other words, you cannot schedule a trigger to run in another application. As a result, while you don’t explicitly need the Dapr Jobs client to be registered in your application to schedule a trigger invocation endpoint, your endpoint will never be invoked without the same app also scheduling the job somehow (whether via this Dapr Jobs .NET SDK or an HTTP call to the sidecar).
It’s possible that you may want to provide some configuration options to the Dapr Jobs client that
should be present with each call to the sidecar such as a Dapr API token, or you want to use a non-standard
HTTP or gRPC endpoint. This is possible through use of an overload of the registration method that allows configuration of a
DaprJobsClientBuilder
instance:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprJobsClient((_, daprJobsClientBuilder) =>
{
daprJobsClientBuilder.UseDaprApiToken("abc123");
daprJobsClientBuilder.UseHttpEndpoint("http://localhost:8512"); //Non-standard sidecar HTTP endpoint
});
var app = builder.Build();
Still, it’s possible that whatever values you wish to inject need to be retrieved from some other source, itself registered as a dependency. There’s one more overload you can use to inject an IServiceProvider
into the configuration action method. In the following example, we register a fictional singleton that can retrieve secrets from somewhere and pass it into the configuration method for AddDaprJobClient
so
we can retrieve our Dapr API token from somewhere else for registration here:
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddSingleton<SecretRetriever>();
builder.Services.AddDaprJobsClient((serviceProvider, daprJobsClientBuilder) =>
{
var secretRetriever = serviceProvider.GetRequiredService<SecretRetriever>();
var daprApiToken = secretRetriever.GetSecret("DaprApiToken").Value;
daprJobsClientBuilder.UseDaprApiToken(daprApiToken);
daprJobsClientBuilder.UseHttpEndpoint("http://localhost:8512");
});
var app = builder.Build();
Use the Dapr Jobs client using IConfiguration
It’s possible to configure the Dapr Jobs client using the values in your registered IConfiguration
as well without
explicitly specifying each of the value overrides using the DaprJobsClientBuilder
as demonstrated in the previous
section. Rather, by populating an IConfiguration
made available through dependency injection the AddDaprJobsClient()
registration will automatically use these values over their respective defaults.
Start by populating the values in your configuration. This can be done in several different ways as demonstrated below.
Configuration via ConfigurationBuilder
Application settings can be configured without using a configuration source and by instead populating the value in-memory
using a ConfigurationBuilder
instance:
var builder = WebApplication.CreateBuilder();
//Create the configuration
var configuration = new ConfigurationBuilder()
.AddInMemoryCollection(new Dictionary<string, string> {
{ "DAPR_HTTP_ENDPOINT", "http://localhost:54321" },
{ "DAPR_API_TOKEN", "abc123" }
})
.Build();
builder.Configuration.AddConfiguration(configuration);
builder.Services.AddDaprJobsClient(); //This will automatically populate the HTTP endpoint and API token values from the IConfiguration
Configuration via Environment Variables
Application settings can be accessed from environment variables available to your application.
The following environment variables will be used to populate both the HTTP endpoint and API token used to register the
Dapr Jobs client.
Key |
Value |
DAPR_HTTP_ENDPOINT |
http://localhost:54321 |
DAPR_API_TOKEN |
abc123 |
var builder = WebApplication.CreateBuilder();
builder.Configuration.AddEnvironmentVariables();
builder.Services.AddDaprJobsClient();
The Dapr Jobs client will be configured to use both the HTTP endpoint http://localhost:54321
and populate all outbound
requests with the API token header abc123
.
Configuration via prefixed Environment Variables
However, in shared-host scenarios where there are multiple applications all running on the same machine without using
containers or in development environments, it’s not uncommon to prefix environment variables. The following example
assumes that both the HTTP endpoint and the API token will be pulled from environment variables prefixed with the
value “myapp_”. The two environment variables used in this scenario are as follows:
Key |
Value |
myapp_DAPR_HTTP_ENDPOINT |
http://localhost:54321 |
myapp_DAPR_API_TOKEN |
abc123 |
These environment variables will be loaded into the registered configuration in the following example and made available
without the prefix attached.
var builder = WebApplication.CreateBuilder();
builder.Configuration.AddEnvironmentVariables(prefix: "myapp_");
builder.Services.AddDaprJobsClient();
The Dapr Jobs client will be configured to use both the HTTP endpoint http://localhost:54321
and populate all outbound
requests with the API token header abc123
.
Use the Dapr Jobs client without relying on dependency injection
While the use of dependency injection simplifies the use of complex types in .NET and makes it easier to
deal with complicated configurations, you’re not required to register the DaprJobsClient
in this way. Rather, you can also elect to create an instance of it from a DaprJobsClientBuilder
instance as demonstrated below:
public class MySampleClass
{
public void DoSomething()
{
var daprJobsClientBuilder = new DaprJobsClientBuilder();
var daprJobsClient = daprJobsClientBuilder.Build();
//Do something with the `daprJobsClient`
}
}
Set up a endpoint to be invoked when the job is triggered
It’s easy to set up a jobs endpoint if you’re at all familiar with minimal APIs in ASP.NET Core as the syntax is the same between the two.
Once dependency injection registration has been completed, configure the application the same way you would to handle mapping an HTTP request via the minimal API functionality in ASP.NET Core. Implemented as an extension method,
pass the name of the job it should be responsive to and a delegate. Services can be injected into the delegate’s arguments as you wish and the job payload can be accessed from the ReadOnlyMemory<byte>
originally provided to the
job registration.
There are two delegates you can use here. One provides an IServiceProvider
in case you need to inject other services into the handler:
//We have this from the example above
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprJobsClient();
var app = builder.Build();
//Add our endpoint registration
app.MapDaprScheduledJob("myJob", (IServiceProvider serviceProvider, string jobName, ReadOnlyMemory<byte> jobPayload) => {
var logger = serviceProvider.GetService<ILogger>();
logger?.LogInformation("Received trigger invocation for '{jobName}'", "myJob");
//Do something...
});
app.Run();
The other overload of the delegate doesn’t require an IServiceProvider
if not necessary:
//We have this from the example above
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprJobsClient();
var app = builder.Build();
//Add our endpoint registration
app.MapDaprScheduledJob("myJob", (string jobName, ReadOnlyMemory<byte> jobPayload) => {
//Do something...
});
app.Run();
Support cancellation tokens when processing mapped invocations
You may want to ensure that timeouts are handled on job invocations so that they don’t indefinitely hang and use system resources. When setting up the job mapping, there’s an optional TimeSpan
parameter that can be
provided as the last argument to specify a timeout for the request. Every time the job mapping invocation is triggered, a new CancellationTokenSource
will be created using this timeout parameter and a CancellationToken
will be created from it to put an upper bound on the processing of the request. If a timeout isn’t provided, this defaults to CancellationToken.None
and a timeout will not be automatically applied to the mapping.
//We have this from the example above
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddDaprJobsClient();
var app = builder.Build();
//Add our endpoint registration
app.MapDaprScheduledJob("myJob", (string jobName, ReadOnlyMemory<byte> jobPayload) => {
//Do something...
}, TimeSpan.FromSeconds(15)); //Assigns a maximum timeout of 15 seconds for handling the invocation request
app.Run();
Register the job
Finally, we have to register the job we want scheduled. Note that from here, all SDK methods have cancellation token support and use a default token if not otherwise set.
There are three different ways to set up a job that vary based on how you want to configure the schedule:
One-time job
A one-time job is exactly that; it will run at a single point in time and will not repeat. This approach requires that you select a job name and specify a time it should be triggered.
Argument Name |
Type |
Description |
Required |
jobName |
string |
The name of the job being scheduled. |
Yes |
scheduledTime |
DateTime |
The point in time when the job should be run. |
Yes |
payload |
ReadOnlyMemory |
Job data provided to the invocation endpoint when triggered. |
No |
cancellationToken |
CancellationToken |
Used to cancel out of the operation early, e.g. because of an operation timeout. |
No |
One-time jobs can be scheduled from the Dapr Jobs client as in the following example:
public class MyOperation(DaprJobsClient daprJobsClient)
{
public async Task ScheduleOneTimeJobAsync(CancellationToken cancellationToken)
{
var today = DateTime.UtcNow;
var threeDaysFromNow = today.AddDays(3);
await daprJobsClient.ScheduleOneTimeJobAsync("myJobName", threeDaysFromNow, cancellationToken: cancellationToken);
}
}
Interval-based job
An interval-based job is one that runs on a recurring loop configured as a fixed amount of time, not unlike how reminders work in the Actors building block today. These jobs can be scheduled with a number of optional arguments as well:
Argument Name |
Type |
Description |
Required |
jobName |
string |
The name of the job being scheduled. |
Yes |
interval |
TimeSpan |
The interval at which the job should be triggered. |
Yes |
startingFrom |
DateTime |
The point in time from which the job schedule should start. |
No |
repeats |
int |
The maximum number of times the job should be triggered. |
No |
ttl |
When the job should expires and no longer trigger. |
No |
|
payload |
ReadOnlyMemory |
Job data provided to the invocation endpoint when triggered. |
No |
cancellationToken |
CancellationToken |
Used to cancel out of the operation early, e.g. because of an operation timeout. |
No |
Interval-based jobs can be scheduled from the Dapr Jobs client as in the following example:
public class MyOperation(DaprJobsClient daprJobsClient)
{
public async Task ScheduleIntervalJobAsync(CancellationToken cancellationToken)
{
var hourlyInterval = TimeSpan.FromHours(1);
//Trigger the job hourly, but a maximum of 5 times
await daprJobsClient.ScheduleIntervalJobAsync("myJobName", hourlyInterval, repeats: 5), cancellationToken: cancellationToken;
}
}
Cron-based job
A Cron-based job is scheduled using a Cron expression. This gives more calendar-based control over when the job is triggered as it can used calendar-based values in the expression. Like the other options, these jobs can be scheduled with a number of optional arguments as well:
Argument Name |
Type |
Description |
Required |
jobName |
string |
The name of the job being scheduled. |
Yes |
cronExpression |
string |
The systemd Cron-like expression indicating when the job should be triggered. |
Yes |
startingFrom |
DateTime |
The point in time from which the job schedule should start. |
No |
repeats |
int |
The maximum number of times the job should be triggered. |
No |
ttl |
When the job should expires and no longer trigger. |
No |
|
payload |
ReadOnlyMemory |
Job data provided to the invocation endpoint when triggered. |
No |
cancellationToken |
CancellationToken |
Used to cancel out of the operation early, e.g. because of an operation timeout. |
No |
A Cron-based job can be scheduled from the Dapr Jobs client as follows:
public class MyOperation(DaprJobsClient daprJobsClient)
{
public async Task ScheduleCronJobAsync(CancellationToken cancellationToken)
{
//At the top of every other hour on the fifth day of the month
const string cronSchedule = "0 */2 5 * *";
//Don't start this until next month
var now = DateTime.UtcNow;
var oneMonthFromNow = now.AddMonths(1);
var firstOfNextMonth = new DateTime(oneMonthFromNow.Year, oneMonthFromNow.Month, 1, 0, 0, 0);
//Trigger the job hourly, but a maximum of 5 times
await daprJobsClient.ScheduleCronJobAsync("myJobName", cronSchedule, dueTime: firstOfNextMonth, cancellationToken: cancellationToken);
}
}
Get details of already-scheduled job
If you know the name of an already-scheduled job, you can retrieve its metadata without waiting for it to
be triggered. The returned JobDetails
exposes a few helpful properties for consuming the information from the Dapr Jobs API:
- If the
Schedule
property contains a Cron expression, the IsCronExpression
property will be true and the expression will also be available in the CronExpression
property.
- If the
Schedule
property contains a duration value, the IsIntervalExpression
property will instead be true and the value will be converted to a TimeSpan
value accessible from the Interval
property.
This can be done by using the following:
public class MyOperation(DaprJobsClient daprJobsClient)
{
public async Task<JobDetails> GetJobDetailsAsync(string jobName, CancellationToken cancellationToken)
{
var jobDetails = await daprJobsClient.GetJobAsync(jobName, canecllationToken);
return jobDetails;
}
}
Delete a scheduled job
To delete a scheduled job, you’ll need to know its name. From there, it’s as simple as calling the DeleteJobAsync
method on the Dapr Jobs client:
public class MyOperation(DaprJobsClient daprJobsClient)
{
public async Task DeleteJobAsync(string jobName, CancellationToken cancellationToken)
{
await daprJobsClient.DeleteJobAsync(jobName, cancellationToken);
}
}
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.
When an operation is cancelled, it will throw an OperationCancelledException
.
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();
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 DaprJobsClientBuiler
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.
That said, there are some helper extension methods available for each of the scheduling methods. If you know that you
want to use a type that’s JSON-serializable, you can use the Schedule*WithPayloadAsync
method for each scheduling
type that accepts an object
as a payload and an optional JsonSerializerOptions
to use when serializing the value.
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.ScheduleCronJobWithPayloadAsync("myJob", "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 = DateTime.UtcNow;
var oneWeekFromNow = now.AddDays(7);
await daprJobsClient.ScheduleOneTimeJobWithPayloadAsync("myOtherJob", 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.
Because the payload is stored as a ReadOnlyMemory<byte>
, the developer has the freedom to serialize and deserialize
as they wish, but there are again two helper extensions included that can deserialize this to either a JSON-compatible
type or a string. Both methods assume that the developer encoded the originally scheduled job (perhaps using the
helper serialization methods) as these methods will not force the bytes to represent something they’re not.
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
Methods on DaprJobsClient
will throw a DaprJobsServiceException
if an issue is encountered between the SDK
and the Jobs API service running on the Dapr sidecar. If a failure is encountered because of a poorly formatted
request made to the Jobs API service through this SDK, a DaprMalformedJobException
will be thrown. In case of
illegal argument values, the appropriate standard exception will be thrown (e.g. ArgumentOutOfRangeException
or ArgumentNullException
) with the name of the offending argument. And for anything else, a DaprException
will be thrown.
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.