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 Dapr Jobs API #1331

Closed
wants to merge 56 commits into from
Closed
Show file tree
Hide file tree
Changes from 6 commits
Commits
Show all changes
56 commits
Select commit Hold shift + click to select a range
f68503d
Squash merge from job-api-sdk to fix DCO error
WhitWaldo Jul 25, 2024
28d288e
Merge branch 'master' into scheduler-api
WhitWaldo Jul 25, 2024
335daca
Merge branch 'master' into scheduler-api
WhitWaldo Aug 13, 2024
d83a49f
Added tests to validate that the generic builder is working as expected.
WhitWaldo Aug 14, 2024
b3cb273
Merge branch 'scheduler-api' of https://github.com/WhitWaldo/dapr-dot…
WhitWaldo Aug 14, 2024
13c32de
Merge branch 'master' into scheduler-api
WhitWaldo Aug 22, 2024
45df52f
Switched from using DateTime to DateTimeOffset per best practices
WhitWaldo Sep 13, 2024
26345eb
Fixed out of date casts from last commit
WhitWaldo Sep 13, 2024
8191e4e
Updated properties in client builder to use internal and updated Dapr…
WhitWaldo Sep 13, 2024
0a3a614
Updated version prefix to reflect the Dapr 1.14 release instead of ma…
WhitWaldo Sep 13, 2024
56d8779
Removed unused constructor arguments for DaprJobsGrpcClient
WhitWaldo Sep 13, 2024
6f4f14d
Generating the user agent once instead of with every call
WhitWaldo Sep 13, 2024
833aebe
Eliminated unnecessary overload
WhitWaldo Sep 13, 2024
e1b8126
Updated to use ReadOnlySpan<byte> over byte[]
WhitWaldo Sep 13, 2024
f40fd6f
Creating JsonSerializerOptions once as static readonly
WhitWaldo Sep 13, 2024
5e70625
Prefer using DateTimeOffset instead of DateTime
WhitWaldo Sep 13, 2024
b84331b
Defining regex fields once instead of each time the method is called
WhitWaldo Sep 13, 2024
700495e
Caught more DateTime -> DateTimeOffset.
WhitWaldo Sep 13, 2024
e586a76
Update uint -> int
WhitWaldo Sep 13, 2024
52ded7d
Added type with factories for building out valid expressions for cron…
WhitWaldo Sep 13, 2024
ae083f4
Removed unused Gprc channel property
WhitWaldo Sep 13, 2024
964b1dd
Updated to include internals access to Dapr.Client. Refactored enum e…
WhitWaldo Sep 13, 2024
b8fd743
Caching JSON serialization options
WhitWaldo Sep 14, 2024
dddbc34
Refactored file locations, simplified API to use the DaprJobSchedule …
WhitWaldo Sep 14, 2024
ac31e79
Fixed string extensions functionality
WhitWaldo Sep 14, 2024
a69192b
Fixed internals accessibility for Dapr.AspNetCore project
WhitWaldo Sep 14, 2024
d948dc5
Added more internals access for Dapr.Common. Removing reference acces…
WhitWaldo Sep 14, 2024
dca8427
Updated sample to reflect API changes
WhitWaldo Sep 14, 2024
3318119
Added support for ranged expressions, adding unit tests
WhitWaldo Sep 14, 2024
86245b6
Removed shared directory and moved types into Dapr.Common
WhitWaldo Sep 14, 2024
f19c85a
Updated InternalsVisibleTo on Dapr.Common to include all Dapr project…
WhitWaldo Sep 14, 2024
72fea1f
Merge branch 'master' into scheduler-api
WhitWaldo Sep 14, 2024
d1c6dee
Removed another reference to the shared class
WhitWaldo Sep 14, 2024
4075fbe
Removed still another reference to the shared type, added Dapr.Extens…
WhitWaldo Sep 14, 2024
f47f84f
Removed unused exceptions/tests.
WhitWaldo Sep 14, 2024
e7eba5e
Removed version prefix altogether from project metadata
WhitWaldo Sep 14, 2024
1a8ee7b
Fleshed out unit tests for CronExpressionBuilder, added overlooked case.
WhitWaldo Sep 14, 2024
6d0f439
Added cached JsonSerializerOptions so it's not recreated with each in…
WhitWaldo Sep 14, 2024
6dc8885
Mild refactoring
WhitWaldo Sep 14, 2024
b843ec2
Building out missing unit tests for broader code coverage
WhitWaldo Sep 14, 2024
d4600aa
Added missing copyright headers
WhitWaldo Sep 14, 2024
0186fcb
Built out unit tests to achieve fuller test coverage (and fixed a few…
WhitWaldo Sep 14, 2024
98d419a
Minor refactoring. Added unit tests to achieve better unit test cover…
WhitWaldo Sep 14, 2024
5118a07
Updated regular expression approach for performance/allocation reasons
WhitWaldo Sep 15, 2024
b594f28
Continued improvements to regex capture of Cron expressions
WhitWaldo Sep 15, 2024
33289a2
Simplifying Cron regex construction
WhitWaldo Sep 15, 2024
9f182f7
Replaced shared null exception validator with ArgumentVerifier.ThrowI…
WhitWaldo Sep 26, 2024
78e98c8
Minor type rename: JobDetails -> DaprJobDetails
WhitWaldo Sep 26, 2024
e819dee
Added StringComparison to extension to handle more specific string co…
WhitWaldo Sep 26, 2024
6362301
Removed unused extensions and unit tests
WhitWaldo Sep 26, 2024
d6e88d2
Updated to initialize field once with getter instead of re-initializi…
WhitWaldo Sep 26, 2024
ff3a037
Changed how exceptions are thrown so we're not reporting a Dapr endpo…
WhitWaldo Sep 26, 2024
1df5bd3
Updated how the mapping handler works. Rather than map for a given na…
WhitWaldo Sep 30, 2024
eea63d6
Updated documentation to reflect the updated handler
WhitWaldo Sep 30, 2024
10cc382
Updated mapping to use something more akin to the minimal API ingesti…
WhitWaldo Sep 30, 2024
4c252d5
Fixed unit tests, fixed duplicate cancellation token in dynamic invoc…
WhitWaldo Sep 30, 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