Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added .NET client for new Dapr Jobs API support #1320

Closed
wants to merge 65 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
65 commits
Select commit Hold shift + click to select a range
3301b03
Added .NET client for new Dapr Jobs API support
WhitWaldo Jul 6, 2024
5732ca5
Removed explicit .NET 7 targeting
WhitWaldo Jul 8, 2024
debf7ad
Using `AddDaprJobsClient` for consistency with other SDKs
WhitWaldo Jul 8, 2024
c3b2a8c
Refactored the DaprClientBuilder out to a common project. Updated dep…
WhitWaldo Jul 9, 2024
12fb796
Renamed to reflect that it's a Dapr exception
WhitWaldo Jul 9, 2024
cfd74f7
Updated solution to propertly find Dapr.Common in /src directory
WhitWaldo Jul 9, 2024
24ae98c
Added scheduler.proto and updated project file to autogenerate approp…
WhitWaldo Jul 9, 2024
4e5d856
Removed unused type
WhitWaldo Jul 9, 2024
d0e577b
Fixed an XML comment that didn't make sense
WhitWaldo Jul 9, 2024
5faa914
Implemented service-specific equivalent of DaprClientBuilder for Dapr…
WhitWaldo Jul 9, 2024
4ca40eb
Updated to support DaprJobClientOptions which specify the App ID and …
WhitWaldo Jul 9, 2024
b0291bf
Added DI registration extensions that support options
WhitWaldo Jul 9, 2024
797be63
Refactored out DaprException to common project - retained same namesp…
WhitWaldo Jul 9, 2024
c4b028b
Fixed method not returning properly
WhitWaldo Jul 9, 2024
c15e090
Added extension method for parsing a TimeSpan from a Golang interval
WhitWaldo Jul 9, 2024
f3a7c41
Implemented all but the Watch method on the client
WhitWaldo Jul 9, 2024
6157cd0
Added missing copyright headers
WhitWaldo Jul 9, 2024
0f0954d
Finished out most of the WatchJobs method.
WhitWaldo Jul 9, 2024
2cdd554
Adding attribute to mark endpoints for Job trigger invocations
WhitWaldo Jul 10, 2024
e713661
Partially through bidirectional watcher implementation, but stopping …
WhitWaldo Jul 10, 2024
49c78eb
Updates to use the correct protos (dapr runtime, not the scheduler se…
WhitWaldo Jul 10, 2024
e76b323
Added default null values
WhitWaldo Jul 10, 2024
79e56d6
Added schedule job overloads for when the developer doesn't want to s…
WhitWaldo Jul 10, 2024
9723ff5
Rather than 6 overloads of the same method, changed the names of each…
WhitWaldo Jul 10, 2024
6bd99c5
Solution update
WhitWaldo Jul 10, 2024
63df29d
Removed serialization from SDK - swapping to only accepting and retur…
WhitWaldo Jul 11, 2024
d6cbf51
Update to use ReadOnlyMemory<byte> instead of byte[]
WhitWaldo Jul 12, 2024
bcdf08b
Refactored the generic client builder so that less identical code nee…
WhitWaldo Jul 12, 2024
597ac0d
Updating constructor to use protected instead of public
WhitWaldo Jul 12, 2024
c08376a
Neglected to update DaprJobsGrpcClient to reflect signature changes. …
WhitWaldo Jul 12, 2024
ff3cf9c
Removed DaprJobClientOptions as no longer needed since we're using th…
WhitWaldo Jul 15, 2024
3e1b0f3
Consolidated all the extensions
WhitWaldo Jul 15, 2024
e44dca0
Renamed to ScheduledJobAttribute to match the API name
WhitWaldo Jul 15, 2024
5b7600d
Adding ASP.NET Core to package to add route builder extension support
WhitWaldo Jul 15, 2024
c15fa66
Added another registration overload so the developer isn't forced to …
WhitWaldo Jul 15, 2024
a29a4dd
Shifted schedule deserialization into the JobDetails record itself an…
WhitWaldo Jul 15, 2024
1252ef6
Adding JobsSample project to prove out API
WhitWaldo Jul 15, 2024
4057ca5
Removed tentatively unnecessary attribute since we're relying on ASP.…
WhitWaldo Jul 15, 2024
449609e
Simplified example
WhitWaldo Jul 15, 2024
0101327
Fixing unit tests - resolving errors due to lack of InternalsVisibleT…
WhitWaldo Jul 15, 2024
c860260
Fixed last locally-broken test client due to lack of InternalsVisible…
WhitWaldo Jul 15, 2024
fb983a9
Updated name of invocation endpoint registration method to include "D…
WhitWaldo Jul 15, 2024
b3db843
Building out unit tests
WhitWaldo Jul 16, 2024
f6333e9
Removed unused using
WhitWaldo Jul 16, 2024
2bf4cfd
Added some null validation
WhitWaldo Jul 16, 2024
e5d5646
Refactored to use the same naming convention as the other types in th…
WhitWaldo Jul 16, 2024
61b1978
Building on last commit, updated extension names
WhitWaldo Jul 16, 2024
90a5fe1
Adding more unit tests
WhitWaldo Jul 16, 2024
e674fa8
Removed unused type
WhitWaldo Jul 16, 2024
4215d87
Updated to use int? instead of uint? for repeats parameter. Added che…
WhitWaldo Jul 16, 2024
8e72f5a
Added support for IDisposable
WhitWaldo Jul 16, 2024
20be1d5
Updated to support provisioning an HttpClient from IHttpClientFactory…
WhitWaldo Jul 16, 2024
1780337
Fixed an issue with the API token not being properly passed into clie…
WhitWaldo Jul 16, 2024
22d3a17
Another tweak to finalize fix for Dapr API token - updated tests as well
WhitWaldo Jul 16, 2024
60b1c88
Adding helper deserialization extension methods + tests
WhitWaldo Jul 16, 2024
db4f93f
Added more helper extension methods for serializing strings and JSON-…
WhitWaldo Jul 16, 2024
62052e6
Naming correction to ensure parameter names are similar from one sche…
WhitWaldo Jul 16, 2024
9fef177
Removed JsonSerializerOptions support from DaprJobsClient as it's not…
WhitWaldo Jul 16, 2024
39dbcf8
First pass at updating documentation
WhitWaldo Jul 16, 2024
8d59928
Updated packages to fix build errors because of everything's dependen…
WhitWaldo Jul 16, 2024
4a101a6
Forced to update these packages to 8.0.0 because of other transitive …
WhitWaldo Jul 16, 2024
ffdb47b
Fixed build errors introduced by the transient reference to Microsoft…
WhitWaldo Jul 16, 2024
8eb57f5
Fixed issues in the unit tests caused by over-eager fix on the Dapr A…
WhitWaldo Jul 16, 2024
81f9ef5
Merge branch 'master' into job-api-sdk
WhitWaldo Jul 24, 2024
2e26e13
Fixed incorrect unit test
WhitWaldo Jul 25, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ This repo builds the following packages:
- Dapr.Actors.AspNetCore
- Dapr.Extensions.Configuration
- Dapr.Workflow
- Dapr.Jobs

### Prerequisites

Expand Down
38 changes: 38 additions & 0 deletions all.sln
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,18 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.E2E.Test.Actors.Genera
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Cryptography", "examples\Client\Cryptography\Cryptography.csproj", "{C74FBA78-13E8-407F-A173-4555AEE41FF3}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Jobs", "src\Dapr.Jobs\Dapr.Jobs.csproj", "{D34F9326-8D8C-43C4-975B-7201A9C97E6E}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Jobs.Test", "test\Dapr.Jobs.Test\Dapr.Jobs.Test.csproj", "{EDEE625E-6815-40E1-935F-35129771A0F8}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Dapr.Common", "src\Dapr.Common\Dapr.Common.csproj", "{3E075F71-185E-4C09-9449-79D21A958487}"
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Jobs", "Jobs", "{4DA40205-C38D-4E19-BD9A-0F18EE06CBAB}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "JobsSample", "examples\Jobs\JobsSample\JobsSample.csproj", "{11E59564-D677-4137-81BD-CF0B142530DB}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Dapr.Common.Test", "test\Dapr.Common.Test\Dapr.Common.Test.csproj", "{229BB84C-69A4-4A40-AD49-1FD6C237E0C5}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Expand Down Expand Up @@ -290,6 +302,26 @@ Global
{C74FBA78-13E8-407F-A173-4555AEE41FF3}.Debug|Any CPU.Build.0 = Debug|Any CPU
{C74FBA78-13E8-407F-A173-4555AEE41FF3}.Release|Any CPU.ActiveCfg = Release|Any CPU
{C74FBA78-13E8-407F-A173-4555AEE41FF3}.Release|Any CPU.Build.0 = Release|Any CPU
{D34F9326-8D8C-43C4-975B-7201A9C97E6E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D34F9326-8D8C-43C4-975B-7201A9C97E6E}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D34F9326-8D8C-43C4-975B-7201A9C97E6E}.Release|Any CPU.ActiveCfg = Release|Any CPU
{D34F9326-8D8C-43C4-975B-7201A9C97E6E}.Release|Any CPU.Build.0 = Release|Any CPU
{EDEE625E-6815-40E1-935F-35129771A0F8}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EDEE625E-6815-40E1-935F-35129771A0F8}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EDEE625E-6815-40E1-935F-35129771A0F8}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EDEE625E-6815-40E1-935F-35129771A0F8}.Release|Any CPU.Build.0 = Release|Any CPU
{3E075F71-185E-4C09-9449-79D21A958487}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{3E075F71-185E-4C09-9449-79D21A958487}.Debug|Any CPU.Build.0 = Debug|Any CPU
{3E075F71-185E-4C09-9449-79D21A958487}.Release|Any CPU.ActiveCfg = Release|Any CPU
{3E075F71-185E-4C09-9449-79D21A958487}.Release|Any CPU.Build.0 = Release|Any CPU
{11E59564-D677-4137-81BD-CF0B142530DB}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{11E59564-D677-4137-81BD-CF0B142530DB}.Debug|Any CPU.Build.0 = Debug|Any CPU
{11E59564-D677-4137-81BD-CF0B142530DB}.Release|Any CPU.ActiveCfg = Release|Any CPU
{11E59564-D677-4137-81BD-CF0B142530DB}.Release|Any CPU.Build.0 = Release|Any CPU
{229BB84C-69A4-4A40-AD49-1FD6C237E0C5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{229BB84C-69A4-4A40-AD49-1FD6C237E0C5}.Debug|Any CPU.Build.0 = Debug|Any CPU
{229BB84C-69A4-4A40-AD49-1FD6C237E0C5}.Release|Any CPU.ActiveCfg = Release|Any CPU
{229BB84C-69A4-4A40-AD49-1FD6C237E0C5}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
Expand Down Expand Up @@ -343,6 +375,12 @@ Global
{AF89083D-4715-42E6-93E9-38497D12A8A6} = {DD020B34-460F-455F-8D17-CF4A949F100B}
{B5CDB0DC-B26D-48F1-B934-FE5C1C991940} = {DD020B34-460F-455F-8D17-CF4A949F100B}
{C74FBA78-13E8-407F-A173-4555AEE41FF3} = {A7F41094-8648-446B-AECD-DCC2CC871F73}
{D34F9326-8D8C-43C4-975B-7201A9C97E6E} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{EDEE625E-6815-40E1-935F-35129771A0F8} = {DD020B34-460F-455F-8D17-CF4A949F100B}
{3E075F71-185E-4C09-9449-79D21A958487} = {27C5D71D-0721-4221-9286-B94AB07B58CF}
{4DA40205-C38D-4E19-BD9A-0F18EE06CBAB} = {D687DDC4-66C5-4667-9E3A-FD8B78ECAA78}
{11E59564-D677-4137-81BD-CF0B142530DB} = {4DA40205-C38D-4E19-BD9A-0F18EE06CBAB}
{229BB84C-69A4-4A40-AD49-1FD6C237E0C5} = {DD020B34-460F-455F-8D17-CF4A949F100B}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {65220BF2-EAE1-4CB2-AA58-EBE80768CB40}
Expand Down
7 changes: 7 additions & 0 deletions daprdocs/content/en/dotnet-sdk-docs/_index.md
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,13 @@ Put the Dapr .NET SDK to the test. Walk through the .NET quickstarts and tutoria
<a href="{{< ref dotnet-workflow >}}" class="stretched-link"></a>
</div>
</div>
<div class="card">
<div class="card-body">
<h5 class="card-title"><b>Jobs</b></h5>
<p class="card-text">Create and manage the scheduling and orchestration of jobs in .NET.</p>
<a href="{{< ref dotnet-jobs >}}" class="stretched-link"></a>
</div>
</div>
</div>

## More information
Expand Down
8 changes: 8 additions & 0 deletions daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
---
type: docs
title: "Dapr Jobs .NET SDK"
linkTitle: "Jobs"
weight: 50000
description: Get up and running with Dapr Jobs and the Dapr .NET SDK
---

269 changes: 269 additions & 0 deletions daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,269 @@
---
type: docs
title: "How to: Author and manage Dapr Jobs in the .NET SDK"
linkTitle: "How to: Author & manage jobs"
weight: 10000
description: 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](https://github.com/dapr/dotnet-sdk/tree/master/examples/Jobs), 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](https://github.com/dapr/dotnet-sdk/tree/master/examples/Jobs/JobsSample))
- Utilize the .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`](https://github.com/dapr/dotnet-sdk/tree/master/examples/Jobs/JobsSample/Program.cs) file comprises the entirety of this demonstration.

## Prerequisites
- [.NET 6+](https://dotnet.microsoft.com/download) installed
- [Dapr CLI](https://docs.dapr.io/getting-started/install-dapr-cli/)
- [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost)
- [Dapr Jobs .NET SDK](https://github.com/dapr/dotnet-sdk)

## Set up the environment
Clone the [.NET SDK repo](https://github.com/dapr/dotnet-sdk).

```sh
git clone https://github.com/dapr/dotnet-sdk.git
```

From the .NET SDK root directory, navigate to the Dapr Jobs example.

```sh
cd examples/Jobs
```

## 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.

```sh
cd JobsSample
```

We'll run a command that starts both the Dapr sidecar and the .NET program at the same time.

```sh
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:

```cs
var builder = WebApplication.CreateBuilder(args);

//Add anywhere between these two
builder.Services.AddDaprJobsClient(); //That's it

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 an overload of the register method that allows configuration of a `DaprJobsClientBuilder` instance:

```cs
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:

```cs
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 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:

```cs

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](https://learn.microsoft.com/en-us/aspnet/core/fundamentals/minimal-apis/overview) 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 you can optionally pass a `JobDetails` to get information about the job that has been triggered (e.g. access its scheduling setup or payload):

```cs
//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", (JobDetails jobDetails, ILogger logger) => {
logger.LogInformation("Received trigger invocation for '{jobName}'", "myJob");

//Do something...
});

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<byte> | 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:

```cs
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](https://docs.dapr.io/developing-applications/building-blocks/actors/actors-timers-reminders/#actor-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<byte> | 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:

```cs
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<byte> | 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:

```cs
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:

```cs
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:

```cs
public class MyOperation(DaprJobsClient daprJobsClient)
{
public async Task DeleteJobAsync(string jobName, CancellationToken cancellationToken)
{
await daprJobsClient.DeleteJobAsync(jobName, cancellationToken);
}
}
```
Loading
Loading