diff --git a/README.md b/README.md index 948516fe2..b55dc89fd 100644 --- a/README.md +++ b/README.md @@ -46,6 +46,7 @@ This repo builds the following packages: - Dapr.Actors.AspNetCore - Dapr.Extensions.Configuration - Dapr.Workflow +- Dapr.Jobs ### Prerequisites diff --git a/all.sln b/all.sln index 228047852..1e1903b90 100644 --- a/all.sln +++ b/all.sln @@ -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 @@ -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 @@ -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} diff --git a/daprdocs/content/en/dotnet-sdk-docs/_index.md b/daprdocs/content/en/dotnet-sdk-docs/_index.md index 121dde310..72e8b71d9 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/_index.md +++ b/daprdocs/content/en/dotnet-sdk-docs/_index.md @@ -69,6 +69,13 @@ Put the Dapr .NET SDK to the test. Walk through the .NET quickstarts and tutoria +
+
+
Jobs
+

Create and manage the scheduling and orchestration of jobs in .NET.

+ +
+
## More information diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md new file mode 100644 index 000000000..de000571e --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/_index.md @@ -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 +--- + diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md new file mode 100644 index 000000000..51414b93b --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobs-howto.md @@ -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(); +builder.Services.AddDaprJobsClient((serviceProvider, daprJobsClientBuilder) => +{ + var secretRetriever = serviceProvider.GetRequiredService(); + 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 | 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 | 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 | 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 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); + } +} +``` \ No newline at end of file diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobsclient-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobsclient-usage.md new file mode 100644 index 000000000..ac4b6b82d --- /dev/null +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-jobs/dotnet-jobsclient-usage.md @@ -0,0 +1,171 @@ +--- +type: docs +title: "DaprJobsClient usage" +linkTitle: "DaprJobsClient usage" +weight: 5000 +description: 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` 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. + +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()`. + +```cs +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](https://grpc.github.io/grpc/csharp-dotnet/api/Grpc.Net.Client.GrpcChannelOptions.html#Grpc_Net_Client_GrpcChannelOptions_ThrowOperationCanceledOnCancellation). + +```cs +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 idiom 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 exhaution and other issues. + +In the first approach, there's no configuration done by the developer and the `DaprJobsClient` is configured with the default settings. + +```cs +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. + +```cs +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: + +```cs +var builder = WebApplication.CreateBuilder(args); + +//Register a fictional service that retrieves secrets from somewhere +builder.Services.AddSingleton(); + +builder.Services.AddDaprJobsClient((serviceProvider, daprJobsClientBuilder) => { + //Retrieve an instance of the `SecretService` from the service provider + var secretService = serviceProvider.GetRequiredService(); + 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` 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: + +```cs +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 exampe of what this might look like when scheduling a one-time job: + +```cs +var now = DateTime.UtcNow; +var oneWeekFromNow = now.AddDays(7); +await daprJobsClient.ScheduleOneTimeJobWithPayloadAsync("myOtherJob", oneWeekFromNow, "This is a test!"); +``` + +The `JobDetails` type returns the data as a `ReadOnlyMemory?` so the developer has the freedom to 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: +```cs +if (jobDetails.Payload is not null) +{ + string payloadAsString = jobDetails.Payload.DeserializeToString(); //If successful, returns a string value with the value +} +``` + +To deserialize JSON-encoded UTF-8 bytes to the corresponding type, the following helper method can be used. An overload argument is available that permits the developer to pass in their own `JsonSerializerOptions` to be applied during deserialization. + +```cs +public sealed record Doodad (string Name, int Value); + +//... + +if (jobDetails.Payload is not null) +{ + var deserializedDoodad = jobDetails.Payload.DeserializeFromJsonBytes(); +} +``` + +## 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. \ No newline at end of file diff --git a/examples/Jobs/JobsSample/JobsSample.csproj b/examples/Jobs/JobsSample/JobsSample.csproj new file mode 100644 index 000000000..89ee1baf3 --- /dev/null +++ b/examples/Jobs/JobsSample/JobsSample.csproj @@ -0,0 +1,13 @@ + + + + net8.0 + enable + enable + + + + + + + diff --git a/examples/Jobs/JobsSample/Program.cs b/examples/Jobs/JobsSample/Program.cs new file mode 100644 index 000000000..cf89f5b33 --- /dev/null +++ b/examples/Jobs/JobsSample/Program.cs @@ -0,0 +1,42 @@ +#pragma warning disable CS0618 // Type or member is obsolete +using System.Text; +using Dapr.Jobs; +using Dapr.Jobs.Extensions; +using Dapr.Jobs.Models; +using Dapr.Jobs.Models.Responses; + +var builder = WebApplication.CreateBuilder(args); + +builder.Services.AddDaprJobsClient(); + +var app = builder.Build(); + +// Configure the HTTP request pipeline. + +app.MapDaprScheduledJob("myJob", (ILogger logger, DaprJobDetails details) => +{ + logger.LogInformation("Received trigger invocation for 'myJob'"); + + if (details.Payload is not null) + { + var deserializedPayload = Encoding.UTF8.GetString(details.Payload.Value.ToArray()); + + logger.LogInformation($"Received invocation for the 'myJob' job with payload '{deserializedPayload}'"); + return; + } + logger.LogInformation("Failed to deserialize payload for trigger invocation"); +}); + +app.Run(); + +await using var scope = app.Services.CreateAsyncScope(); +var logger = scope.ServiceProvider.GetRequiredService(); +var daprJobsClient = scope.ServiceProvider.GetRequiredService(); + +logger.LogInformation("Scheduling one-time job 'myJob' to execute 10 seconds from now"); +await daprJobsClient.ScheduleJobAsync("myJob", DaprJobSchedule.FromDateTime(DateTime.UtcNow.AddSeconds(10)), + Encoding.UTF8.GetBytes("This is a test")); +logger.LogInformation("Scheduled one-time job 'myJob'"); + + +#pragma warning restore CS0618 // Type or member is obsolete diff --git a/examples/Jobs/JobsSample/Properties/launchSettings.json b/examples/Jobs/JobsSample/Properties/launchSettings.json new file mode 100644 index 000000000..edc5d029f --- /dev/null +++ b/examples/Jobs/JobsSample/Properties/launchSettings.json @@ -0,0 +1,30 @@ +{ + "$schema": "http://json.schemastore.org/launchsettings.json", + "iisSettings": { + "windowsAuthentication": false, + "anonymousAuthentication": true, + "iisExpress": { + "applicationUrl": "http://localhost:40682", + "sslPort": 0 + } + }, + "profiles": { + "http": { + "commandName": "Project", + "dotnetRunMessages": true, + "launchBrowser": false, + "applicationUrl": "http://localhost:5227", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + }, + "IIS Express": { + "commandName": "IISExpress", + "launchBrowser": false, + "launchUrl": "weatherforecast", + "environmentVariables": { + "ASPNETCORE_ENVIRONMENT": "Development" + } + } + } +} diff --git a/examples/Jobs/JobsSample/appsettings.Development.json b/examples/Jobs/JobsSample/appsettings.Development.json new file mode 100644 index 000000000..0c208ae91 --- /dev/null +++ b/examples/Jobs/JobsSample/appsettings.Development.json @@ -0,0 +1,8 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + } +} diff --git a/examples/Jobs/JobsSample/appsettings.json b/examples/Jobs/JobsSample/appsettings.json new file mode 100644 index 000000000..10f68b8c8 --- /dev/null +++ b/examples/Jobs/JobsSample/appsettings.json @@ -0,0 +1,9 @@ +{ + "Logging": { + "LogLevel": { + "Default": "Information", + "Microsoft.AspNetCore": "Warning" + } + }, + "AllowedHosts": "*" +} diff --git a/properties/IsExternalInit.cs b/properties/IsExternalInit.cs index 34357c39a..28e38a0c8 100644 --- a/properties/IsExternalInit.cs +++ b/properties/IsExternalInit.cs @@ -13,5 +13,5 @@ namespace System.Runtime.CompilerServices internal static class IsExternalInit { } - + } diff --git a/src/Dapr.Actors/Dapr.Actors.csproj b/src/Dapr.Actors/Dapr.Actors.csproj index 3fb63ea20..564bd2be5 100644 --- a/src/Dapr.Actors/Dapr.Actors.csproj +++ b/src/Dapr.Actors/Dapr.Actors.csproj @@ -6,17 +6,12 @@ $(PackageTags);Actors - - - - - - + diff --git a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj index 54996e4bc..12b512fbb 100644 --- a/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj +++ b/src/Dapr.AspNetCore/Dapr.AspNetCore.csproj @@ -5,17 +5,13 @@ This package contains the reference assemblies for developing services using Dapr and AspNetCore. - - - - - + diff --git a/src/Dapr.AspNetCore/DaprAuthenticationBuilderExtensions.cs b/src/Dapr.AspNetCore/DaprAuthenticationBuilderExtensions.cs index 0c37dbc8f..e55b2916e 100644 --- a/src/Dapr.AspNetCore/DaprAuthenticationBuilderExtensions.cs +++ b/src/Dapr.AspNetCore/DaprAuthenticationBuilderExtensions.cs @@ -12,7 +12,6 @@ // ------------------------------------------------------------------------ using System; -using Dapr; using Dapr.AspNetCore; namespace Microsoft.AspNetCore.Authentication diff --git a/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs b/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs index 388015b80..9bfafb065 100644 --- a/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs +++ b/src/Dapr.AspNetCore/DaprServiceCollectionExtensions.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -14,7 +14,6 @@ namespace Microsoft.Extensions.DependencyInjection { using System; - using System.Linq; using Dapr.Client; using Extensions; diff --git a/src/Dapr.Client/Dapr.Client.csproj b/src/Dapr.Client/Dapr.Client.csproj index 1a348cc86..b0cacfe8f 100644 --- a/src/Dapr.Client/Dapr.Client.csproj +++ b/src/Dapr.Client/Dapr.Client.csproj @@ -1,10 +1,5 @@  - - - - - @@ -21,9 +16,8 @@ - - + diff --git a/src/Dapr.Client/DaprClientBuilder.cs b/src/Dapr.Client/DaprClientBuilder.cs index 50a4979d1..ce643c862 100644 --- a/src/Dapr.Client/DaprClientBuilder.cs +++ b/src/Dapr.Client/DaprClientBuilder.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,180 +11,26 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client -{ - using System; - using System.Net.Http; - using System.Text.Json; - using Grpc.Net.Client; - using Autogenerated = Autogen.Grpc.v1; +using Dapr.Common; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Client; +/// +/// Builds a . +/// +public sealed class DaprClientBuilder : DaprGenericClientBuilder +{ /// - /// Builder for building + /// Builds the client instance from the properties of the builder. /// - public sealed class DaprClientBuilder + public override DaprClient Build() { - /// - /// Initializes a new instance of the class. - /// - public DaprClientBuilder() - { - this.GrpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); - this.HttpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(); - - this.GrpcChannelOptions = new GrpcChannelOptions() - { - // The gRPC client doesn't throw the right exception for cancellation - // by default, this switches that behavior on. - ThrowOperationCanceledOnCancellation = true, - }; - - this.JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); - this.DaprApiToken = DaprDefaults.GetDefaultDaprApiToken(); - } - - // property exposed for testing purposes - internal string GrpcEndpoint { get; private set; } - - // property exposed for testing purposes - internal string HttpEndpoint { get; private set; } - - private Func HttpClientFactory { get; set; } - - // property exposed for testing purposes - internal JsonSerializerOptions JsonSerializerOptions { get; private set; } - - // property exposed for testing purposes - internal GrpcChannelOptions GrpcChannelOptions { get; private set; } - internal string DaprApiToken { get; private set; } - internal TimeSpan Timeout { get; private set; } - - /// - /// Overrides the HTTP endpoint used by for communicating with the Dapr runtime. - /// - /// - /// The URI endpoint to use for HTTP calls to the Dapr runtime. The default value will be - /// DAPR_HTTP_ENDPOINT first, or http://127.0.0.1:DAPR_HTTP_PORT as fallback - /// where DAPR_HTTP_ENDPOINT and DAPR_HTTP_PORT represents the value of the - /// corresponding environment variables. - /// - /// The instance. - public DaprClientBuilder UseHttpEndpoint(string httpEndpoint) - { - ArgumentVerifier.ThrowIfNullOrEmpty(httpEndpoint, nameof(httpEndpoint)); - this.HttpEndpoint = httpEndpoint; - return this; - } - - // Internal for testing of DaprClient - internal DaprClientBuilder UseHttpClientFactory(Func factory) - { - this.HttpClientFactory = factory; - return this; - } - - /// - /// Overrides the gRPC endpoint used by for communicating with the Dapr runtime. - /// - /// - /// The URI endpoint to use for gRPC calls to the Dapr runtime. The default value will be - /// http://127.0.0.1:DAPR_GRPC_PORT where DAPR_GRPC_PORT represents the value of the - /// DAPR_GRPC_PORT environment variable. - /// - /// The instance. - public DaprClientBuilder UseGrpcEndpoint(string grpcEndpoint) - { - ArgumentVerifier.ThrowIfNullOrEmpty(grpcEndpoint, nameof(grpcEndpoint)); - this.GrpcEndpoint = grpcEndpoint; - return this; - } - - /// - /// - /// Uses the specified when serializing or deserializing using . - /// - /// - /// The default value is created using . - /// - /// - /// Json serialization options. - /// The instance. - public DaprClientBuilder UseJsonSerializationOptions(JsonSerializerOptions options) - { - this.JsonSerializerOptions = options; - return this; - } - - /// - /// Uses the provided for creating the . - /// - /// The to use for creating the . - /// The instance. - public DaprClientBuilder UseGrpcChannelOptions(GrpcChannelOptions grpcChannelOptions) - { - this.GrpcChannelOptions = grpcChannelOptions; - return this; - } - - /// - /// Adds the provided on every request to the Dapr runtime. - /// - /// The token to be added to the request headers/>. - /// The instance. - public DaprClientBuilder UseDaprApiToken(string apiToken) - { - this.DaprApiToken = apiToken; - return this; - } - - /// - /// Sets the timeout for the HTTP client used by the . - /// - /// - /// - public DaprClientBuilder UseTimeout(TimeSpan timeout) - { - this.Timeout = timeout; - return this; - } - - /// - /// Builds a instance from the properties of the builder. - /// - /// The . - public DaprClient Build() - { - var grpcEndpoint = new Uri(this.GrpcEndpoint); - if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https") - { - throw new InvalidOperationException("The gRPC endpoint must use http or https."); - } - - if (grpcEndpoint.Scheme.Equals(Uri.UriSchemeHttp)) - { - // Set correct switch to maksecure gRPC service calls. This switch must be set before creating the GrpcChannel. - AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); - } - - var httpEndpoint = new Uri(this.HttpEndpoint); - if (httpEndpoint.Scheme != "http" && httpEndpoint.Scheme != "https") - { - throw new InvalidOperationException("The HTTP endpoint must use http or https."); - } - - var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); - var client = new Autogenerated.Dapr.DaprClient(channel); - - - var apiTokenHeader = DaprClient.GetDaprApiTokenHeader(this.DaprApiToken); - var httpClient = HttpClientFactory is object ? HttpClientFactory() : new HttpClient(); - - if (this.Timeout > TimeSpan.Zero) - { - httpClient.Timeout = this.Timeout; - } + var daprClientDependencies = this.BuildDaprClientDependencies(); - return new DaprClientGrpc(channel, client, httpClient, httpEndpoint, this.JsonSerializerOptions, apiTokenHeader); - } + var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); + var apiTokenHeader = this.DaprApiToken is not null ? DaprClient.GetDaprApiTokenHeader(this.DaprApiToken) : null; + return new DaprClientGrpc(daprClientDependencies.channel, client, daprClientDependencies.httpClient, + daprClientDependencies.httpEndpoint, this.JsonSerializerOptions, apiTokenHeader); } } diff --git a/src/Dapr.Client/DaprClientGrpc.cs b/src/Dapr.Client/DaprClientGrpc.cs index af245afc3..877b9562e 100644 --- a/src/Dapr.Client/DaprClientGrpc.cs +++ b/src/Dapr.Client/DaprClientGrpc.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Dapr.Common; + namespace Dapr.Client { using System; diff --git a/src/Dapr.Client/properties/AssemblyInfo.cs b/src/Dapr.Client/properties/AssemblyInfo.cs index ee00f7bfe..4decc0647 100644 --- a/src/Dapr.Client/properties/AssemblyInfo.cs +++ b/src/Dapr.Client/properties/AssemblyInfo.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/src/Dapr.Common/ArgumentVerifier.cs b/src/Dapr.Common/ArgumentVerifier.cs new file mode 100644 index 000000000..e850604b8 --- /dev/null +++ b/src/Dapr.Common/ArgumentVerifier.cs @@ -0,0 +1,53 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +// TODO: Remove this when every project that uses this file has nullable enabled. +#nullable enable + +namespace Dapr; + +using System; +using System.Diagnostics.CodeAnalysis; + +/// +/// A utility class to perform argument validations. +/// +internal static class ArgumentVerifier +{ + /// + /// Throws ArgumentNullException if argument is null. + /// + /// Argument value to check. + /// Name of Argument. + public static void ThrowIfNull([NotNull] object? value, string name) + { + if (value == null) + throw new ArgumentNullException(name); + } + + /// + /// Validates string and throws: + /// ArgumentNullException if argument is null. + /// ArgumentException if argument is empty. + /// + /// Argument value to check. + /// Name of Argument. + public static void ThrowIfNullOrEmpty([NotNull] string? value, string name) + { + if (value == null) + throw new ArgumentNullException(name); + + if (string.IsNullOrEmpty(value)) + throw new ArgumentException("The value cannot be null or empty", name); + } +} diff --git a/src/Dapr.Common/AssemblyInfo.cs b/src/Dapr.Common/AssemblyInfo.cs new file mode 100644 index 000000000..e4a209d3d --- /dev/null +++ b/src/Dapr.Common/AssemblyInfo.cs @@ -0,0 +1,42 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Dapr.Actors, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.Generators, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Client, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.AspNetCore, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Extensions.Configuration, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Jobs, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Workflow, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] + +[assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore.IntegrationTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore.IntegrationTest.App, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.Generators.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Actors.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.AspNetCore.IntegrationTest.App, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.AspNetCore.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Client.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Common.Test ,PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test ,PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test.Actors ,PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test.Actors.Generators ,PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test.App ,PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test.App.Grpc ,PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.E2E.Test.App.ReentrantActors ,PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Extensions.Configuration.Test ,PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] +[assembly: InternalsVisibleTo("Dapr.Jobs.Test ,PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2")] diff --git a/src/Dapr.Common/Dapr.Common.csproj b/src/Dapr.Common/Dapr.Common.csproj new file mode 100644 index 000000000..7452b0fed --- /dev/null +++ b/src/Dapr.Common/Dapr.Common.csproj @@ -0,0 +1,14 @@ + + + + net6.0;net8.0 + enable + enable + + + + + + + + \ No newline at end of file diff --git a/src/Dapr.Common/DaprDefaults.cs b/src/Dapr.Common/DaprDefaults.cs new file mode 100644 index 000000000..058ed7638 --- /dev/null +++ b/src/Dapr.Common/DaprDefaults.cs @@ -0,0 +1,102 @@ +// ------------------------------------------------------------------------ +// Copyright 2021 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr; + +internal static class DaprDefaults +{ + private static string httpEndpoint = string.Empty; + private static string grpcEndpoint = string.Empty; + private static string daprApiToken = string.Empty; + private static string appApiToken = string.Empty; + + /// + /// Get the value of environment variable DAPR_API_TOKEN + /// + /// The value of environment variable DAPR_API_TOKEN + public static string GetDefaultDaprApiToken() + { + // Lazy-init is safe because this is just populating the default + // We don't plan to support the case where the user changes environment variables + // for a running process. + if (string.IsNullOrEmpty(daprApiToken)) + { + // Treat empty the same as null since it's an environment variable + var value = Environment.GetEnvironmentVariable("DAPR_API_TOKEN"); + daprApiToken = string.IsNullOrEmpty(value) ? string.Empty : value; + } + + return daprApiToken; + } + + /// + /// Get the value of environment variable APP_API_TOKEN + /// + /// The value of environment variable APP_API_TOKEN + public static string GetDefaultAppApiToken() + { + if (string.IsNullOrEmpty(appApiToken)) + { + var value = Environment.GetEnvironmentVariable("APP_API_TOKEN"); + appApiToken = string.IsNullOrEmpty(value) ? string.Empty : value; + } + + return appApiToken; + } + + /// + /// Get the value of HTTP endpoint based off environment variables + /// + /// The value of HTTP endpoint based off environment variables + public static string GetDefaultHttpEndpoint() + { + if (string.IsNullOrEmpty(httpEndpoint)) + { + var endpoint = Environment.GetEnvironmentVariable("DAPR_HTTP_ENDPOINT"); + if (!string.IsNullOrEmpty(endpoint)) + { + httpEndpoint = endpoint; + return httpEndpoint; + } + + var port = Environment.GetEnvironmentVariable("DAPR_HTTP_PORT"); + port = string.IsNullOrEmpty(port) ? "3500" : port; + httpEndpoint = $"http://127.0.0.1:{port}"; + } + + return httpEndpoint; + } + + /// + /// Get the value of gRPC endpoint based off environment variables + /// + /// The value of gRPC endpoint based off environment variables + public static string GetDefaultGrpcEndpoint() + { + if (string.IsNullOrEmpty(grpcEndpoint)) + { + var endpoint = Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT"); + if (!string.IsNullOrEmpty(endpoint)) + { + grpcEndpoint = endpoint; + return grpcEndpoint; + } + + var port = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); + port = string.IsNullOrEmpty(port) ? "50001" : port; + grpcEndpoint = $"http://127.0.0.1:{port}"; + } + + return grpcEndpoint; + } +} diff --git a/src/Dapr.Client/DaprException.cs b/src/Dapr.Common/DaprException.cs similarity index 96% rename from src/Dapr.Client/DaprException.cs rename to src/Dapr.Common/DaprException.cs index e7b1efaba..2b600ef3a 100644 --- a/src/Dapr.Client/DaprException.cs +++ b/src/Dapr.Common/DaprException.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,7 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using System; using System.Runtime.Serialization; namespace Dapr diff --git a/src/Dapr.Common/DaprGenericClientBuilder.cs b/src/Dapr.Common/DaprGenericClientBuilder.cs new file mode 100644 index 000000000..10c072ec0 --- /dev/null +++ b/src/Dapr.Common/DaprGenericClientBuilder.cs @@ -0,0 +1,226 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Common; + +using System; +using System.Net.Http; +using System.Text.Json; +using Grpc.Net.Client; + +/// +/// Builder for building a generic Dapr client. +/// +public abstract class DaprGenericClientBuilder where TClientBuilder : class +{ + /// + /// Initializes a new instance of the class. + /// + protected DaprGenericClientBuilder() + { + this.GrpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); + this.HttpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(); + + this.GrpcChannelOptions = new GrpcChannelOptions() + { + // The gRPC client doesn't throw the right exception for cancellation + // by default, this switches that behavior on. + ThrowOperationCanceledOnCancellation = true, + }; + + this.JsonSerializerOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); + this.DaprApiToken = DaprDefaults.GetDefaultDaprApiToken(); + } + + /// + /// Property exposed for testing purposes. + /// + internal string GrpcEndpoint { get; private set; } + + /// + /// Property exposed for testing purposes. + /// + internal string HttpEndpoint { get; private set; } + + /// + /// Property exposed for testing purposes. + /// + internal Func? HttpClientFactory { get; set; } + + /// + /// Property exposed for testing purposes. + /// + internal JsonSerializerOptions JsonSerializerOptions { get; private set; } + + /// + /// Property exposed for testing purposes. + /// + internal GrpcChannelOptions GrpcChannelOptions { get; private set; } + + /// + /// Property exposed for testing purposes. + /// + internal string DaprApiToken { get; private set; } + /// + /// Property exposed for testing purposes. + /// + internal TimeSpan Timeout { get; private set; } + + /// + /// Overrides the HTTP endpoint used by the Dapr client for communicating with the Dapr runtime. + /// + /// + /// The URI endpoint to use for HTTP calls to the Dapr runtime. The default value will be + /// DAPR_HTTP_ENDPOINT first, or http://127.0.0.1:DAPR_HTTP_PORT as fallback + /// where DAPR_HTTP_ENDPOINT and DAPR_HTTP_PORT represents the value of the + /// corresponding environment variables. + /// + /// The instance. + public DaprGenericClientBuilder UseHttpEndpoint(string httpEndpoint) + { + ArgumentVerifier.ThrowIfNullOrEmpty(httpEndpoint, nameof(httpEndpoint)); + this.HttpEndpoint = httpEndpoint; + return this; + } + + /// + /// Specifies a factory used to create the to use. + /// + internal DaprGenericClientBuilder UseHttpClientFactory(Func factory) + { + this.HttpClientFactory = factory; + return this; + } + + /// + /// Overrides the legacy mechanism for building an HttpClient and uses the new + /// introduced in .NET Core 2.1. + /// + /// The factory used to create instances. + /// + public DaprGenericClientBuilder UseHttpClientFactory(IHttpClientFactory httpClientFactory) + { + this.HttpClientFactory = httpClientFactory.CreateClient; + return this; + } + + /// + /// Overrides the gRPC endpoint used by the Dapr client for communicating with the Dapr runtime. + /// + /// + /// The URI endpoint to use for gRPC calls to the Dapr runtime. The default value will be + /// http://127.0.0.1:DAPR_GRPC_PORT where DAPR_GRPC_PORT represents the value of the + /// DAPR_GRPC_PORT environment variable. + /// + /// The instance. + public DaprGenericClientBuilder UseGrpcEndpoint(string grpcEndpoint) + { + ArgumentVerifier.ThrowIfNullOrEmpty(grpcEndpoint, nameof(grpcEndpoint)); + this.GrpcEndpoint = grpcEndpoint; + return this; + } + + /// + /// + /// Uses the specified when serializing or deserializing using . + /// + /// + /// The default value is created using . + /// + /// + /// Json serialization options. + /// The instance. + public DaprGenericClientBuilder UseJsonSerializationOptions(JsonSerializerOptions options) + { + this.JsonSerializerOptions = options; + return this; + } + + /// + /// Uses the provided for creating the . + /// + /// The to use for creating the . + /// The instance. + public DaprGenericClientBuilder UseGrpcChannelOptions(GrpcChannelOptions grpcChannelOptions) + { + this.GrpcChannelOptions = grpcChannelOptions; + return this; + } + + /// + /// Adds the provided on every request to the Dapr runtime. + /// + /// The token to be added to the request headers/>. + /// The instance. + public DaprGenericClientBuilder UseDaprApiToken(string apiToken) + { + this.DaprApiToken = apiToken; + return this; + } + + /// + /// Sets the timeout for the HTTP client used by the Dapr client. + /// + /// + /// + public DaprGenericClientBuilder UseTimeout(TimeSpan timeout) + { + this.Timeout = timeout; + return this; + } + + /// + /// Builds out the inner DaprClient that provides the core shape of the + /// runtime gRPC client used by the consuming package. + /// + /// + protected (GrpcChannel channel, HttpClient httpClient, Uri httpEndpoint) BuildDaprClientDependencies() + { + var grpcEndpoint = new Uri(this.GrpcEndpoint); + if (grpcEndpoint.Scheme != "http" && grpcEndpoint.Scheme != "https") + { + throw new InvalidOperationException("The gRPC endpoint must use http or https."); + } + + if (grpcEndpoint.Scheme.Equals(Uri.UriSchemeHttp)) + { + // Set correct switch to make secure gRPC service calls. This switch must be set before creating the GrpcChannel. + AppContext.SetSwitch("System.Net.Http.SocketsHttpHandler.Http2UnencryptedSupport", true); + } + + var httpEndpoint = new Uri(this.HttpEndpoint); + if (httpEndpoint.Scheme != "http" && httpEndpoint.Scheme != "https") + { + throw new InvalidOperationException("The HTTP endpoint must use http or https."); + } + + var channel = GrpcChannel.ForAddress(this.GrpcEndpoint, this.GrpcChannelOptions); + + var httpClient = HttpClientFactory is not null ? HttpClientFactory() : new HttpClient(); + if (this.Timeout > TimeSpan.Zero) + { + httpClient.Timeout = this.Timeout; + } + + return (channel, httpClient, httpEndpoint); + } + + /// + /// Builds the client instance from the properties of the builder. + /// + /// The Dapr client instance. + /// + /// Builds the client instance from the properties of the builder. + /// + public abstract TClientBuilder Build(); +} diff --git a/src/Dapr.Client/Extensions/EnumExtensions.cs b/src/Dapr.Common/Extensions/EnumExtensions.cs similarity index 97% rename from src/Dapr.Client/Extensions/EnumExtensions.cs rename to src/Dapr.Common/Extensions/EnumExtensions.cs index df9c9ad33..a6eb5ab9f 100644 --- a/src/Dapr.Client/Extensions/EnumExtensions.cs +++ b/src/Dapr.Common/Extensions/EnumExtensions.cs @@ -11,12 +11,10 @@ // limitations under the License. // ------------------------------------------------------------------------ -#nullable enable -using System; using System.Reflection; using System.Runtime.Serialization; -namespace Dapr.Client +namespace Dapr.Common { internal static class EnumExtensions { diff --git a/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj b/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj index 71fd0153e..bfde0ee97 100644 --- a/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj +++ b/src/Dapr.Extensions.Configuration/Dapr.Extensions.Configuration.csproj @@ -1,13 +1,9 @@ - + enable - - - - Dapr Secret Store configuration provider implementation for Microsoft.Extensions.Configuration. @@ -18,7 +14,7 @@ - + \ No newline at end of file diff --git a/src/Dapr.Jobs/AssemblyInfo.cs b/src/Dapr.Jobs/AssemblyInfo.cs new file mode 100644 index 000000000..870a8dde4 --- /dev/null +++ b/src/Dapr.Jobs/AssemblyInfo.cs @@ -0,0 +1,16 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Dapr.Jobs.Test, PublicKey=0024000004800000940000000602000000240000525341310004000001000100b1f597635c44597fcecb493e2b1327033b29b1a98ac956a1a538664b68f87d45fbaada0438a15a6265e62864947cc067d8da3a7d93c5eb2fcbb850e396c8684dba74ea477d82a1bbb18932c0efb30b64ff1677f85ae833818707ac8b49ad8062ca01d2c89d8ab1843ae73e8ba9649cd28666b539444dcdee3639f95e2a099bb2" )] diff --git a/src/Dapr.Jobs/CronExpressionBuilder.cs b/src/Dapr.Jobs/CronExpressionBuilder.cs new file mode 100644 index 000000000..85781c4eb --- /dev/null +++ b/src/Dapr.Jobs/CronExpressionBuilder.cs @@ -0,0 +1,475 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Runtime.Serialization; +using System.Text.RegularExpressions; +using Dapr.Common; +using ArgumentException = System.ArgumentException; +using ArgumentOutOfRangeException = System.ArgumentOutOfRangeException; + +namespace Dapr.Jobs; + +/// +/// A fluent API used to build a valid Cron expression. +/// +public sealed class CronExpressionBuilder +{ + private const string SecondsAndMinutesRegexText = @"([0-5]?\d-[0-5]?\d)|([0-5]?\d,?)|(\*(\/[0-5]?\d)?)"; + private const string HoursRegexText = @"(([0-1]?\d)|(2[0-3])-([0-1]?\d)|(2[0-3]))|(([0-1]?\d)|(2[0-3]),?)|(\*(\/([0-1]?\d)|(2[0-3]))?)"; + private const string DayOfMonthRegexText = @"\*|(\*\/(([0-2]?\d)|(3[0-1])))|(((([0-2]?\d)|(3[0-1]))(-(([0-2]?\d)|(3[0-1])))?))"; + private const string MonthRegexText = @"(^(\*\/)?((0?\d)|(1[0-2]))$)|(^\*$)|(^((0?\d)|(1[0-2]))(-((0?\d)|(1[0-2]))?)$)|(^(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(?:-(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?(?:,(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(?:-(?:JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC))?)*$)"; + private const string DayOfWeekRegexText = @"\*|(\*\/(0?[0-6])|(0?[0-6](-0?[0-6])?)|((,?(SUN|MON|TUE|WED|THU|FRI|SAT))+)|((SUN|MON|TUE|WED|THU|FRI|SAT)(-(SUN|MON|TUE|WED|THU|FRI|SAT))?))"; + + private static readonly Regex cronExpressionRegex = + new( + $"{SecondsAndMinutesRegexText} {SecondsAndMinutesRegexText} {HoursRegexText} {DayOfMonthRegexText} {MonthRegexText} {DayOfWeekRegexText}", RegexOptions.Compiled); + + private string seconds = "*"; + private string minutes = "*"; + private string hours = "*"; + private string dayOfMonth = "*"; + private string month = "*"; + private string dayOfWeek = "*"; + + /// + /// Reflects an expression in which the developer specifies a series of numeric values and the period they're associated + /// with indicating when the trigger should occur. + /// + /// The period of time within which the values should be associated. + /// The numerical values of the time period on which the schedule should trigger. + /// + public CronExpressionBuilder On(OnCronPeriod period, params int[] values) + { + switch (period) + { + //Validate by period + case OnCronPeriod.Second or OnCronPeriod.Minute or OnCronPeriod.Hour when values.Any(a => a is < 0 or > 59): + throw new ArgumentOutOfRangeException(nameof(values), "All values must be within 0 and 59, inclusively."); + case OnCronPeriod.DayOfMonth when values.Any(a => a is < 0 or > 31): + throw new ArgumentOutOfRangeException(nameof(values), "All values must be within 1 and 31, inclusively."); + } + + var strValue = string.Join(',', values.Distinct().OrderBy(a => a)); + + switch (period) + { + case OnCronPeriod.Second: + seconds = strValue; + break; + case OnCronPeriod.Minute: + minutes = strValue; + break; + case OnCronPeriod.Hour: + hours = strValue; + break; + case OnCronPeriod.DayOfMonth: + dayOfMonth = strValue; + break; + } + + return this; + } + + /// + /// Reflects an expression in which the developer specifies a series of months in the year on which the trigger should occur. + /// + /// The months of the year to invoke the trigger on. + public CronExpressionBuilder On(params MonthOfYear[] months) + { + month = string.Join(',', months.Distinct().OrderBy(a => a).Select(a => a.GetValueFromEnumMember())); + return this; + } + + /// + /// Reflects an expression in which the developer specifies a series of days of the week on which the trigger should occur. + /// + /// The days of the week to invoke the trigger on. + public CronExpressionBuilder On(params DayOfWeek[] days) + { + dayOfWeek = string.Join(',', days.Distinct().OrderBy(a => a).Select(a => a.GetValueFromEnumMember())); + return this; + } + + /// + /// Reflects an expression in which the developer defines bounded range of numerical values for the specified period. + /// + /// The period of time within which the values should be associated. + /// The start of the range. + /// The end of the range. + public CronExpressionBuilder Through(ThroughCronPeriod period, int from, int to) + { + if (from > to) + throw new ArgumentException("The date representing the From property should precede the To property"); + if (from == to) + throw new ArgumentException("The From and To properties should not be equivalent"); + + var stringValue = $"{from}-{to}"; + + switch (period) + { + case ThroughCronPeriod.Second: + seconds = stringValue; + break; + case ThroughCronPeriod.Minute: + minutes = stringValue; + break; + case ThroughCronPeriod.Hour: + hours = stringValue; + break; + case ThroughCronPeriod.DayOfMonth: + dayOfMonth = stringValue; + break; + case ThroughCronPeriod.Month: + month = stringValue; + break; + } + + return this; + } + + /// + /// Reflects an expression in which the developer defines a bounded range of days. + /// + /// The start of the range. + /// The end of the range. + public CronExpressionBuilder Through(DayOfWeek from, DayOfWeek to) + { + if (from > to) + throw new ArgumentException("The day representing the From property should precede the To property"); + if (from == to) + throw new ArgumentException("The From and To properties should not be equivalent"); + + dayOfWeek = $"{from.GetValueFromEnumMember()}-{to.GetValueFromEnumMember()}"; + return this; + } + + /// + /// Reflects an expression in which the developer defines a bounded range of months. + /// + /// The start of the range. + /// The end of the range. + public CronExpressionBuilder Through(MonthOfYear from, MonthOfYear to) + { + if (from > to) + throw new ArgumentException("The month representing the From property should precede the To property"); + if (from == to) + throw new ArgumentException("The From and To properties should not be equivalent"); + + month = $"{from.GetValueFromEnumMember()}-{to.GetValueFromEnumMember()}"; + return this; + } + + /// + /// Reflects an expression in which the trigger should happen each time the value of the specified period changes. + /// + /// The period of time that should be evaluated. + /// + public CronExpressionBuilder Each(CronPeriod period) + { + switch (period) + { + case CronPeriod.Second: + seconds = "*"; + break; + case CronPeriod.Minute: + minutes = "*"; + break; + case CronPeriod.Hour: + hours = "*"; + break; + case CronPeriod.DayOfMonth: + dayOfMonth = "*"; + break; + case CronPeriod.Month: + month = "*"; + break; + case CronPeriod.DayOfWeek: + dayOfWeek = "*"; + break; + } + + return this; + } + + /// + /// Reflects an expression in which the trigger should happen at a regular interval of the specified period type. + /// + /// The length of time represented in a unit interval. + /// The number of period units that should elapse between each trigger. + /// + public CronExpressionBuilder Every(EveryCronPeriod period, int interval) + { + if (interval < 0) + throw new ArgumentOutOfRangeException(nameof(interval)); + + var value = $"*/{interval}"; + + switch (period) + { + case EveryCronPeriod.Second: + seconds = value; + break; + case EveryCronPeriod.Minute: + minutes = value; + break; + case EveryCronPeriod.Hour: + hours = value; + break; + case EveryCronPeriod.Month: + month = value; + break; + case EveryCronPeriod.DayInMonth: + dayOfMonth = value; + break; + case EveryCronPeriod.DayInWeek: + dayOfWeek = value; + break; + } + + return this; + } + + /// + /// Validates whether a given expression is valid Cron syntax. + /// + /// The string to evaluate. + /// True if the expression is valid Cron syntax; false if not. + internal static bool IsCronExpression(string expression) => expression.Split(' ').Length == 6 && cronExpressionRegex.IsMatch(expression); + + /// + /// Builds the Cron expression. + /// + /// + public override string ToString() => $"{seconds} {minutes} {hours} {dayOfMonth} {month} {dayOfWeek}"; +} + +/// +/// Identifies the valid Cron periods in an "On" expression. +/// +public enum OnCronPeriod +{ + /// + /// Identifies the second value for an "On" expression. + /// + Second, + /// + /// Identifies the minute value for an "On" expression. + /// + Minute, + /// + /// Identifies the hour value for an "On" expression. + /// + Hour, + /// + /// Identifies the day in the month for an "On" expression. + /// + DayOfMonth +} + +/// +/// Identifies the valid Cron periods in an "Every" expression. +/// +public enum EveryCronPeriod +{ + /// + /// Identifies the second value in an "Every" expression. + /// + Second, + /// + /// Identifies the minute value in an "Every" expression. + /// + Minute, + /// + /// Identifies the hour value in an "Every" expression. + /// + Hour, + /// + /// Identifies the month value in an "Every" expression. + /// + Month, + /// + /// Identifies the days in the month value in an "Every" expression. + /// + DayInMonth, + /// + /// Identifies the days in the week value in an "Every" expression. + /// + DayInWeek, +} + +/// +/// Identifies the various Cron periods valid to use in a "Through" expression. +/// +public enum ThroughCronPeriod +{ + /// + /// Identifies the second value in the Cron expression. + /// + Second, + /// + /// Identifies the minute value in the Cron expression. + /// + Minute, + /// + /// Identifies the hour value in the Cron expression. + /// + Hour, + /// + /// Identifies the day of month value in the Cron expression. + /// + DayOfMonth, + /// + /// Identifies the month value in the Cron expression. + /// + Month +} + +/// +/// Identifies the various Cron periods. +/// +public enum CronPeriod +{ + /// + /// Identifies the second value in the Cron expression. + /// + Second, + /// + /// Identifies the minute value in the Cron expression. + /// + Minute, + /// + /// Identifies the hour value in the Cron expression. + /// + Hour, + /// + /// Identifies the day of month value in the Cron expression. + /// + DayOfMonth, + /// + /// Identifies the month value in the Cron expression. + /// + Month, + /// + /// Identifies the day of week value in the Cron expression. + /// + DayOfWeek +} + +/// +/// Identifies the days in the week. +/// +public enum DayOfWeek +{ + /// + /// Sunday. + /// + [EnumMember(Value = "SUN")] + Sunday = 0, + /// + /// Monday. + /// + [EnumMember(Value = "MON")] + Monday = 1, + /// + /// Tuesday. + /// + [EnumMember(Value = "TUE")] + Tuesday = 2, + /// + /// Wednesday. + /// + [EnumMember(Value = "WED")] + Wednesday = 3, + /// + /// Thursday. + /// + [EnumMember(Value = "THU")] + Thursday = 4, + /// + /// Friday. + /// + [EnumMember(Value = "FRI")] + Friday = 5, + /// + /// Saturday. + /// + [EnumMember(Value = "SAT")] + Saturday = 6 +} + +/// +/// Identifies the months in the year. +/// +public enum MonthOfYear +{ + /// + /// Month of January. + /// + [EnumMember(Value = "JAN")] + January = 1, + /// + /// Month of February. + /// + [EnumMember(Value = "FEB")] + February = 2, + /// + /// Month of March. + /// + [EnumMember(Value = "MAR")] + March = 3, + /// + /// Month of April. + /// + [EnumMember(Value = "APR")] + April = 4, + /// + /// Month of May. + /// + [EnumMember(Value = "MAY")] + May = 5, + /// + /// Month of June. + /// + [EnumMember(Value = "JUN")] + June = 6, + /// + /// Month of July. + /// + [EnumMember(Value = "JUL")] + July = 7, + /// + /// Month of August. + /// + [EnumMember(Value = "AUG")] + August = 8, + /// + /// Month of September. + /// + [EnumMember(Value = "SEP")] + September = 9, + /// + /// Month of October. + /// + [EnumMember(Value = "OCT")] + October = 10, + /// + /// Month of November. + /// + [EnumMember(Value = "NOV")] + November = 11, + /// + /// Month of December. + /// + [EnumMember(Value = "DEC")] + December = 12 +} diff --git a/src/Dapr.Jobs/Dapr.Jobs.csproj b/src/Dapr.Jobs/Dapr.Jobs.csproj new file mode 100644 index 000000000..d7b12ace2 --- /dev/null +++ b/src/Dapr.Jobs/Dapr.Jobs.csproj @@ -0,0 +1,35 @@ + + + + net6;net8 + enable + enable + Dapr.Jobs + Dapr Jobs Authoring SDK + Dapr Jobs SDK for scheduling jobs and tasks with Dapr + alpha + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Dapr.Jobs/DaprJobsClient.cs b/src/Dapr.Jobs/DaprJobsClient.cs new file mode 100644 index 000000000..f6e885ed7 --- /dev/null +++ b/src/Dapr.Jobs/DaprJobsClient.cs @@ -0,0 +1,97 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Jobs.Models; +using Dapr.Jobs.Models.Responses; + +namespace Dapr.Jobs; + +/// +/// +/// Defines client operations for managing Dapr jobs. +/// Use to create a or register +/// for use with dependency injection via +/// DaprJobsServiceCollectionExtensions.AddDaprJobsClient. +/// +/// +/// Implementations of implement because the +/// client accesses network resources. For best performance, create a single long-lived client instance +/// and share it for the lifetime of the application. This is done for you if created via the DI extensions. Avoid +/// creating a disposing a client instance for each operation that the application performs - this can lead to socket +/// exhaustion and other problems. +/// +/// +public abstract class DaprJobsClient : IDisposable +{ + private bool disposed; + + /// + /// Schedules a job with Dapr. + /// + /// The name of the job being scheduled. + /// The schedule defining when the job will be triggered. + /// The main payload of the job. + /// The optional point-in-time from which the job schedule should start. + /// The optional number of times the job should be triggered. + /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// Cancellation token. + [Obsolete( + "The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task ScheduleJobAsync(string jobName, DaprJobSchedule schedule, + ReadOnlyMemory? payload = null, DateTimeOffset? startingFrom = null, int? repeats = null, + DateTimeOffset? ttl = null, + CancellationToken cancellationToken = default); + + /// + /// Retrieves the details of a registered job. + /// + /// The jobName of the job. + /// Cancellation token. + /// The details comprising the job. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task GetJobAsync(string jobName, CancellationToken cancellationToken = default); + + /// + /// Deletes the specified job. + /// + /// The jobName of the job. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public abstract Task DeleteJobAsync(string jobName, CancellationToken cancellationToken = default); + + internal static KeyValuePair? GetDaprApiTokenHeader(string apiToken) + { + if (string.IsNullOrWhiteSpace(apiToken)) + return null; + + return new KeyValuePair("dapr-api-token", apiToken); + } + + /// + public void Dispose() + { + if (!this.disposed) + { + Dispose(disposing: true); + this.disposed = true; + } + } + + /// + /// Disposes the resources associated with the object. + /// + /// true if called by a call to the Dispose method; otherwise false. + protected virtual void Dispose(bool disposing) + { + } +} diff --git a/src/Dapr.Jobs/DaprJobsClientBuilder.cs b/src/Dapr.Jobs/DaprJobsClientBuilder.cs new file mode 100644 index 000000000..390d52236 --- /dev/null +++ b/src/Dapr.Jobs/DaprJobsClientBuilder.cs @@ -0,0 +1,37 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Dapr.Common; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Jobs; + +/// +/// Builds a . +/// +public sealed class DaprJobsClientBuilder : DaprGenericClientBuilder +{ + /// + /// Builds the client instance from the properties of the builder. + /// + /// The Dapr client instance. + public override DaprJobsClient Build() + { + var daprClientDependencies = this.BuildDaprClientDependencies(); + + var client = new Autogenerated.Dapr.DaprClient(daprClientDependencies.channel); + var apiTokenHeader = this.DaprApiToken is not null ? DaprJobsClient.GetDaprApiTokenHeader(this.DaprApiToken) : null; + + return new DaprJobsGrpcClient(client, daprClientDependencies.httpClient, apiTokenHeader); + } +} diff --git a/src/Dapr.Jobs/DaprJobsGrpcClient.cs b/src/Dapr.Jobs/DaprJobsGrpcClient.cs new file mode 100644 index 000000000..b2fee1028 --- /dev/null +++ b/src/Dapr.Jobs/DaprJobsGrpcClient.cs @@ -0,0 +1,236 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Net.Http.Headers; +using System.Reflection; +using Dapr.Jobs.Models; +using Dapr.Jobs.Models.Responses; +using Google.Protobuf; +using Google.Protobuf.WellKnownTypes; +using Grpc.Core; +using Autogenerated = Dapr.Client.Autogen.Grpc.v1; + +namespace Dapr.Jobs; + +/// +/// A client for interacting with the Dapr endpoints. +/// +internal sealed class DaprJobsGrpcClient : DaprJobsClient +{ + /// + /// Present only for testing purposes. + /// + internal readonly HttpClient httpClient; + + /// + /// Used to populate options headers with API token value. + /// + internal readonly KeyValuePair? apiTokenHeader; + + private readonly Autogenerated.Dapr.DaprClient client; + private readonly string userAgent = UserAgent().ToString(); + + // property exposed for testing purposes + internal Autogenerated.Dapr.DaprClient Client => client; + + internal DaprJobsGrpcClient( + Autogenerated.Dapr.DaprClient innerClient, + HttpClient httpClient, + KeyValuePair? apiTokenHeader) + { + this.client = innerClient; + this.httpClient = httpClient; + this.apiTokenHeader = apiTokenHeader; + + this.httpClient.DefaultRequestHeaders.UserAgent.Add(UserAgent()); + } + + /// + /// Schedules a job with Dapr. + /// + /// The name of the job being scheduled. + /// The schedule defining when the job will be triggered. + /// The main payload of the job. + /// The optional point-in-time from which the job schedule should start. + /// The optional number of times the job should be triggered. + /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task ScheduleJobAsync(string jobName, DaprJobSchedule schedule, + ReadOnlyMemory? payload = null, DateTimeOffset? startingFrom = null, int? repeats = null, + DateTimeOffset? ttl = null, + CancellationToken cancellationToken = default) + { + ArgumentVerifier.ThrowIfNullOrEmpty(jobName, nameof(jobName)); + ArgumentNullException.ThrowIfNull(schedule, nameof(schedule)); + + var job = new Autogenerated.Job { Name = jobName, Schedule = schedule.ExpressionValue }; + + if (startingFrom is not null) + job.DueTime = ((DateTimeOffset)startingFrom).ToString("O"); + + if (repeats is not null) + { + if (repeats < 0) + throw new ArgumentOutOfRangeException(nameof(repeats)); + + job.Repeats = (uint)repeats; + } + + if (payload is not null) + job.Data = new Any { Value = ByteString.CopyFrom(payload.Value.Span), TypeUrl = "dapr.io/schedule/jobpayload" }; + + if (ttl is not null) + { + if (ttl <= startingFrom) + throw new ArgumentException( + $"When both {nameof(ttl)} and {nameof(startingFrom)} are specified, {nameof(ttl)} must represent a later point in time"); + + job.Ttl = ((DateTimeOffset)ttl).ToString("O"); + } + + var envelope = new Autogenerated.ScheduleJobRequest { Job = job }; + + var callOptions = CreateCallOptions(headers: null, cancellationToken); + + try + { + await client.ScheduleJobAlpha1Async(envelope, callOptions); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + //Ignore our own cancellation + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled && cancellationToken.IsCancellationRequested) + { + // Ignore a remote cancellation due to our own cancellation + } + catch (Exception ex) + { + throw new DaprException( + "Schedule job operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); + } + } + + /// + /// Retrieves the details of a registered job. + /// + /// The name of the job. + /// Cancellation token. + /// The details comprising the job. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task GetJobAsync(string jobName, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(jobName)) + throw new ArgumentNullException(nameof(jobName)); + + try + { + var envelope = new Autogenerated.GetJobRequest { Name = jobName }; + var callOptions = CreateCallOptions(headers: null, cancellationToken); + var response = await client.GetJobAlpha1Async(envelope, callOptions); + return new DaprJobDetails(new DaprJobSchedule(response.Job.Schedule)) + { + DueTime = response.Job.DueTime is not null ? DateTime.Parse(response.Job.DueTime) : null, + Ttl = response.Job.Ttl is not null ? DateTime.Parse(response.Job.Ttl) : null, + RepeatCount = response.Job.Repeats == default ? null : (int?)response.Job.Repeats, + Payload = response.Job.Data.ToByteArray() + }; + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + //Ignore our own cancellation + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled && cancellationToken.IsCancellationRequested) + { + // Ignore a remote cancellation due to our own cancellation + } + catch (Exception ex) + { + throw new DaprException( + "Get job operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", ex); + } + + throw new DaprException("Get job operation failed: the Dapr endpoint did not return the expected value."); + } + + /// + /// Deletes the specified job. + /// + /// The name of the job. + /// Cancellation token. + /// + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public override async Task DeleteJobAsync(string jobName, CancellationToken cancellationToken = default) + { + if (string.IsNullOrWhiteSpace(jobName)) + throw new ArgumentNullException(nameof(jobName)); + + try + { + var envelope = new Autogenerated.DeleteJobRequest { Name = jobName }; + var callOptions = CreateCallOptions(headers: null, cancellationToken); + await client.DeleteJobAlpha1Async(envelope, callOptions); + } + catch (OperationCanceledException) when (cancellationToken.IsCancellationRequested) + { + //Ignore our own cancellation + } + catch (RpcException ex) when (ex.StatusCode == StatusCode.Cancelled && cancellationToken.IsCancellationRequested) + { + // Ignore a remote cancellation due to our own cancellation + } + catch (Exception ex) + { + throw new DaprException( + "Delete job operation failed: the Dapr endpoint indicated a failure. See InnerException for details.", + ex); + } + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + this.httpClient.Dispose(); + } + } + + private CallOptions CreateCallOptions(Metadata? headers, CancellationToken cancellationToken) + { + var callOptions = new CallOptions(headers: headers ?? new Metadata(), cancellationToken: cancellationToken); + + callOptions.Headers!.Add("User-Agent", this.userAgent); + + if (apiTokenHeader is not null) + callOptions.Headers.Add(apiTokenHeader.Value.Key, apiTokenHeader.Value.Value); + + return callOptions; + } + + /// + /// Returns the value for the User-Agent. + /// + /// A containing the value to use for the User-Agent. + private static ProductInfoHeaderValue UserAgent() + { + var assembly = typeof(DaprJobsClient).Assembly; + var assemblyVersion = assembly + .GetCustomAttributes() + .FirstOrDefault()? + .InformationalVersion; + + return new ProductInfoHeaderValue("dapr-sdk-dotnet", $"v{assemblyVersion}"); + } +} diff --git a/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs new file mode 100644 index 000000000..67e718985 --- /dev/null +++ b/src/Dapr.Jobs/Extensions/DaprJobsServiceCollectionExtensions.cs @@ -0,0 +1,78 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyInjection.Extensions; + +namespace Dapr.Jobs.Extensions; + +/// +/// Contains extension methods for using Dapr Jobs with dependency injection. +/// +public static class DaprJobsServiceCollectionExtensions +{ + /// + /// Adds Dapr Jobs client support to the service collection. + /// + /// The . + /// Optionally allows greater configuration of the . + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure = null) + { + ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); + + //Register the IHttpClientFactory implementation + serviceCollection.AddHttpClient(); + + serviceCollection.TryAddSingleton(serviceProvider => + { + var httpClientFactory = serviceProvider.GetRequiredService(); + + var builder = new DaprJobsClientBuilder(); + builder.UseHttpClientFactory(httpClientFactory); + + configure?.Invoke(builder); + + return builder.Build(); + }); + + return serviceCollection; + } + + /// + /// Adds Dapr Jobs client support to the service collection. + /// + /// The . + /// Optionally allows greater configuration of the using injected services. + /// + public static IServiceCollection AddDaprJobsClient(this IServiceCollection serviceCollection, Action? configure) + { + ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); + + //Register the IHttpClientFactory implementation + serviceCollection.AddHttpClient(); + + serviceCollection.TryAddSingleton(serviceProvider => + { + var httpClientFactory = serviceProvider.GetRequiredService(); + + var builder = new DaprJobsClientBuilder(); + builder.UseHttpClientFactory(httpClientFactory); + + configure?.Invoke(serviceProvider, builder); + + return builder.Build(); + }); + + return serviceCollection; + } +} diff --git a/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs b/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs new file mode 100644 index 000000000..1f02a32cd --- /dev/null +++ b/src/Dapr.Jobs/Extensions/DaprSerializationExtensions.cs @@ -0,0 +1,78 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text; +using System.Text.Json; +using Dapr.Jobs.Models; + +namespace Dapr.Jobs.Extensions; + +/// +/// Provides helper extensions for performing serialization operations when scheduling one-time Cron jobs for the developer. +/// +public static class DaprJobsSerializationExtensions +{ + /// + /// Default JSON serializer options. + /// + private static readonly JsonSerializerOptions defaultOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web); + + /// + /// Schedules a job with Dapr. + /// + /// The instance. + /// The name of the job being scheduled. + /// The schedule defining when the job will be triggered. + /// The main payload of the job expressed as a JSON-serializable object. + /// The optional point-in-time from which the job schedule should start. + /// The optional number of times the job should be triggered. + /// Optional JSON serialization options. + /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public static async Task ScheduleJobWithPayloadAsync(this DaprJobsClient client, string jobName, DaprJobSchedule schedule, + object payload, DateTime? startingFrom = null, int? repeats = null, JsonSerializerOptions? jsonSerializerOptions = null, DateTimeOffset? ttl = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(payload, nameof(payload)); + + var serializerOptions = jsonSerializerOptions ?? defaultOptions; + var payloadBytes = + JsonSerializer.SerializeToUtf8Bytes(payload, serializerOptions); + + await client.ScheduleJobAsync(jobName, schedule, payloadBytes, startingFrom, repeats, ttl, cancellationToken); + } + + /// + /// Schedules a job with Dapr. + /// + /// The instance. + /// The name of the job being scheduled. + /// The schedule defining when the job will be triggered. + /// The main payload of the job expressed as a string. + /// The optional point-in-time from which the job schedule should start. + /// The optional number of times the job should be triggered. + /// Represents when the job should expire. If both this and DueTime are set, TTL needs to represent a later point in time. + /// Cancellation token. + [Obsolete("The API is currently not stable as it is in the Alpha stage. This attribute will be removed once it is stable.")] + public static async Task ScheduleJobWithPayloadAsync(this DaprJobsClient client, string jobName, DaprJobSchedule schedule, + string payload, DateTime? startingFrom = null, int? repeats = null, DateTimeOffset? ttl = null, + CancellationToken cancellationToken = default) + { + ArgumentNullException.ThrowIfNull(payload, nameof(payload)); + + var payloadBytes = Encoding.UTF8.GetBytes(payload); + + await client.ScheduleJobAsync(jobName, schedule, payloadBytes, startingFrom, repeats, ttl, cancellationToken); + } +} diff --git a/src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs b/src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs new file mode 100644 index 000000000..bbf4417b5 --- /dev/null +++ b/src/Dapr.Jobs/Extensions/EndpointRouteBuilderExtensions.cs @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using Microsoft.AspNetCore.Builder; +using Microsoft.AspNetCore.Routing; + +namespace Dapr.Jobs.Extensions; + +/// +/// Provides extension methods to register endpoints for Dapr Job Scheduler invocations. +/// +public static class EndpointRouteBuilderExtensions +{ + /// + /// Adds a to the that registers a + /// Dapr scheduled job trigger invocation. + /// + /// The to add the route to. + /// The name of the job that should trigger this method when invoked. + /// The delegate executed when the endpoint is matched. + /// + public static IEndpointConventionBuilder MapDaprScheduledJob(this IEndpointRouteBuilder endpoints, string jobName, + Delegate handler) + { + ArgumentNullException.ThrowIfNull(endpoints, nameof(endpoints)); + ArgumentNullException.ThrowIfNull(jobName, nameof(jobName)); + ArgumentNullException.ThrowIfNull(handler, nameof(handler)); + + return endpoints.MapPost($"/job/{jobName}", handler); + } +} diff --git a/src/Dapr.Jobs/Extensions/StringExtensions.cs b/src/Dapr.Jobs/Extensions/StringExtensions.cs new file mode 100644 index 000000000..98e3525de --- /dev/null +++ b/src/Dapr.Jobs/Extensions/StringExtensions.cs @@ -0,0 +1,27 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Jobs.Extensions; + +internal static class StringExtensions +{ + /// + /// Extension method that validates a string against a list of possible matches. + /// + /// The string value to evaluate. + /// The possible values to look for a match within. + /// The type of string comparison to perform. + /// True if the value ends with any of the possible values; otherwise false. + public static bool EndsWithAny(this string value, IReadOnlyList possibleValues, + StringComparison comparisonType) => possibleValues.Any(val => value.EndsWith(val, comparisonType)); +} diff --git a/src/Dapr.Jobs/Extensions/TimeSpanExtensions.cs b/src/Dapr.Jobs/Extensions/TimeSpanExtensions.cs new file mode 100644 index 000000000..2ebbd8086 --- /dev/null +++ b/src/Dapr.Jobs/Extensions/TimeSpanExtensions.cs @@ -0,0 +1,101 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text; +using System.Text.RegularExpressions; + +namespace Dapr.Jobs; + +/// +/// Provides extension methods used with . +/// +internal static class TimeSpanExtensions +{ + private static readonly Regex hourRegex = new Regex(@"(\d+)h", RegexOptions.Compiled); + private static readonly Regex minuteRegex = new Regex(@"(\d+)m", RegexOptions.Compiled); + private static readonly Regex secondRegex = new Regex(@"(\d+)s", RegexOptions.Compiled); + private static readonly Regex millisecondRegex = new Regex(@"(\d+)q", RegexOptions.Compiled); + + /// + /// Creates a duration string that matches the specification at https://pkg.go.dev/time#ParseDuration per the + /// Jobs API specification https://v1-14.docs.dapr.io/reference/api/jobs_api/#schedule-a-job. + /// + /// The timespan being evaluated. + /// + public static string ToDurationString(this TimeSpan timespan) + { + var sb = new StringBuilder(); + + //Hours is the largest unit of measure in the duration string + if (timespan.Hours > 0) + sb.Append($"{timespan.Hours}h"); + + if (timespan.Minutes > 0) + sb.Append($"{timespan.Minutes}m"); + + if (timespan.Seconds > 0) + sb.Append($"{timespan.Seconds}s"); + + if (timespan.Milliseconds > 0) + sb.Append($"{timespan.Milliseconds}ms"); + + return sb.ToString(); + } + + /// + /// Validates whether a given string represents a parseable Golang duration string. + /// + /// The duration string to parse. + /// True if the string represents a parseable interval duration; false if not. + public static bool IsDurationString(this string interval) + { + interval = interval.Replace("ms", "q"); + return hourRegex.Match(interval).Success || + minuteRegex.Match(interval).Success || + secondRegex.Match(interval).Success || + millisecondRegex.Match(interval).Success; + } + + /// + /// Creates a given a Golang duration string. + /// + /// The duration string to parse. + /// A timespan value. + public static TimeSpan FromDurationString(this string interval) + { + interval = interval.Replace("ms", "q"); + + int hours = 0; + int minutes = 0; + int seconds = 0; + int milliseconds = 0; + + var hourMatch = hourRegex.Match(interval); + if (hourMatch.Success) + hours = int.Parse(hourMatch.Groups[1].Value); + + var minuteMatch = minuteRegex.Match(interval); + if (minuteMatch.Success) + minutes = int.Parse(minuteMatch.Groups[1].Value); + + var secondMatch = secondRegex.Match(interval); + if (secondMatch.Success) + seconds = int.Parse(secondMatch.Groups[1].Value); + + var millisecondMatch = millisecondRegex.Match(interval); + if (millisecondMatch.Success) + milliseconds = int.Parse(millisecondMatch.Groups[1].Value); + + return new TimeSpan(0, hours, minutes, seconds, milliseconds); + } +} diff --git a/src/Dapr.Jobs/Models/DaprJobSchedule.cs b/src/Dapr.Jobs/Models/DaprJobSchedule.cs new file mode 100644 index 000000000..1fc0f63b9 --- /dev/null +++ b/src/Dapr.Jobs/Models/DaprJobSchedule.cs @@ -0,0 +1,143 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System.Text.RegularExpressions; +using Dapr.Jobs.Extensions; + +namespace Dapr.Jobs.Models; + +/// +/// Used to build a schedule for a job. +/// +public sealed class DaprJobSchedule +{ + /// + /// A regular expression used to evaluate whether a given prefix period embodies an @every statement. + /// + private readonly Regex isEveryExpression = new(@"^@every (\d+(m?s|m|h))+$", RegexOptions.Compiled); + /// + /// The various prefixed period values allowed. + /// + private readonly string[] acceptablePeriodValues = { "yearly", "monthly", "weekly", "daily", "midnight", "hourly" }; + + /// + /// The value of the expression represented by the schedule. + /// + public string ExpressionValue { get; } + + /// + /// Initializes the value of based on the provided value from each of the factory methods. + /// + /// + /// Developers are intended to create a new using the provided static factory methods. + /// + /// The value of the scheduling expression. + internal DaprJobSchedule(string expressionValue) + { + ExpressionValue = expressionValue; + } + + /// + /// Specifies a schedule built using the fluent Cron expression builder. + /// + /// The fluent Cron expression builder. + public static DaprJobSchedule FromCronExpression(CronExpressionBuilder builder) + { + ArgumentNullException.ThrowIfNull(builder, nameof(builder)); + return new DaprJobSchedule(builder.ToString()); + } + + /// + /// Specifies a single point in time. + /// + /// The date and time when the job should be triggered. + /// + public static DaprJobSchedule FromDateTime(DateTimeOffset scheduledTime) + { + ArgumentNullException.ThrowIfNull(scheduledTime, nameof(scheduledTime)); + return new DaprJobSchedule(scheduledTime.ToString("O")); + } + + /// + /// Specifies a schedule using a Cron-like expression or '@' prefixed period strings. + /// + /// The systemd Cron-like expression indicating when the job should be triggered. + public static DaprJobSchedule FromExpression(string expression) + { + ArgumentVerifier.ThrowIfNullOrEmpty(expression, nameof(expression)); + return new DaprJobSchedule(expression); + } + + /// + /// Specifies a schedule using a duration interval articulated via a . + /// + /// The duration interval. + public static DaprJobSchedule FromDuration(TimeSpan duration) + { + ArgumentNullException.ThrowIfNull(duration, nameof(duration)); + return new DaprJobSchedule(duration.ToDurationString()); + } + + /// + /// Specifies a schedule in which the job is triggered to run once a year. + /// + public static DaprJobSchedule Yearly { get; } = new DaprJobSchedule("@yearly"); + + /// + /// Specifies a schedule in which the job is triggered monthly. + /// + public static DaprJobSchedule Monthly { get; } = new DaprJobSchedule("@monthly"); + + /// + /// Specifies a schedule in which the job is triggered weekly. + /// + public static DaprJobSchedule Weekly { get; } =new DaprJobSchedule("@weekly"); + + /// + /// Specifies a schedule in which the job is triggered daily. + /// + public static DaprJobSchedule Daily { get; } = new DaprJobSchedule("@daily"); + + /// + /// Specifies a schedule in which the job is triggered once a day at midnight. + /// + public static DaprJobSchedule Midnight { get; } = new DaprJobSchedule("@midnight"); + + /// + /// Specifies a schedule in which the job is triggered at the top of every hour. + /// + public static DaprJobSchedule Hourly { get; } = new DaprJobSchedule("@hourly"); + + /// + /// Reflects that the schedule represents a prefixed period expression. + /// + public bool IsPrefixedPeriodExpression => + ExpressionValue.StartsWith('@') && + (isEveryExpression.IsMatch(ExpressionValue) || + ExpressionValue.EndsWithAny(acceptablePeriodValues, StringComparison.InvariantCulture)); + + /// + /// Reflects that the schedule represents a fixed point in time. + /// + public bool IsPointInTimeExpression => DateTimeOffset.TryParse(ExpressionValue, out _); + + /// + /// Reflects that the schedule represents a Golang duration expression. + /// + public bool IsDurationExpression => ExpressionValue.IsDurationString(); + + /// + /// Reflects that the schedule represents a Cron expression. + /// + public bool IsCronExpression => CronExpressionBuilder.IsCronExpression(ExpressionValue); +} diff --git a/src/Dapr.Jobs/Models/Responses/DaprJobDetails.cs b/src/Dapr.Jobs/Models/Responses/DaprJobDetails.cs new file mode 100644 index 000000000..e40b0c4b3 --- /dev/null +++ b/src/Dapr.Jobs/Models/Responses/DaprJobDetails.cs @@ -0,0 +1,45 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Jobs.Models.Responses; + +/// +/// Represents the details of a retrieved job. +/// +/// Represents the schedule that triggers the job. +public sealed record DaprJobDetails(DaprJobSchedule Schedule) +{ + /// + /// Allows for jobs with fixed repeat counts. + /// + public int? RepeatCount { get; init; } = null; + + /// + /// Identifies a point-in-time representing when the job schedule should start from, + /// or as a "one-shot" time if other scheduling fields are not provided. + /// + public DateTimeOffset? DueTime { get; init; } = null; + + /// + /// A point-in-time value representing with the job should expire. + /// + /// + /// This must be greater than if both are set. + /// + public DateTimeOffset? Ttl { get; init; } = null; + + /// + /// Stores the main payload of the job which is passed to the trigger function. + /// + public ReadOnlyMemory? Payload { get; init; } = null; +} diff --git a/src/Dapr.Jobs/Protos/dapr/proto/common/v1/common.proto b/src/Dapr.Jobs/Protos/dapr/proto/common/v1/common.proto new file mode 100644 index 000000000..1e63b885d --- /dev/null +++ b/src/Dapr.Jobs/Protos/dapr/proto/common/v1/common.proto @@ -0,0 +1,160 @@ +/* +Copyright 2021 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +syntax = "proto3"; + +package dapr.proto.common.v1; + +import "google/protobuf/any.proto"; + +option csharp_namespace = "Dapr.Client.Autogen.Grpc.v1"; +option java_outer_classname = "CommonProtos"; +option java_package = "io.dapr.v1"; +option go_package = "github.com/dapr/dapr/pkg/proto/common/v1;common"; + +// HTTPExtension includes HTTP verb and querystring +// when Dapr runtime delivers HTTP content. +// +// For example, when callers calls http invoke api +// POST http://localhost:3500/v1.0/invoke//method/?query1=value1&query2=value2 +// +// Dapr runtime will parse POST as a verb and extract querystring to quersytring map. +message HTTPExtension { + // Type of HTTP 1.1 Methods + // RFC 7231: https://tools.ietf.org/html/rfc7231#page-24 + // RFC 5789: https://datatracker.ietf.org/doc/html/rfc5789 + enum Verb { + NONE = 0; + GET = 1; + HEAD = 2; + POST = 3; + PUT = 4; + DELETE = 5; + CONNECT = 6; + OPTIONS = 7; + TRACE = 8; + PATCH = 9; + } + + // Required. HTTP verb. + Verb verb = 1; + + // Optional. querystring represents an encoded HTTP url query string in the following format: name=value&name2=value2 + string querystring = 2; +} + +// InvokeRequest is the message to invoke a method with the data. +// This message is used in InvokeService of Dapr gRPC Service and OnInvoke +// of AppCallback gRPC service. +message InvokeRequest { + // Required. method is a method name which will be invoked by caller. + string method = 1; + + // Required in unary RPCs. Bytes value or Protobuf message which caller sent. + // Dapr treats Any.value as bytes type if Any.type_url is unset. + google.protobuf.Any data = 2; + + // The type of data content. + // + // This field is required if data delivers http request body + // Otherwise, this is optional. + string content_type = 3; + + // HTTP specific fields if request conveys http-compatible request. + // + // This field is required for http-compatible request. Otherwise, + // this field is optional. + HTTPExtension http_extension = 4; +} + +// InvokeResponse is the response message including data and its content type +// from app callback. +// This message is used in InvokeService of Dapr gRPC Service and OnInvoke +// of AppCallback gRPC service. +message InvokeResponse { + // Required in unary RPCs. The content body of InvokeService response. + google.protobuf.Any data = 1; + + // Required. The type of data content. + string content_type = 2; +} + +// Chunk of data sent in a streaming request or response. +// This is used in requests including InternalInvokeRequestStream. +message StreamPayload { + // Data sent in the chunk. + // The amount of data included in each chunk is up to the discretion of the sender, and can be empty. + // Additionally, the amount of data doesn't need to be fixed and subsequent messages can send more, or less, data. + // Receivers must not make assumptions about the number of bytes they'll receive in each chunk. + bytes data = 1; + + // Sequence number. This is a counter that starts from 0 and increments by 1 on each chunk sent. + uint64 seq = 2; +} + +// StateItem represents state key, value, and additional options to save state. +message StateItem { + // Required. The state key + string key = 1; + + // Required. The state data for key + bytes value = 2; + + // The entity tag which represents the specific version of data. + // The exact ETag format is defined by the corresponding data store. + Etag etag = 3; + + // The metadata which will be passed to state store component. + map metadata = 4; + + // Options for concurrency and consistency to save the state. + StateOptions options = 5; +} + +// Etag represents a state item version +message Etag { + // value sets the etag value + string value = 1; +} + +// StateOptions configures concurrency and consistency for state operations +message StateOptions { + // Enum describing the supported concurrency for state. + enum StateConcurrency { + CONCURRENCY_UNSPECIFIED = 0; + CONCURRENCY_FIRST_WRITE = 1; + CONCURRENCY_LAST_WRITE = 2; + } + + // Enum describing the supported consistency for state. + enum StateConsistency { + CONSISTENCY_UNSPECIFIED = 0; + CONSISTENCY_EVENTUAL = 1; + CONSISTENCY_STRONG = 2; + } + + StateConcurrency concurrency = 1; + StateConsistency consistency = 2; +} + +// ConfigurationItem represents all the configuration with its name(key). +message ConfigurationItem { + // Required. The value of configuration item. + string value = 1; + + // Version is response only and cannot be fetched. Store is not expected to keep all versions available + string version = 2; + + // the metadata which will be passed to/from configuration store component. + map metadata = 3; +} diff --git a/src/Dapr.Jobs/Protos/dapr/proto/runtime/v1/appcallback.proto b/src/Dapr.Jobs/Protos/dapr/proto/runtime/v1/appcallback.proto new file mode 100644 index 000000000..3e98b5366 --- /dev/null +++ b/src/Dapr.Jobs/Protos/dapr/proto/runtime/v1/appcallback.proto @@ -0,0 +1,343 @@ +/* +Copyright 2021 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +syntax = "proto3"; + +package dapr.proto.runtime.v1; + +import "google/protobuf/any.proto"; +import "google/protobuf/empty.proto"; +import "dapr/proto/common/v1/common.proto"; +import "google/protobuf/struct.proto"; + +option csharp_namespace = "Dapr.AppCallback.Autogen.Grpc.v1"; +option java_outer_classname = "DaprAppCallbackProtos"; +option java_package = "io.dapr.v1"; +option go_package = "github.com/dapr/dapr/pkg/proto/runtime/v1;runtime"; + +// AppCallback V1 allows user application to interact with Dapr runtime. +// User application needs to implement AppCallback service if it needs to +// receive message from dapr runtime. +service AppCallback { + // Invokes service method with InvokeRequest. + rpc OnInvoke (common.v1.InvokeRequest) returns (common.v1.InvokeResponse) {} + + // Lists all topics subscribed by this app. + rpc ListTopicSubscriptions(google.protobuf.Empty) returns (ListTopicSubscriptionsResponse) {} + + // Subscribes events from Pubsub + rpc OnTopicEvent(TopicEventRequest) returns (TopicEventResponse) {} + + // Lists all input bindings subscribed by this app. + rpc ListInputBindings(google.protobuf.Empty) returns (ListInputBindingsResponse) {} + + // Listens events from the input bindings + // + // User application can save the states or send the events to the output + // bindings optionally by returning BindingEventResponse. + rpc OnBindingEvent(BindingEventRequest) returns (BindingEventResponse) {} +} + +// AppCallbackHealthCheck V1 is an optional extension to AppCallback V1 to implement +// the HealthCheck method. +service AppCallbackHealthCheck { + // Health check. + rpc HealthCheck(google.protobuf.Empty) returns (HealthCheckResponse) {} +} + +// AppCallbackAlpha V1 is an optional extension to AppCallback V1 to opt +// for Alpha RPCs. +service AppCallbackAlpha { + // Subscribes bulk events from Pubsub + rpc OnBulkTopicEventAlpha1(TopicEventBulkRequest) returns (TopicEventBulkResponse) {} + + // Sends job back to the app's endpoint at trigger time. + rpc OnJobEventAlpha1 (JobEventRequest) returns (JobEventResponse); +} + +message JobEventRequest { + // Job name. + string name = 1; + + // Job data to be sent back to app. + google.protobuf.Any data = 2; + + // Required. method is a method name which will be invoked by caller. + string method = 3; + + // The type of data content. + // + // This field is required if data delivers http request body + // Otherwise, this is optional. + string content_type = 4; + + // HTTP specific fields if request conveys http-compatible request. + // + // This field is required for http-compatible request. Otherwise, + // this field is optional. + common.v1.HTTPExtension http_extension = 5; +} + +// JobEventResponse is the response from the app when a job is triggered. +message JobEventResponse {} + +// TopicEventRequest message is compatible with CloudEvent spec v1.0 +// https://github.com/cloudevents/spec/blob/v1.0/spec.md +message TopicEventRequest { + // id identifies the event. Producers MUST ensure that source + id + // is unique for each distinct event. If a duplicate event is re-sent + // (e.g. due to a network error) it MAY have the same id. + string id = 1; + + // source identifies the context in which an event happened. + // Often this will include information such as the type of the + // event source, the organization publishing the event or the process + // that produced the event. The exact syntax and semantics behind + // the data encoded in the URI is defined by the event producer. + string source = 2; + + // The type of event related to the originating occurrence. + string type = 3; + + // The version of the CloudEvents specification. + string spec_version = 4; + + // The content type of data value. + string data_content_type = 5; + + // The content of the event. + bytes data = 7; + + // The pubsub topic which publisher sent to. + string topic = 6; + + // The name of the pubsub the publisher sent to. + string pubsub_name = 8; + + // The matching path from TopicSubscription/routes (if specified) for this event. + // This value is used by OnTopicEvent to "switch" inside the handler. + string path = 9; + + // The map of additional custom properties to be sent to the app. These are considered to be cloud event extensions. + google.protobuf.Struct extensions = 10; +} + +// TopicEventResponse is response from app on published message +message TopicEventResponse { + // TopicEventResponseStatus allows apps to have finer control over handling of the message. + enum TopicEventResponseStatus { + // SUCCESS is the default behavior: message is acknowledged and not retried or logged. + SUCCESS = 0; + // RETRY status signals Dapr to retry the message as part of an expected scenario (no warning is logged). + RETRY = 1; + // DROP status signals Dapr to drop the message as part of an unexpected scenario (warning is logged). + DROP = 2; + } + + // The list of output bindings. + TopicEventResponseStatus status = 1; +} + +// TopicEventCERequest message is compatible with CloudEvent spec v1.0 +message TopicEventCERequest { + // The unique identifier of this cloud event. + string id = 1; + + // source identifies the context in which an event happened. + string source = 2; + + // The type of event related to the originating occurrence. + string type = 3; + + // The version of the CloudEvents specification. + string spec_version = 4; + + // The content type of data value. + string data_content_type = 5; + + // The content of the event. + bytes data = 6; + + // Custom attributes which includes cloud event extensions. + google.protobuf.Struct extensions = 7; +} + +// TopicEventBulkRequestEntry represents a single message inside a bulk request +message TopicEventBulkRequestEntry { + // Unique identifier for the message. + string entry_id = 1; + + // The content of the event. + oneof event { + bytes bytes = 2; + TopicEventCERequest cloud_event = 3; + } + + // content type of the event contained. + string content_type = 4; + + // The metadata associated with the event. + map metadata = 5; +} + +// TopicEventBulkRequest represents request for bulk message +message TopicEventBulkRequest { + // Unique identifier for the bulk request. + string id = 1; + + // The list of items inside this bulk request. + repeated TopicEventBulkRequestEntry entries = 2; + + // The metadata associated with the this bulk request. + map metadata = 3; + + // The pubsub topic which publisher sent to. + string topic = 4; + + // The name of the pubsub the publisher sent to. + string pubsub_name = 5; + + // The type of event related to the originating occurrence. + string type = 6; + + // The matching path from TopicSubscription/routes (if specified) for this event. + // This value is used by OnTopicEvent to "switch" inside the handler. + string path = 7; +} + +// TopicEventBulkResponseEntry Represents single response, as part of TopicEventBulkResponse, to be +// sent by subscibed App for the corresponding single message during bulk subscribe +message TopicEventBulkResponseEntry { + // Unique identifier associated the message. + string entry_id = 1; + + // The status of the response. + TopicEventResponse.TopicEventResponseStatus status = 2; +} + +// AppBulkResponse is response from app on published message +message TopicEventBulkResponse { + + // The list of all responses for the bulk request. + repeated TopicEventBulkResponseEntry statuses = 1; +} + +// BindingEventRequest represents input bindings event. +message BindingEventRequest { + // Required. The name of the input binding component. + string name = 1; + + // Required. The payload that the input bindings sent + bytes data = 2; + + // The metadata set by the input binging components. + map metadata = 3; +} + +// BindingEventResponse includes operations to save state or +// send data to output bindings optionally. +message BindingEventResponse { + // The name of state store where states are saved. + string store_name = 1; + + // The state key values which will be stored in store_name. + repeated common.v1.StateItem states = 2; + + // BindingEventConcurrency is the kind of concurrency + enum BindingEventConcurrency { + // SEQUENTIAL sends data to output bindings specified in "to" sequentially. + SEQUENTIAL = 0; + // PARALLEL sends data to output bindings specified in "to" in parallel. + PARALLEL = 1; + } + + // The list of output bindings. + repeated string to = 3; + + // The content which will be sent to "to" output bindings. + bytes data = 4; + + // The concurrency of output bindings to send data to + // "to" output bindings list. The default is SEQUENTIAL. + BindingEventConcurrency concurrency = 5; +} + +// ListTopicSubscriptionsResponse is the message including the list of the subscribing topics. +message ListTopicSubscriptionsResponse { + // The list of topics. + repeated TopicSubscription subscriptions = 1; +} + +// TopicSubscription represents topic and metadata. +message TopicSubscription { + // Required. The name of the pubsub containing the topic below to subscribe to. + string pubsub_name = 1; + + // Required. The name of topic which will be subscribed + string topic = 2; + + // The optional properties used for this topic's subscription e.g. session id + map metadata = 3; + + // The optional routing rules to match against. In the gRPC interface, OnTopicEvent + // is still invoked but the matching path is sent in the TopicEventRequest. + TopicRoutes routes = 5; + + // The optional dead letter queue for this topic to send events to. + string dead_letter_topic = 6; + + // The optional bulk subscribe settings for this topic. + BulkSubscribeConfig bulk_subscribe = 7; +} + +message TopicRoutes { + // The list of rules for this topic. + repeated TopicRule rules = 1; + + // The default path for this topic. + string default = 2; +} + +message TopicRule { + // The optional CEL expression used to match the event. + // If the match is not specified, then the route is considered + // the default. + string match = 1; + + // The path used to identify matches for this subscription. + // This value is passed in TopicEventRequest and used by OnTopicEvent to "switch" + // inside the handler. + string path = 2; +} + +// BulkSubscribeConfig is the message to pass settings for bulk subscribe +message BulkSubscribeConfig { + // Required. Flag to enable/disable bulk subscribe + bool enabled = 1; + + // Optional. Max number of messages to be sent in a single bulk request + int32 max_messages_count = 2; + + // Optional. Max duration to wait for messages to be sent in a single bulk request + int32 max_await_duration_ms = 3; +} + +// ListInputBindingsResponse is the message including the list of input bindings. +message ListInputBindingsResponse { + // The list of input bindings. + repeated string bindings = 1; +} + +// HealthCheckResponse is the message with the response to the health check. +// This message is currently empty as used as placeholder. +message HealthCheckResponse {} diff --git a/src/Dapr.Jobs/Protos/dapr/proto/runtime/v1/dapr.proto b/src/Dapr.Jobs/Protos/dapr/proto/runtime/v1/dapr.proto new file mode 100644 index 000000000..ed4ae6deb --- /dev/null +++ b/src/Dapr.Jobs/Protos/dapr/proto/runtime/v1/dapr.proto @@ -0,0 +1,1234 @@ +/* +Copyright 2021 The Dapr Authors +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + http://www.apache.org/licenses/LICENSE-2.0 +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +syntax = "proto3"; + +package dapr.proto.runtime.v1; + +import "google/protobuf/any.proto"; +import "google/protobuf/empty.proto"; +import "google/protobuf/timestamp.proto"; +import "dapr/proto/common/v1/common.proto"; +import "dapr/proto/runtime/v1/appcallback.proto"; + +option csharp_namespace = "Dapr.Client.Autogen.Grpc.v1"; +option java_outer_classname = "DaprProtos"; +option java_package = "io.dapr.v1"; +option go_package = "github.com/dapr/dapr/pkg/proto/runtime/v1;runtime"; + +// Dapr service provides APIs to user application to access Dapr building blocks. +service Dapr { + // Invokes a method on a remote Dapr app. + // Deprecated: Use proxy mode service invocation instead. + rpc InvokeService(InvokeServiceRequest) returns (common.v1.InvokeResponse) {} + + // Gets the state for a specific key. + rpc GetState(GetStateRequest) returns (GetStateResponse) {} + + // Gets a bulk of state items for a list of keys + rpc GetBulkState(GetBulkStateRequest) returns (GetBulkStateResponse) {} + + // Saves the state for a specific key. + rpc SaveState(SaveStateRequest) returns (google.protobuf.Empty) {} + + // Queries the state. + rpc QueryStateAlpha1(QueryStateRequest) returns (QueryStateResponse) {} + + // Deletes the state for a specific key. + rpc DeleteState(DeleteStateRequest) returns (google.protobuf.Empty) {} + + // Deletes a bulk of state items for a list of keys + rpc DeleteBulkState(DeleteBulkStateRequest) returns (google.protobuf.Empty) {} + + // Executes transactions for a specified store + rpc ExecuteStateTransaction(ExecuteStateTransactionRequest) returns (google.protobuf.Empty) {} + + // Publishes events to the specific topic. + rpc PublishEvent(PublishEventRequest) returns (google.protobuf.Empty) {} + + // Bulk Publishes multiple events to the specified topic. + rpc BulkPublishEventAlpha1(BulkPublishRequest) returns (BulkPublishResponse) {} + + // SubscribeTopicEventsAlpha1 subscribes to a PubSub topic and receives topic + // events from it. + rpc SubscribeTopicEventsAlpha1(stream SubscribeTopicEventsRequestAlpha1) returns (stream TopicEventRequest) {} + + // Invokes binding data to specific output bindings + rpc InvokeBinding(InvokeBindingRequest) returns (InvokeBindingResponse) {} + + // Gets secrets from secret stores. + rpc GetSecret(GetSecretRequest) returns (GetSecretResponse) {} + + // Gets a bulk of secrets + rpc GetBulkSecret(GetBulkSecretRequest) returns (GetBulkSecretResponse) {} + + // Register an actor timer. + rpc RegisterActorTimer(RegisterActorTimerRequest) returns (google.protobuf.Empty) {} + + // Unregister an actor timer. + rpc UnregisterActorTimer(UnregisterActorTimerRequest) returns (google.protobuf.Empty) {} + + // Register an actor reminder. + rpc RegisterActorReminder(RegisterActorReminderRequest) returns (google.protobuf.Empty) {} + + // Unregister an actor reminder. + rpc UnregisterActorReminder(UnregisterActorReminderRequest) returns (google.protobuf.Empty) {} + + // Gets the state for a specific actor. + rpc GetActorState(GetActorStateRequest) returns (GetActorStateResponse) {} + + // Executes state transactions for a specified actor + rpc ExecuteActorStateTransaction(ExecuteActorStateTransactionRequest) returns (google.protobuf.Empty) {} + + // InvokeActor calls a method on an actor. + rpc InvokeActor (InvokeActorRequest) returns (InvokeActorResponse) {} + + // GetConfiguration gets configuration from configuration store. + rpc GetConfigurationAlpha1(GetConfigurationRequest) returns (GetConfigurationResponse) {} + + // GetConfiguration gets configuration from configuration store. + rpc GetConfiguration(GetConfigurationRequest) returns (GetConfigurationResponse) {} + + // SubscribeConfiguration gets configuration from configuration store and subscribe the updates event by grpc stream + rpc SubscribeConfigurationAlpha1(SubscribeConfigurationRequest) returns (stream SubscribeConfigurationResponse) {} + + // SubscribeConfiguration gets configuration from configuration store and subscribe the updates event by grpc stream + rpc SubscribeConfiguration(SubscribeConfigurationRequest) returns (stream SubscribeConfigurationResponse) {} + + // UnSubscribeConfiguration unsubscribe the subscription of configuration + rpc UnsubscribeConfigurationAlpha1(UnsubscribeConfigurationRequest) returns (UnsubscribeConfigurationResponse) {} + + // UnSubscribeConfiguration unsubscribe the subscription of configuration + rpc UnsubscribeConfiguration(UnsubscribeConfigurationRequest) returns (UnsubscribeConfigurationResponse) {} + + // TryLockAlpha1 tries to get a lock with an expiry. + rpc TryLockAlpha1(TryLockRequest)returns (TryLockResponse) {} + + // UnlockAlpha1 unlocks a lock. + rpc UnlockAlpha1(UnlockRequest)returns (UnlockResponse) {} + + // EncryptAlpha1 encrypts a message using the Dapr encryption scheme and a key stored in the vault. + rpc EncryptAlpha1(stream EncryptRequest) returns (stream EncryptResponse); + + // DecryptAlpha1 decrypts a message using the Dapr encryption scheme and a key stored in the vault. + rpc DecryptAlpha1(stream DecryptRequest) returns (stream DecryptResponse); + + // Gets metadata of the sidecar + rpc GetMetadata (GetMetadataRequest) returns (GetMetadataResponse) {} + + // Sets value in extended metadata of the sidecar + rpc SetMetadata (SetMetadataRequest) returns (google.protobuf.Empty) {} + + // SubtleGetKeyAlpha1 returns the public part of an asymmetric key stored in the vault. + rpc SubtleGetKeyAlpha1(SubtleGetKeyRequest) returns (SubtleGetKeyResponse); + + // SubtleEncryptAlpha1 encrypts a small message using a key stored in the vault. + rpc SubtleEncryptAlpha1(SubtleEncryptRequest) returns (SubtleEncryptResponse); + + // SubtleDecryptAlpha1 decrypts a small message using a key stored in the vault. + rpc SubtleDecryptAlpha1(SubtleDecryptRequest) returns (SubtleDecryptResponse); + + // SubtleWrapKeyAlpha1 wraps a key using a key stored in the vault. + rpc SubtleWrapKeyAlpha1(SubtleWrapKeyRequest) returns (SubtleWrapKeyResponse); + + // SubtleUnwrapKeyAlpha1 unwraps a key using a key stored in the vault. + rpc SubtleUnwrapKeyAlpha1(SubtleUnwrapKeyRequest) returns (SubtleUnwrapKeyResponse); + + // SubtleSignAlpha1 signs a message using a key stored in the vault. + rpc SubtleSignAlpha1(SubtleSignRequest) returns (SubtleSignResponse); + + // SubtleVerifyAlpha1 verifies the signature of a message using a key stored in the vault. + rpc SubtleVerifyAlpha1(SubtleVerifyRequest) returns (SubtleVerifyResponse); + + // Starts a new instance of a workflow + rpc StartWorkflowAlpha1 (StartWorkflowRequest) returns (StartWorkflowResponse) {} + + // Gets details about a started workflow instance + rpc GetWorkflowAlpha1 (GetWorkflowRequest) returns (GetWorkflowResponse) {} + + // Purge Workflow + rpc PurgeWorkflowAlpha1 (PurgeWorkflowRequest) returns (google.protobuf.Empty) {} + + // Terminates a running workflow instance + rpc TerminateWorkflowAlpha1 (TerminateWorkflowRequest) returns (google.protobuf.Empty) {} + + // Pauses a running workflow instance + rpc PauseWorkflowAlpha1 (PauseWorkflowRequest) returns (google.protobuf.Empty) {} + + // Resumes a paused workflow instance + rpc ResumeWorkflowAlpha1 (ResumeWorkflowRequest) returns (google.protobuf.Empty) {} + + // Raise an event to a running workflow instance + rpc RaiseEventWorkflowAlpha1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) {} + + // Starts a new instance of a workflow + rpc StartWorkflowBeta1 (StartWorkflowRequest) returns (StartWorkflowResponse) {} + + // Gets details about a started workflow instance + rpc GetWorkflowBeta1 (GetWorkflowRequest) returns (GetWorkflowResponse) {} + + // Purge Workflow + rpc PurgeWorkflowBeta1 (PurgeWorkflowRequest) returns (google.protobuf.Empty) {} + + // Terminates a running workflow instance + rpc TerminateWorkflowBeta1 (TerminateWorkflowRequest) returns (google.protobuf.Empty) {} + + // Pauses a running workflow instance + rpc PauseWorkflowBeta1 (PauseWorkflowRequest) returns (google.protobuf.Empty) {} + + // Resumes a paused workflow instance + rpc ResumeWorkflowBeta1 (ResumeWorkflowRequest) returns (google.protobuf.Empty) {} + + // Raise an event to a running workflow instance + rpc RaiseEventWorkflowBeta1 (RaiseEventWorkflowRequest) returns (google.protobuf.Empty) {} + // Shutdown the sidecar + rpc Shutdown (ShutdownRequest) returns (google.protobuf.Empty) {} + + // Create and schedule a job + rpc ScheduleJobAlpha1(ScheduleJobRequest) returns (ScheduleJobResponse) {} + + // Gets a scheduled job + rpc GetJobAlpha1(GetJobRequest) returns (GetJobResponse) {} + + // Delete a job + rpc DeleteJobAlpha1(DeleteJobRequest) returns (DeleteJobResponse) {} +} + +// InvokeServiceRequest represents the request message for Service invocation. +message InvokeServiceRequest { + // Required. Callee's app id. + string id = 1; + + // Required. message which will be delivered to callee. + common.v1.InvokeRequest message = 3; +} + +// GetStateRequest is the message to get key-value states from specific state store. +message GetStateRequest { + // The name of state store. + string store_name = 1; + + // The key of the desired state + string key = 2; + + // The read consistency of the state store. + common.v1.StateOptions.StateConsistency consistency = 3; + + // The metadata which will be sent to state store components. + map metadata = 4; +} + +// GetBulkStateRequest is the message to get a list of key-value states from specific state store. +message GetBulkStateRequest { + // The name of state store. + string store_name = 1; + + // The keys to get. + repeated string keys = 2; + + // The number of parallel operations executed on the state store for a get operation. + int32 parallelism = 3; + + // The metadata which will be sent to state store components. + map metadata = 4; +} + +// GetBulkStateResponse is the response conveying the list of state values. +message GetBulkStateResponse { + // The list of items containing the keys to get values for. + repeated BulkStateItem items = 1; +} + +// BulkStateItem is the response item for a bulk get operation. +// Return values include the item key, data and etag. +message BulkStateItem { + // state item key + string key = 1; + + // The byte array data + bytes data = 2; + + // The entity tag which represents the specific version of data. + // ETag format is defined by the corresponding data store. + string etag = 3; + + // The error that was returned from the state store in case of a failed get operation. + string error = 4; + + // The metadata which will be sent to app. + map metadata = 5; +} + +// GetStateResponse is the response conveying the state value and etag. +message GetStateResponse { + // The byte array data + bytes data = 1; + + // The entity tag which represents the specific version of data. + // ETag format is defined by the corresponding data store. + string etag = 2; + + // The metadata which will be sent to app. + map metadata = 3; +} + +// DeleteStateRequest is the message to delete key-value states in the specific state store. +message DeleteStateRequest { + // The name of state store. + string store_name = 1; + + // The key of the desired state + string key = 2; + + // The entity tag which represents the specific version of data. + // The exact ETag format is defined by the corresponding data store. + common.v1.Etag etag = 3; + + // State operation options which includes concurrency/ + // consistency/retry_policy. + common.v1.StateOptions options = 4; + + // The metadata which will be sent to state store components. + map metadata = 5; +} + +// DeleteBulkStateRequest is the message to delete a list of key-value states from specific state store. +message DeleteBulkStateRequest { + // The name of state store. + string store_name = 1; + + // The array of the state key values. + repeated common.v1.StateItem states = 2; +} + +// SaveStateRequest is the message to save multiple states into state store. +message SaveStateRequest { + // The name of state store. + string store_name = 1; + + // The array of the state key values. + repeated common.v1.StateItem states = 2; +} + +// QueryStateRequest is the message to query state store. +message QueryStateRequest { + // The name of state store. + string store_name = 1 [json_name = "storeName"]; + + // The query in JSON format. + string query = 2; + + // The metadata which will be sent to state store components. + map metadata = 3; +} + +message QueryStateItem { + // The object key. + string key = 1; + + // The object value. + bytes data = 2; + + // The entity tag which represents the specific version of data. + // ETag format is defined by the corresponding data store. + string etag = 3; + + // The error message indicating an error in processing of the query result. + string error = 4; +} + +// QueryStateResponse is the response conveying the query results. +message QueryStateResponse { + // An array of query results. + repeated QueryStateItem results = 1; + + // Pagination token. + string token = 2; + + // The metadata which will be sent to app. + map metadata = 3; +} + +// PublishEventRequest is the message to publish event data to pubsub topic +message PublishEventRequest { + // The name of the pubsub component + string pubsub_name = 1; + + // The pubsub topic + string topic = 2; + + // The data which will be published to topic. + bytes data = 3; + + // The content type for the data (optional). + string data_content_type = 4; + + // The metadata passing to pub components + // + // metadata property: + // - key : the key of the message. + map metadata = 5; +} + +// BulkPublishRequest is the message to bulk publish events to pubsub topic +message BulkPublishRequest { + // The name of the pubsub component + string pubsub_name = 1; + + // The pubsub topic + string topic = 2; + + // The entries which contain the individual events and associated details to be published + repeated BulkPublishRequestEntry entries = 3; + + // The request level metadata passing to to the pubsub components + map metadata = 4; +} + +// BulkPublishRequestEntry is the message containing the event to be bulk published +message BulkPublishRequestEntry { + // The request scoped unique ID referring to this message. Used to map status in response + string entry_id = 1; + + // The event which will be pulished to the topic + bytes event = 2; + + // The content type for the event + string content_type = 3; + + // The event level metadata passing to the pubsub component + map metadata = 4; +} + +// BulkPublishResponse is the message returned from a BulkPublishEvent call +message BulkPublishResponse { + // The entries for different events that failed publish in the BulkPublishEvent call + repeated BulkPublishResponseFailedEntry failedEntries = 1; +} + +// BulkPublishResponseFailedEntry is the message containing the entryID and error of a failed event in BulkPublishEvent call +message BulkPublishResponseFailedEntry { + // The response scoped unique ID referring to this message + string entry_id = 1; + + // The error message if any on failure + string error = 2; +} + +// SubscribeTopicEventsRequestAlpha1 is a message containing the details for +// subscribing to a topic via streaming. +// The first message must always be the initial request. All subsequent +// messages must be event responses. +message SubscribeTopicEventsRequestAlpha1 { + oneof subscribe_topic_events_request_type { + SubscribeTopicEventsInitialRequestAlpha1 initial_request = 1; + SubscribeTopicEventsResponseAlpha1 event_response = 2; + } +} + +// SubscribeTopicEventsInitialRequestAlpha1 is the initial message containing the +// details for subscribing to a topic via streaming. +message SubscribeTopicEventsInitialRequestAlpha1 { + // The name of the pubsub component + string pubsub_name = 1; + + // The pubsub topic + string topic = 2; + + // The metadata passing to pub components + // + // metadata property: + // - key : the key of the message. + map metadata = 3; + + // dead_letter_topic is the topic to which messages that fail to be processed + // are sent. + optional string dead_letter_topic = 4; +} + +// SubscribeTopicEventsResponseAlpha1 is a message containing the result of a +// subscription to a topic. +message SubscribeTopicEventsResponseAlpha1 { + // id is the unique identifier for the subscription request. + string id = 1; + + // status is the result of the subscription request. + TopicEventResponse status = 2; +} + +// InvokeBindingRequest is the message to send data to output bindings +message InvokeBindingRequest { + // The name of the output binding to invoke. + string name = 1; + + // The data which will be sent to output binding. + bytes data = 2; + + // The metadata passing to output binding components + // + // Common metadata property: + // - ttlInSeconds : the time to live in seconds for the message. + // If set in the binding definition will cause all messages to + // have a default time to live. The message ttl overrides any value + // in the binding definition. + map metadata = 3; + + // The name of the operation type for the binding to invoke + string operation = 4; +} + +// InvokeBindingResponse is the message returned from an output binding invocation +message InvokeBindingResponse { + // The data which will be sent to output binding. + bytes data = 1; + + // The metadata returned from an external system + map metadata = 2; +} + +// GetSecretRequest is the message to get secret from secret store. +message GetSecretRequest { + // The name of secret store. + string store_name = 1 [json_name = "storeName"]; + + // The name of secret key. + string key = 2; + + // The metadata which will be sent to secret store components. + map metadata = 3; +} + +// GetSecretResponse is the response message to convey the requested secret. +message GetSecretResponse { + // data is the secret value. Some secret store, such as kubernetes secret + // store, can save multiple secrets for single secret key. + map data = 1; +} + +// GetBulkSecretRequest is the message to get the secrets from secret store. +message GetBulkSecretRequest { + // The name of secret store. + string store_name = 1 [json_name = "storeName"]; + + // The metadata which will be sent to secret store components. + map metadata = 2; +} + +// SecretResponse is a map of decrypted string/string values +message SecretResponse { + map secrets = 1; +} + +// GetBulkSecretResponse is the response message to convey the requested secrets. +message GetBulkSecretResponse { + // data hold the secret values. Some secret store, such as kubernetes secret + // store, can save multiple secrets for single secret key. + map data = 1; +} + +// TransactionalStateOperation is the message to execute a specified operation with a key-value pair. +message TransactionalStateOperation { + // The type of operation to be executed + string operationType = 1; + + // State values to be operated on + common.v1.StateItem request = 2; +} + +// ExecuteStateTransactionRequest is the message to execute multiple operations on a specified store. +message ExecuteStateTransactionRequest { + // Required. name of state store. + string storeName = 1; + + // Required. transactional operation list. + repeated TransactionalStateOperation operations = 2; + + // The metadata used for transactional operations. + map metadata = 3; +} + +// RegisterActorTimerRequest is the message to register a timer for an actor of a given type and id. +message RegisterActorTimerRequest { + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; + string name = 3; + string due_time = 4 [json_name = "dueTime"]; + string period = 5; + string callback = 6; + bytes data = 7; + string ttl = 8; +} + +// UnregisterActorTimerRequest is the message to unregister an actor timer +message UnregisterActorTimerRequest { + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; + string name = 3; +} + +// RegisterActorReminderRequest is the message to register a reminder for an actor of a given type and id. +message RegisterActorReminderRequest { + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; + string name = 3; + string due_time = 4 [json_name = "dueTime"]; + string period = 5; + bytes data = 6; + string ttl = 7; +} + +// UnregisterActorReminderRequest is the message to unregister an actor reminder. +message UnregisterActorReminderRequest { + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; + string name = 3; +} + +// GetActorStateRequest is the message to get key-value states from specific actor. +message GetActorStateRequest { + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; + string key = 3; +} + +// GetActorStateResponse is the response conveying the actor's state value. +message GetActorStateResponse { + bytes data = 1; + + // The metadata which will be sent to app. + map metadata = 2; +} + +// ExecuteActorStateTransactionRequest is the message to execute multiple operations on a specified actor. +message ExecuteActorStateTransactionRequest { + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; + repeated TransactionalActorStateOperation operations = 3; +} + +// TransactionalActorStateOperation is the message to execute a specified operation with a key-value pair. +message TransactionalActorStateOperation { + string operationType = 1; + string key = 2; + google.protobuf.Any value = 3; + // The metadata used for transactional operations. + // + // Common metadata property: + // - ttlInSeconds : the time to live in seconds for the stored value. + map metadata = 4; +} + +// InvokeActorRequest is the message to call an actor. +message InvokeActorRequest { + string actor_type = 1 [json_name = "actorType"]; + string actor_id = 2 [json_name = "actorId"]; + string method = 3; + bytes data = 4; + map metadata = 5; +} + +// InvokeActorResponse is the method that returns an actor invocation response. +message InvokeActorResponse { + bytes data = 1; +} + +// GetMetadataRequest is the message for the GetMetadata request. +message GetMetadataRequest { + // Empty +} + +// GetMetadataResponse is a message that is returned on GetMetadata rpc call. +message GetMetadataResponse { + string id = 1; + // Deprecated alias for actor_runtime.active_actors. + repeated ActiveActorsCount active_actors_count = 2 [json_name = "actors", deprecated = true]; + repeated RegisteredComponents registered_components = 3 [json_name = "components"]; + map extended_metadata = 4 [json_name = "extended"]; + repeated PubsubSubscription subscriptions = 5 [json_name = "subscriptions"]; + repeated MetadataHTTPEndpoint http_endpoints = 6 [json_name = "httpEndpoints"]; + AppConnectionProperties app_connection_properties = 7 [json_name = "appConnectionProperties"]; + string runtime_version = 8 [json_name = "runtimeVersion"]; + repeated string enabled_features = 9 [json_name = "enabledFeatures"]; + ActorRuntime actor_runtime = 10 [json_name = "actorRuntime"]; + //TODO: Cassie: probably add scheduler runtime status +} + +message ActorRuntime { + enum ActorRuntimeStatus { + // Indicates that the actor runtime is still being initialized. + INITIALIZING = 0; + // Indicates that the actor runtime is disabled. + // This normally happens when Dapr is started without "placement-host-address" + DISABLED = 1; + // Indicates the actor runtime is running, either as an actor host or client. + RUNNING = 2; + } + + // Contains an enum indicating whether the actor runtime has been initialized. + ActorRuntimeStatus runtime_status = 1 [json_name = "runtimeStatus"]; + // Count of active actors per type. + repeated ActiveActorsCount active_actors = 2 [json_name = "activeActors"]; + // Indicates whether the actor runtime is ready to host actors. + bool host_ready = 3 [json_name = "hostReady"]; + // Custom message from the placement provider. + string placement = 4 [json_name = "placement"]; +} + +message ActiveActorsCount { + string type = 1; + int32 count = 2; +} + +message RegisteredComponents { + string name = 1; + string type = 2; + string version = 3; + repeated string capabilities = 4; +} + +message MetadataHTTPEndpoint { + string name = 1 [json_name = "name"]; +} + +message AppConnectionProperties { + int32 port = 1; + string protocol = 2; + string channel_address = 3 [json_name = "channelAddress"]; + int32 max_concurrency = 4 [json_name = "maxConcurrency"]; + AppConnectionHealthProperties health = 5; +} + +message AppConnectionHealthProperties { + string health_check_path = 1 [json_name = "healthCheckPath"]; + string health_probe_interval = 2 [json_name = "healthProbeInterval"]; + string health_probe_timeout = 3 [json_name = "healthProbeTimeout"]; + int32 health_threshold = 4 [json_name = "healthThreshold"]; +} + +message PubsubSubscription { + string pubsub_name = 1 [json_name = "pubsubname"]; + string topic = 2 [json_name = "topic"]; + map metadata = 3 [json_name = "metadata"]; + PubsubSubscriptionRules rules = 4 [json_name = "rules"]; + string dead_letter_topic = 5 [json_name = "deadLetterTopic"]; + PubsubSubscriptionType type = 6 [json_name = "type"]; +} + +// PubsubSubscriptionType indicates the type of subscription +enum PubsubSubscriptionType { + // UNKNOWN is the default value for the subscription type. + UNKNOWN = 0; + // Declarative subscription (k8s CRD) + DECLARATIVE = 1; + // Programmatically created subscription + PROGRAMMATIC = 2; + // Bidirectional Streaming subscription + STREAMING = 3; +} + +message PubsubSubscriptionRules { + repeated PubsubSubscriptionRule rules = 1; +} + +message PubsubSubscriptionRule { + string match = 1; + string path = 2; +} + +message SetMetadataRequest { + string key = 1; + string value = 2; +} + +// GetConfigurationRequest is the message to get a list of key-value configuration from specified configuration store. +message GetConfigurationRequest { + // Required. The name of configuration store. + string store_name = 1; + + // Optional. The key of the configuration item to fetch. + // If set, only query for the specified configuration items. + // Empty list means fetch all. + repeated string keys = 2; + + // Optional. The metadata which will be sent to configuration store components. + map metadata = 3; +} + +// GetConfigurationResponse is the response conveying the list of configuration values. +// It should be the FULL configuration of specified application which contains all of its configuration items. +message GetConfigurationResponse { + map items = 1; +} + +// SubscribeConfigurationRequest is the message to get a list of key-value configuration from specified configuration store. +message SubscribeConfigurationRequest { + // The name of configuration store. + string store_name = 1; + + // Optional. The key of the configuration item to fetch. + // If set, only query for the specified configuration items. + // Empty list means fetch all. + repeated string keys = 2; + + // The metadata which will be sent to configuration store components. + map metadata = 3; +} + +// UnSubscribeConfigurationRequest is the message to stop watching the key-value configuration. +message UnsubscribeConfigurationRequest { + // The name of configuration store. + string store_name = 1; + + // The id to unsubscribe. + string id = 2; +} + +message SubscribeConfigurationResponse { + // Subscribe id, used to stop subscription. + string id = 1; + + // The list of items containing configuration values + map items = 2; +} + +message UnsubscribeConfigurationResponse { + bool ok = 1; + string message = 2; +} + +message TryLockRequest { + // Required. The lock store name,e.g. `redis`. + string store_name = 1 [json_name = "storeName"]; + + // Required. resource_id is the lock key. e.g. `order_id_111` + // It stands for "which resource I want to protect" + string resource_id = 2 [json_name = "resourceId"]; + + // Required. lock_owner indicate the identifier of lock owner. + // You can generate a uuid as lock_owner.For example,in golang: + // + // req.LockOwner = uuid.New().String() + // + // This field is per request,not per process,so it is different for each request, + // which aims to prevent multi-thread in the same process trying the same lock concurrently. + // + // The reason why we don't make it automatically generated is: + // 1. If it is automatically generated,there must be a 'my_lock_owner_id' field in the response. + // This name is so weird that we think it is inappropriate to put it into the api spec + // 2. If we change the field 'my_lock_owner_id' in the response to 'lock_owner',which means the current lock owner of this lock, + // we find that in some lock services users can't get the current lock owner.Actually users don't need it at all. + // 3. When reentrant lock is needed,the existing lock_owner is required to identify client and check "whether this client can reenter this lock". + // So this field in the request shouldn't be removed. + string lock_owner = 3 [json_name = "lockOwner"]; + + // Required. The time before expiry.The time unit is second. + int32 expiry_in_seconds = 4 [json_name = "expiryInSeconds"]; +} + +message TryLockResponse { + bool success = 1; +} + +message UnlockRequest { + string store_name = 1 [json_name = "storeName"]; + // resource_id is the lock key. + string resource_id = 2 [json_name = "resourceId"]; + string lock_owner = 3 [json_name = "lockOwner"]; +} + +message UnlockResponse { + enum Status { + SUCCESS = 0; + LOCK_DOES_NOT_EXIST = 1; + LOCK_BELONGS_TO_OTHERS = 2; + INTERNAL_ERROR = 3; + } + + Status status = 1; +} + +// SubtleGetKeyRequest is the request object for SubtleGetKeyAlpha1. +message SubtleGetKeyRequest { + enum KeyFormat { + // PEM (PKIX) (default) + PEM = 0; + // JSON (JSON Web Key) as string + JSON = 1; + } + + // Name of the component + string component_name = 1 [json_name="componentName"]; + // Name (or name/version) of the key to use in the key vault + string name = 2; + // Response format + KeyFormat format = 3; +} + +// SubtleGetKeyResponse is the response for SubtleGetKeyAlpha1. +message SubtleGetKeyResponse { + // Name (or name/version) of the key. + // This is returned as response too in case there is a version. + string name = 1; + // Public key, encoded in the requested format + string public_key = 2 [json_name="publicKey"]; +} + +// SubtleEncryptRequest is the request for SubtleEncryptAlpha1. +message SubtleEncryptRequest { + // Name of the component + string component_name = 1 [json_name="componentName"]; + // Message to encrypt. + bytes plaintext = 2; + // Algorithm to use, as in the JWA standard. + string algorithm = 3; + // Name (or name/version) of the key. + string key_name = 4 [json_name="keyName"]; + // Nonce / initialization vector. + // Ignored with asymmetric ciphers. + bytes nonce = 5; + // Associated Data when using AEAD ciphers (optional). + bytes associated_data = 6 [json_name="associatedData"]; +} + +// SubtleEncryptResponse is the response for SubtleEncryptAlpha1. +message SubtleEncryptResponse { + // Encrypted ciphertext. + bytes ciphertext = 1; + // Authentication tag. + // This is nil when not using an authenticated cipher. + bytes tag = 2; +} + +// SubtleDecryptRequest is the request for SubtleDecryptAlpha1. +message SubtleDecryptRequest { + // Name of the component + string component_name = 1 [json_name="componentName"]; + // Message to decrypt. + bytes ciphertext = 2; + // Algorithm to use, as in the JWA standard. + string algorithm = 3; + // Name (or name/version) of the key. + string key_name = 4 [json_name="keyName"]; + // Nonce / initialization vector. + // Ignored with asymmetric ciphers. + bytes nonce = 5; + // Authentication tag. + // This is nil when not using an authenticated cipher. + bytes tag = 6; + // Associated Data when using AEAD ciphers (optional). + bytes associated_data = 7 [json_name="associatedData"]; +} + +// SubtleDecryptResponse is the response for SubtleDecryptAlpha1. +message SubtleDecryptResponse { + // Decrypted plaintext. + bytes plaintext = 1; +} + +// SubtleWrapKeyRequest is the request for SubtleWrapKeyAlpha1. +message SubtleWrapKeyRequest { + // Name of the component + string component_name = 1 [json_name="componentName"]; + // Key to wrap + bytes plaintext_key = 2 [json_name="plaintextKey"]; + // Algorithm to use, as in the JWA standard. + string algorithm = 3; + // Name (or name/version) of the key. + string key_name = 4 [json_name="keyName"]; + // Nonce / initialization vector. + // Ignored with asymmetric ciphers. + bytes nonce = 5; + // Associated Data when using AEAD ciphers (optional). + bytes associated_data = 6 [json_name="associatedData"]; +} + +// SubtleWrapKeyResponse is the response for SubtleWrapKeyAlpha1. +message SubtleWrapKeyResponse { + // Wrapped key. + bytes wrapped_key = 1 [json_name="wrappedKey"]; + // Authentication tag. + // This is nil when not using an authenticated cipher. + bytes tag = 2; +} + +// SubtleUnwrapKeyRequest is the request for SubtleUnwrapKeyAlpha1. +message SubtleUnwrapKeyRequest { + // Name of the component + string component_name = 1 [json_name="componentName"]; + // Wrapped key. + bytes wrapped_key = 2 [json_name="wrappedKey"]; + // Algorithm to use, as in the JWA standard. + string algorithm = 3; + // Name (or name/version) of the key. + string key_name = 4 [json_name="keyName"]; + // Nonce / initialization vector. + // Ignored with asymmetric ciphers. + bytes nonce = 5; + // Authentication tag. + // This is nil when not using an authenticated cipher. + bytes tag = 6; + // Associated Data when using AEAD ciphers (optional). + bytes associated_data = 7 [json_name="associatedData"]; +} + +// SubtleUnwrapKeyResponse is the response for SubtleUnwrapKeyAlpha1. +message SubtleUnwrapKeyResponse { + // Key in plaintext + bytes plaintext_key = 1 [json_name="plaintextKey"]; +} + +// SubtleSignRequest is the request for SubtleSignAlpha1. +message SubtleSignRequest { + // Name of the component + string component_name = 1 [json_name="componentName"]; + // Digest to sign. + bytes digest = 2; + // Algorithm to use, as in the JWA standard. + string algorithm = 3; + // Name (or name/version) of the key. + string key_name = 4 [json_name="keyName"]; +} + +// SubtleSignResponse is the response for SubtleSignAlpha1. +message SubtleSignResponse { + // The signature that was computed + bytes signature = 1; +} + +// SubtleVerifyRequest is the request for SubtleVerifyAlpha1. +message SubtleVerifyRequest { + // Name of the component + string component_name = 1 [json_name="componentName"]; + // Digest of the message. + bytes digest = 2; + // Algorithm to use, as in the JWA standard. + string algorithm = 3; + // Name (or name/version) of the key. + string key_name = 4 [json_name="keyName"]; + // Signature to verify. + bytes signature = 5; +} + +// SubtleVerifyResponse is the response for SubtleVerifyAlpha1. +message SubtleVerifyResponse { + // True if the signature is valid. + bool valid = 1; +} + +// EncryptRequest is the request for EncryptAlpha1. +message EncryptRequest { + // Request details. Must be present in the first message only. + EncryptRequestOptions options = 1; + // Chunk of data of arbitrary size. + common.v1.StreamPayload payload = 2; +} + +// EncryptRequestOptions contains options for the first message in the EncryptAlpha1 request. +message EncryptRequestOptions { + // Name of the component. Required. + string component_name = 1 [json_name="componentName"]; + // Name (or name/version) of the key. Required. + string key_name = 2 [json_name="keyName"]; + // Key wrapping algorithm to use. Required. + // Supported options include: A256KW (alias: AES), A128CBC, A192CBC, A256CBC, RSA-OAEP-256 (alias: RSA). + string key_wrap_algorithm = 3; + // Cipher used to encrypt data (optional): "aes-gcm" (default) or "chacha20-poly1305" + string data_encryption_cipher = 10; + // If true, the encrypted document does not contain a key reference. + // In that case, calls to the Decrypt method must provide a key reference (name or name/version). + // Defaults to false. + bool omit_decryption_key_name = 11 [json_name="omitDecryptionKeyName"]; + // Key reference to embed in the encrypted document (name or name/version). + // This is helpful if the reference of the key used to decrypt the document is different from the one used to encrypt it. + // If unset, uses the reference of the key used to encrypt the document (this is the default behavior). + // This option is ignored if omit_decryption_key_name is true. + string decryption_key_name = 12 [json_name="decryptionKeyName"]; +} + +// EncryptResponse is the response for EncryptAlpha1. +message EncryptResponse { + // Chunk of data. + common.v1.StreamPayload payload = 1; +} + +// DecryptRequest is the request for DecryptAlpha1. +message DecryptRequest { + // Request details. Must be present in the first message only. + DecryptRequestOptions options = 1; + // Chunk of data of arbitrary size. + common.v1.StreamPayload payload = 2; +} + +// DecryptRequestOptions contains options for the first message in the DecryptAlpha1 request. +message DecryptRequestOptions { + // Name of the component + string component_name = 1 [json_name="componentName"]; + // Name (or name/version) of the key to decrypt the message. + // Overrides any key reference included in the message if present. + // This is required if the message doesn't include a key reference (i.e. was created with omit_decryption_key_name set to true). + string key_name = 12 [json_name="keyName"]; +} + +// DecryptResponse is the response for DecryptAlpha1. +message DecryptResponse { + // Chunk of data. + common.v1.StreamPayload payload = 1; +} + +// GetWorkflowRequest is the request for GetWorkflowBeta1. +message GetWorkflowRequest { + // ID of the workflow instance to query. + string instance_id = 1 [json_name = "instanceID"]; + // Name of the workflow component. + string workflow_component = 2 [json_name = "workflowComponent"]; +} + +// GetWorkflowResponse is the response for GetWorkflowBeta1. +message GetWorkflowResponse { + // ID of the workflow instance. + string instance_id = 1 [json_name = "instanceID"]; + // Name of the workflow. + string workflow_name = 2 [json_name = "workflowName"]; + // The time at which the workflow instance was created. + google.protobuf.Timestamp created_at = 3 [json_name = "createdAt"]; + // The last time at which the workflow instance had its state changed. + google.protobuf.Timestamp last_updated_at = 4 [json_name = "lastUpdatedAt"]; + // The current status of the workflow instance, for example, "PENDING", "RUNNING", "SUSPENDED", "COMPLETED", "FAILED", and "TERMINATED". + string runtime_status = 5 [json_name = "runtimeStatus"]; + // Additional component-specific properties of the workflow instance. + map properties = 6; +} + +// StartWorkflowRequest is the request for StartWorkflowBeta1. +message StartWorkflowRequest { + // The ID to assign to the started workflow instance. If empty, a random ID is generated. + string instance_id = 1 [json_name = "instanceID"]; + // Name of the workflow component. + string workflow_component = 2 [json_name = "workflowComponent"]; + // Name of the workflow. + string workflow_name = 3 [json_name = "workflowName"]; + // Additional component-specific options for starting the workflow instance. + map options = 4; + // Input data for the workflow instance. + bytes input = 5; +} + +// StartWorkflowResponse is the response for StartWorkflowBeta1. +message StartWorkflowResponse { + // ID of the started workflow instance. + string instance_id = 1 [json_name = "instanceID"]; +} + +// TerminateWorkflowRequest is the request for TerminateWorkflowBeta1. +message TerminateWorkflowRequest { + // ID of the workflow instance to terminate. + string instance_id = 1 [json_name = "instanceID"]; + // Name of the workflow component. + string workflow_component = 2 [json_name = "workflowComponent"]; +} + +// PauseWorkflowRequest is the request for PauseWorkflowBeta1. +message PauseWorkflowRequest { + // ID of the workflow instance to pause. + string instance_id = 1 [json_name = "instanceID"]; + // Name of the workflow component. + string workflow_component = 2 [json_name = "workflowComponent"]; +} + +// ResumeWorkflowRequest is the request for ResumeWorkflowBeta1. +message ResumeWorkflowRequest { + // ID of the workflow instance to resume. + string instance_id = 1 [json_name = "instanceID"]; + // Name of the workflow component. + string workflow_component = 2 [json_name = "workflowComponent"]; +} + +// RaiseEventWorkflowRequest is the request for RaiseEventWorkflowBeta1. +message RaiseEventWorkflowRequest { + // ID of the workflow instance to raise an event for. + string instance_id = 1 [json_name = "instanceID"]; + // Name of the workflow component. + string workflow_component = 2 [json_name = "workflowComponent"]; + // Name of the event. + string event_name = 3 [json_name = "eventName"]; + // Data associated with the event. + bytes event_data = 4; +} + +// PurgeWorkflowRequest is the request for PurgeWorkflowBeta1. +message PurgeWorkflowRequest { + // ID of the workflow instance to purge. + string instance_id = 1 [json_name = "instanceID"]; + // Name of the workflow component. + string workflow_component = 2 [json_name = "workflowComponent"]; +} + +// ShutdownRequest is the request for Shutdown. +message ShutdownRequest { + // Empty +} + +// Job is the definition of a job. +message Job { + // The unique name for the job. + string name = 1; + + // The schedule for the job. + optional string schedule = 2; + + // Optional: jobs with fixed repeat counts (accounting for Actor Reminders). + optional uint32 repeats = 3; + + // Optional: sets time at which or time interval before the callback is invoked for the first time. + optional string due_time = 4; + + // Optional: Time To Live to allow for auto deletes (accounting for Actor Reminders). + optional string ttl = 5; + + // Job data. + google.protobuf.Any data = 6; +} + +// ScheduleJobRequest is the message to create/schedule the job. +message ScheduleJobRequest { + // The job details. + Job job = 1; +} + +// ScheduleJobResponse is the message response to create/schedule the job. +message ScheduleJobResponse { + // Empty +} + +// GetJobRequest is the message to retrieve a job. +message GetJobRequest { + // The name of the job. + string name = 1; +} + +// GetJobResponse is the message's response for a job retrieved. +message GetJobResponse { + // The job details. + Job job = 1; +} + +// DeleteJobRequest is the message to delete the job by name. +message DeleteJobRequest { + // The name of the job. + string name = 1; +} + +// DeleteJobResponse is the message response to delete the job by name. +message DeleteJobResponse { + // Empty +} \ No newline at end of file diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index 9092b101a..990aa42db 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -17,10 +17,6 @@ - - - - diff --git a/src/Shared/ArgumentVerifier.cs b/src/Shared/ArgumentVerifier.cs deleted file mode 100644 index 907543f01..000000000 --- a/src/Shared/ArgumentVerifier.cs +++ /dev/null @@ -1,60 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2021 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -// TODO: Remove this when every project that uses this file has nullable enabled. -#nullable enable - -namespace Dapr -{ - using System; - using System.Diagnostics.CodeAnalysis; - - /// - /// A utility class to perform argument validations. - /// - internal static class ArgumentVerifier - { - /// - /// Throws ArgumentNullException if argument is null. - /// - /// Argument value to check. - /// Name of Argument. - public static void ThrowIfNull([NotNull] object? value, string name) - { - if (value == null) - { - throw new ArgumentNullException(name); - } - } - - /// - /// Validates string and throws: - /// ArgumentNullException if argument is null. - /// ArgumentException if argument is empty. - /// - /// Argument value to check. - /// Name of Argument. - public static void ThrowIfNullOrEmpty([NotNull] string? value, string name) - { - if (value == null) - { - throw new ArgumentNullException(name); - } - - if (string.IsNullOrEmpty(value)) - { - throw new ArgumentException("The value cannot be null or empty", name); - } - } - } -} diff --git a/src/Shared/DaprDefaults.cs b/src/Shared/DaprDefaults.cs deleted file mode 100644 index b738de921..000000000 --- a/src/Shared/DaprDefaults.cs +++ /dev/null @@ -1,103 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2021 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -using System; - -namespace Dapr -{ - internal static class DaprDefaults - { - private static string httpEndpoint = string.Empty; - private static string grpcEndpoint = string.Empty; - private static string daprApiToken = string.Empty; - private static string appApiToken = string.Empty; - - /// - /// Get the value of environment variable DAPR_API_TOKEN - /// - /// The value of environment variable DAPR_API_TOKEN - public static string GetDefaultDaprApiToken() - { - // Lazy-init is safe because this is just populating the default - // We don't plan to support the case where the user changes environment variables - // for a running process. - if (string.IsNullOrEmpty(daprApiToken)) - { - // Treat empty the same as null since it's an environment variable - var value = Environment.GetEnvironmentVariable("DAPR_API_TOKEN"); - daprApiToken = string.IsNullOrEmpty(value) ? string.Empty : value; - } - - return daprApiToken; - } - - /// - /// Get the value of environment variable APP_API_TOKEN - /// - /// The value of environment variable APP_API_TOKEN - public static string GetDefaultAppApiToken() - { - if (string.IsNullOrEmpty(appApiToken)) - { - var value = Environment.GetEnvironmentVariable("APP_API_TOKEN"); - appApiToken = string.IsNullOrEmpty(value) ? string.Empty : value; - } - - return appApiToken; - } - - /// - /// Get the value of HTTP endpoint based off environment variables - /// - /// The value of HTTP endpoint based off environment variables - public static string GetDefaultHttpEndpoint() - { - if (string.IsNullOrEmpty(httpEndpoint)) - { - var endpoint = Environment.GetEnvironmentVariable("DAPR_HTTP_ENDPOINT"); - if (!string.IsNullOrEmpty(endpoint)) { - httpEndpoint = endpoint; - return httpEndpoint; - } - - var port = Environment.GetEnvironmentVariable("DAPR_HTTP_PORT"); - port = string.IsNullOrEmpty(port) ? "3500" : port; - httpEndpoint = $"http://127.0.0.1:{port}"; - } - - return httpEndpoint; - } - - /// - /// Get the value of gRPC endpoint based off environment variables - /// - /// The value of gRPC endpoint based off environment variables - public static string GetDefaultGrpcEndpoint() - { - if (string.IsNullOrEmpty(grpcEndpoint)) - { - var endpoint = Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT"); - if (!string.IsNullOrEmpty(endpoint)) { - grpcEndpoint = endpoint; - return grpcEndpoint; - } - - var port = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); - port = string.IsNullOrEmpty(port) ? "50001" : port; - grpcEndpoint = $"http://127.0.0.1:{port}"; - } - - return grpcEndpoint; - } - } -} diff --git a/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj b/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj index 8852dd465..4ad43f220 100644 --- a/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj +++ b/test/Dapr.Actors.Test/Dapr.Actors.Test.csproj @@ -10,7 +10,7 @@ all - + diff --git a/test/Dapr.AspNetCore.IntegrationTest/StateTestClient.cs b/test/Dapr.AspNetCore.IntegrationTest/StateTestClient.cs index f9ad63b7b..43edc3227 100644 --- a/test/Dapr.AspNetCore.IntegrationTest/StateTestClient.cs +++ b/test/Dapr.AspNetCore.IntegrationTest/StateTestClient.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. diff --git a/test/Dapr.Client.Test/ArgumentVerifierTest.cs b/test/Dapr.Common.Test/ArgumentVerifierTest.cs similarity index 93% rename from test/Dapr.Client.Test/ArgumentVerifierTest.cs rename to test/Dapr.Common.Test/ArgumentVerifierTest.cs index c839ac3eb..27515018d 100644 --- a/test/Dapr.Client.Test/ArgumentVerifierTest.cs +++ b/test/Dapr.Common.Test/ArgumentVerifierTest.cs @@ -1,4 +1,4 @@ -// ------------------------------------------------------------------------ +// ------------------------------------------------------------------------ // Copyright 2021 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -11,11 +11,12 @@ // limitations under the License. // ------------------------------------------------------------------------ -namespace Dapr.Client.Test +namespace Dapr.Common.Test { using System; using Xunit; - + + public class ArgumentVerifierTest { [Fact] diff --git a/test/Dapr.Common.Test/Dapr.Common.Test.csproj b/test/Dapr.Common.Test/Dapr.Common.Test.csproj new file mode 100644 index 000000000..9f9787b8b --- /dev/null +++ b/test/Dapr.Common.Test/Dapr.Common.Test.csproj @@ -0,0 +1,21 @@ + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + \ No newline at end of file diff --git a/test/Dapr.Common.Test/DaprGenericBuilderTests.cs b/test/Dapr.Common.Test/DaprGenericBuilderTests.cs new file mode 100644 index 000000000..28faeda83 --- /dev/null +++ b/test/Dapr.Common.Test/DaprGenericBuilderTests.cs @@ -0,0 +1,38 @@ +using Xunit; + +namespace Dapr.Common.Test +{ + public class DaprGenericBuilderTests + { + [Fact] + public void ShouldUseValuesFromTheEnvironmentVariables() + { + var builder = new MockDaprBuilder(); + + var grpcEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); + var httpEndpoint = DaprDefaults.GetDefaultHttpEndpoint(); + var apiToken = DaprDefaults.GetDefaultDaprApiToken(); + + Assert.Equal(grpcEndpoint, builder.GrpcEndpoint); + Assert.Equal(httpEndpoint, builder.HttpEndpoint); + Assert.Equal(apiToken, builder.DaprApiToken); + } + + [Fact] + public void ShouldUseConfiguredValuesAtRegistration() + { + const string httpEndpoint = "https://http.abc123.com"; + const string grpcEndpoint = "https://grpc.abc123.com"; + const string apiToken = "abc123"; + + var builder = new MockDaprBuilder(); + builder.UseHttpEndpoint(httpEndpoint); + builder.UseGrpcEndpoint(grpcEndpoint); + builder.UseDaprApiToken(apiToken); + + Assert.Equal(httpEndpoint, builder.HttpEndpoint); + Assert.Equal(grpcEndpoint, builder.GrpcEndpoint); + Assert.Equal(apiToken, builder.DaprApiToken); + } + } +} diff --git a/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs b/test/Dapr.Common.Test/Extensions/EnumExtensionTest.cs similarity index 95% rename from test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs rename to test/Dapr.Common.Test/Extensions/EnumExtensionTest.cs index 83c4354f9..5ddddb476 100644 --- a/test/Dapr.Client.Test/Extensions/EnumExtensionTest.cs +++ b/test/Dapr.Common.Test/Extensions/EnumExtensionTest.cs @@ -1,7 +1,7 @@ using System.Runtime.Serialization; using Xunit; -namespace Dapr.Client.Test.Extensions +namespace Dapr.Common.Test.Extensions { public class EnumExtensionTest { diff --git a/test/Dapr.Common.Test/MockDaprBuilder.cs b/test/Dapr.Common.Test/MockDaprBuilder.cs new file mode 100644 index 000000000..00a383664 --- /dev/null +++ b/test/Dapr.Common.Test/MockDaprBuilder.cs @@ -0,0 +1,27 @@ +using System; + +namespace Dapr.Common.Test +{ + public sealed class MockDaprBuilder : DaprGenericClientBuilder + { + public MockDaprBuilder() + { + } + + /// + /// Builds the client instance from the properties of the builder. + /// + /// The Dapr client instance. + /// + /// Builds the client instance from the properties of the builder. + /// + public override MockDaprClient Build() + { + throw new NotImplementedException(); + } + } + + public sealed class MockDaprClient + { + } +} diff --git a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj index 7d11d5c40..0bfab6567 100644 --- a/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj +++ b/test/Dapr.Extensions.Configuration.Test/Dapr.Extensions.Configuration.Test.csproj @@ -25,7 +25,7 @@ - + \ No newline at end of file diff --git a/test/Dapr.Jobs.Test/CronExpressionBuilderTests.cs b/test/Dapr.Jobs.Test/CronExpressionBuilderTests.cs new file mode 100644 index 000000000..38031e0eb --- /dev/null +++ b/test/Dapr.Jobs.Test/CronExpressionBuilderTests.cs @@ -0,0 +1,386 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using Xunit; +using ArgumentException = System.ArgumentException; + +namespace Dapr.Jobs.Test; + +public sealed class CronExpressionBuilderTests +{ + [Fact] + public void WildcardByDefault() + { + var builder = new CronExpressionBuilder(); + var result = builder.ToString(); + Assert.Equal("* * * * * *", result); + } + + [Fact] + public void WildcardByAssertion() + { + var builder = new CronExpressionBuilder() + .Each(CronPeriod.Second) + .Each(CronPeriod.Minute) + .Each(CronPeriod.Hour) + .Each(CronPeriod.DayOfWeek) + .Each(CronPeriod.DayOfMonth) + .Each(CronPeriod.Month); + var result = builder.ToString(); + Assert.Equal("* * * * * *", result); + } + + [Fact] + public void OnVariations() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 5) + .On(OnCronPeriod.Minute, 12) + .On(OnCronPeriod.Hour, 16) + .On(OnCronPeriod.DayOfMonth, 7); + var result = builder.ToString(); + Assert.Equal("5 12 16 7 * *", result); + } + + [Fact] + public void BottomOfEveryMinute() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 30); + var result = builder.ToString(); + Assert.Equal("30 * * * * *", result); + } + + [Fact] + public void EveryFiveSeconds() + { + var builder = new CronExpressionBuilder() + .Every(EveryCronPeriod.Second, 5); + var result = builder.ToString(); + Assert.Equal("*/5 * * * * *", result); + } + + [Fact] + public void BottomOfEveryHour() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 0) + .On(OnCronPeriod.Minute, 30); + var result = builder.ToString(); + Assert.Equal("0 30 * * * *", result); + } + + [Fact] + public void EveryTwelveMinutes() + { + var builder = new CronExpressionBuilder() + .Every(EveryCronPeriod.Minute, 12); + var result = builder.ToString(); + Assert.Equal("* */12 * * * *", result); + } + + [Fact] + public void EveryHour() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 0) + .On(OnCronPeriod.Minute, 0); + var result = builder.ToString(); + Assert.Equal("0 0 * * * *", result); + } + + [Fact] + public void EveryFourHours() + { + var builder = new CronExpressionBuilder() + .Every(EveryCronPeriod.Hour, 4); + var result = builder.ToString(); + Assert.Equal("* * */4 * * *", result); + } + + [Fact] + public void EveryOtherMonth() + { + var builder = new CronExpressionBuilder() + .Every(EveryCronPeriod.Month, 2); + var result = builder.ToString(); + Assert.Equal("* * * * */2 *", result); + } + + [Fact] + public void EachMonth() + { + var builder = new CronExpressionBuilder() + .Every(EveryCronPeriod.Month, 4) + .Each(CronPeriod.Month); + var result = builder.ToString(); + Assert.Equal("* * * * * *", result); + } + + [Fact] + public void EveryDayAtMidnight() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 0) + .On(OnCronPeriod.Minute, 0) + .On(OnCronPeriod.Hour, 0); + var result = builder.ToString(); + Assert.Equal("0 0 0 * * *", result); + } + + [Fact] + public void EveryFourthDayInJanAprAugAndDecIfTheDayIsWednesdayOrFriday() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 30) + .On(OnCronPeriod.Minute, 15) + .On(OnCronPeriod.Hour, 6) + .Every(EveryCronPeriod.DayInMonth, 4) + .On(MonthOfYear.January, MonthOfYear.April, MonthOfYear.August, MonthOfYear.December) + .On(DayOfWeek.Wednesday, DayOfWeek.Friday); + var result = builder.ToString(); + Assert.Equal("30 15 6 */4 JAN,APR,AUG,DEC WED,FRI", result); + } + + [Fact] + public void EveryValidation() + { + var builder = new CronExpressionBuilder() + .Every(EveryCronPeriod.Second, 10) + .Every(EveryCronPeriod.Minute, 8) + .Every(EveryCronPeriod.Hour, 2) + .Every(EveryCronPeriod.DayInMonth, 5) + .Every(EveryCronPeriod.DayInWeek, 2) + .Every(EveryCronPeriod.Month, 3); + var result = builder.ToString(); + Assert.Equal("*/10 */8 */2 */5 */3 */2", result); + } + + [Fact] + public void EveryDayAtNoon() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 0) + .On(OnCronPeriod.Minute, 0) + .On(OnCronPeriod.Hour, 12); + var result = builder.ToString(); + Assert.Equal("0 0 12 * * *", result); + } + + [Fact] + public void MidnightOnTuesdaysAndFridays() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 0) + .On(OnCronPeriod.Minute, 0) + .On(OnCronPeriod.Hour, 0) + .On(DayOfWeek.Tuesday, DayOfWeek.Friday); + var result = builder.ToString(); + Assert.Equal("0 0 0 * * TUE,FRI", result); + } + + [Fact] + public void FourThirtyPmOnWednesdayThroughSaturdayFromOctoberToDecember() + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, 0) + .On(OnCronPeriod.Minute, 30) + .On(OnCronPeriod.Hour, 16) + .Through(DayOfWeek.Wednesday, DayOfWeek.Saturday) + .Through(MonthOfYear.October, MonthOfYear.December); + var result = builder.ToString(); + Assert.Equal("0 30 16 * OCT-DEC WED-SAT", result); + } + + [Fact] + public void ThroughFirstAvailableUnits() + { + var builder = new CronExpressionBuilder() + .Through(ThroughCronPeriod.Second, 0, 15) + .Through(ThroughCronPeriod.Minute, 0, 15) + .Through(ThroughCronPeriod.Hour, 0, 15) + .Through(ThroughCronPeriod.DayOfMonth, 1, 10) + .Through(ThroughCronPeriod.Month, 0, 8); + var result = builder.ToString(); + Assert.Equal("0-15 0-15 0-15 1-10 0-8 *", result); + } + + [Fact] + public void ShouldThrowIfIntervalIsBelowRange() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .Every(EveryCronPeriod.Minute, -5); + }); + } + + [Fact] + public void ShouldThrowIfRangeValuesAreEqual() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .Through(ThroughCronPeriod.Hour, 8, 8); + }); + } + + [Fact] + public void ShouldThrowIfRangeValuesAreInDescendingOrder() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .Through(MonthOfYear.December, MonthOfYear.February); + }); + } + + [Fact] + public void ShouldThrowIfRangedMonthsAreEqual() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .Through(MonthOfYear.April, MonthOfYear.April); + }); + } + + [Fact] + public void ShouldThrowIfRangedMonthsAreInDescendingOrder() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .Through(ThroughCronPeriod.Minute, 10, 5); + }); + } + + [Fact] + public void ShouldThrowIfRangedDaysAreEqualInRange() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .Through(DayOfWeek.Thursday, DayOfWeek.Thursday); + }); + } + + [Fact] + public void ShouldThrowIfRangedDaysAreInDescendingOrder() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .Through(DayOfWeek.Thursday, DayOfWeek.Monday); + }); + } + + [Fact] + public void ShouldThrowIfOnValuesAreBelowRange() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Second, -2); + }); + } + + [Fact] + public void ShouldThrowIfOnValuesAreBelowRange2() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Hour, -10); + }); + } + + [Fact] + public void ShouldThrowIfOnValuesAreBelowRange3() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.DayOfMonth, -5); + }); + } + + [Fact] + public void ShouldThrowIfOnValuesAreAboveRange() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.Minute, 60); + }); + } + + [Fact] + public void ShouldThrowIfOnValuesAreAboveRange2() + { + Assert.Throws(() => + { + var builder = new CronExpressionBuilder() + .On(OnCronPeriod.DayOfMonth, 32); + }); + } + + [Theory] + [InlineData("* * * * *", false)] + [InlineData("* * * * * *", true)] + [InlineData("5 12 16 7 * *", true)] + [InlineData("30 * * * * *", true)] + [InlineData("*/5 * * * * *", true)] + [InlineData("0 30 * * * *", true)] + [InlineData("* */12 * * * *", true)] + [InlineData("0 0 * * * *", true)] + [InlineData("* * */4 * * *", true)] + [InlineData("* * * * */2 *", true)] + [InlineData("0 0 0 * * *", true)] + [InlineData("30 15 6 */4 JAN,APR,AUG WED,FRI", true)] + [InlineData("*/10 */8 */2 */5 */3 *", true)] + [InlineData("0 0 12 * * *", true)] + [InlineData("0 0 0 * * TUE,FRI", true)] + [InlineData("0 0 0 * * TUE", true)] + [InlineData("0 0 0 * * TUE-FRI", true)] + [InlineData("0 30 16 * OCT SAT", true)] + [InlineData("0 30 16 * OCT,DEC WED,SAT", true)] + [InlineData("0 30 16 * OCT-DEC WED-SAT", true)] + [InlineData("0-15 * * * * *", true)] + [InlineData("0-15 02-59 * * * *", true)] + [InlineData("0-15 02-59 07-23 * * *", true)] + [InlineData("0-15 0-15 0-15 1-10 8-16 *", true)] + [InlineData("5 12 16 7 FEB *", true)] + [InlineData("5 12 16 7 * MON", true)] + [InlineData("5 12 16 7 JAN SAT", true)] + [InlineData("5 * * * FEB SUN", true)] + [InlineData("* * */2 * * *", true)] + [InlineData("* * * */5 * *", true)] + [InlineData("0,01,3 0,01,2 0,01,2 00,1,02 JAN,FEB,MAR,APR SUN,MON,TUE,WED", true)] + [InlineData("* * * * JAN,FEB,MAR,APR,MAY,JUN,JUL,AUG,SEP,OCT,NOV,DEC SUN,MON,TUE,WED,THU,FRI,SAT", true)] + [InlineData("30 15 6 */4 JAN,APR,AUG WED-FRI", true)] + [InlineData("*/10 */8 */2 */5 */3 */2", true)] + [InlineData("0 0 0 * OCT SAT", true)] + [InlineData("0 0 0 * OCT,DEC WED,SAT", true)] + [InlineData("0 0 0 * OCT-DEC WED-SAT", true)] + [InlineData("1-14 2-59 20-23 * * *", true)] + [InlineData("00-59 0-59 00-23 1-31 JAN-DEC SUN-SAT", true)] + [InlineData("0-59 0-59 0-23 1-31 1-12 0-6", true)] + [InlineData("*/1 2,4,5 * 2-9 JAN,FEB,DEC MON-WED", true)] + public void ValidateCronExpression(string cronValue, bool isValid) + { + var result = CronExpressionBuilder.IsCronExpression(cronValue); + Assert.Equal(result, isValid); + } +} diff --git a/test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj b/test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj new file mode 100644 index 000000000..f8b3b50fb --- /dev/null +++ b/test/Dapr.Jobs.Test/Dapr.Jobs.Test.csproj @@ -0,0 +1,22 @@ + + + + + runtime; build; native; contentfiles; analyzers; buildtransitive + all + + + + + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + + + \ No newline at end of file diff --git a/test/Dapr.Jobs.Test/DaprJobsClientBuilderTest.cs b/test/Dapr.Jobs.Test/DaprJobsClientBuilderTest.cs new file mode 100644 index 000000000..6c150bff6 --- /dev/null +++ b/test/Dapr.Jobs.Test/DaprJobsClientBuilderTest.cs @@ -0,0 +1,122 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.Text.Json; +using Grpc.Net.Client; +using Xunit; + +namespace Dapr.Jobs.Test; + +public class DaprJobsClientBuilderTest +{ + [Fact] + public void DaprClientBuilder_UsesPropertyNameCaseHandlingInsensitiveByDefault() + { + DaprJobsClientBuilder builder = new DaprJobsClientBuilder(); + Assert.True(builder.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + + [Fact] + public void DaprJobsClientBuilder_UsesPropertyNameCaseHandlingAsSpecified() + { + var builder = new DaprJobsClientBuilder(); + builder.UseJsonSerializationOptions(new JsonSerializerOptions + { + PropertyNameCaseInsensitive = false + }); + Assert.False(builder.JsonSerializerOptions.PropertyNameCaseInsensitive); + } + + [Fact] + public void DaprJobsClientBuilder_UsesThrowOperationCanceledOnCancellation_ByDefault() + { + var builder = new DaprJobsClientBuilder(); + var daprClient = builder.Build(); + Assert.True(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); + } + + [Fact] + public void DaprJobsClientBuilder_DoesNotOverrideUserGrpcChannelOptions() + { + var builder = new DaprJobsClientBuilder(); + var daprClient = builder.UseGrpcChannelOptions(new GrpcChannelOptions()).Build(); + Assert.False(builder.GrpcChannelOptions.ThrowOperationCanceledOnCancellation); + } + + [Fact] + public void DaprJobsClientBuilder_ValidatesGrpcEndpointScheme() + { + var builder = new DaprJobsClientBuilder(); + builder.UseGrpcEndpoint("ftp://example.com"); + + var ex = Assert.Throws(() => builder.Build()); + Assert.Equal("The gRPC endpoint must use http or https.", ex.Message); + } + + [Fact] + public void DaprJobsClientBuilder_ValidatesHttpEndpointScheme() + { + var builder = new DaprJobsClientBuilder(); + builder.UseHttpEndpoint("ftp://example.com"); + + var ex = Assert.Throws(() => builder.Build()); + Assert.Equal("The HTTP endpoint must use http or https.", ex.Message); + } + + [Fact] + public void DaprJobsClientBuilder_SetsApiToken() + { + var builder = new DaprJobsClientBuilder(); + builder.UseDaprApiToken("test_token"); + builder.Build(); + Assert.Equal("test_token", builder.DaprApiToken); + } + + [Fact] + public void DaprJobsClientBuilder_SetsNullApiToken() + { + var builder = new DaprJobsClientBuilder(); + builder.UseDaprApiToken(null); + builder.Build(); + Assert.Null(builder.DaprApiToken); + } + + [Fact] + public void DaprJobsClientBuilder_ApiTokenSet_SetsApiTokenHeader() + { + var builder = new DaprJobsClientBuilder(); + builder.UseDaprApiToken("test_token"); + + var entry = DaprJobsClient.GetDaprApiTokenHeader(builder.DaprApiToken); + Assert.NotNull(entry); + Assert.Equal("test_token", entry.Value.Value); + } + + [Fact] + public void DaprJobsClientBuilder_ApiTokenNotSet_EmptyApiTokenHeader() + { + var builder = new DaprJobsClientBuilder(); + var entry = DaprJobsClient.GetDaprApiTokenHeader(builder.DaprApiToken); + Assert.Equal(default, entry); + } + + [Fact] + public void DaprJobsClientBuilder_SetsTimeout() + { + var builder = new DaprJobsClientBuilder(); + builder.UseTimeout(TimeSpan.FromSeconds(2)); + builder.Build(); + Assert.Equal(2, builder.Timeout.Seconds); + } +} diff --git a/test/Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs b/test/Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs new file mode 100644 index 000000000..b9f31b9b5 --- /dev/null +++ b/test/Dapr.Jobs.Test/DaprJobsGrpcClientTests.cs @@ -0,0 +1,160 @@ +using System; +using System.Net.Http; +using Dapr.Jobs.Models; +using Moq; +using Xunit; + +namespace Dapr.Jobs.Test; + +public sealed class DaprJobsGrpcClientTests +{ + + [Fact] + public void ScheduleJobAsync_RepeatsCannotBeLessThanZero() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + await client.ScheduleJobAsync("MyJob", DaprJobSchedule.Daily, null, null, -5, null, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void ScheduleJobAsync_JobNameCannotBeNull() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + await client.ScheduleJobAsync(null, DaprJobSchedule.Daily, null, null, -5, null, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void ScheduleJobAsync_JobNameCannotBeEmpty() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + await client.ScheduleJobAsync(string.Empty, DaprJobSchedule.Daily, null, null, -5, null, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void ScheduleJobAsync_ScheduleCannotBeEmpty() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + await client.ScheduleJobAsync("MyJob", new DaprJobSchedule(string.Empty), null, null, -5, null, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void ScheduleJobAsync_TtlCannotBeEarlierThanStartingFrom() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + var date = DateTime.UtcNow.AddDays(10); + var earlierDate = date.AddDays(-2); + + await client.ScheduleJobAsync("MyJob", DaprJobSchedule.Daily, null, date, null, earlierDate, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void GetJobAsync_NameCannotBeNull() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + await client.GetJobAsync(null, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void GetJobAsync_NameCannotBeEmpty() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + await client.GetJobAsync(string.Empty, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void DeleteJobAsync_NameCannotBeNull() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + await client.DeleteJobAsync(null, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + [Fact] + public void DeleteJobAsync_NameCannotBeEmpty() + { + var mockClient = Mock.Of(); + var httpClient = Mock.Of(); + + var client = new DaprJobsGrpcClient(mockClient, httpClient, null); + +#pragma warning disable CS0618 // Type or member is obsolete + var result = Assert.ThrowsAsync(async () => + { + await client.DeleteJobAsync(string.Empty, default); + }); +#pragma warning restore CS0618 // Type or member is obsolete + } + + private sealed record TestPayload(string Name, string Color); +} diff --git a/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTest.cs b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTest.cs new file mode 100644 index 000000000..febd6ff13 --- /dev/null +++ b/test/Dapr.Jobs.Test/Extensions/DaprJobsServiceCollectionExtensionsTest.cs @@ -0,0 +1,86 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.Net.Http; +using Dapr.Jobs.Extensions; +using Microsoft.Extensions.DependencyInjection; +using Xunit; + +namespace Dapr.Jobs.Test.Extensions; + +public class DaprJobsServiceCollectionExtensionsTest +{ + [Fact] + public void AddDaprJobsClient_RegistersDaprClientOnlyOnce() + { + var services = new ServiceCollection(); + + var clientBuilder = new Action(builder => + builder.UseDaprApiToken("abc")); + + services.AddDaprJobsClient(); //Sets a default API token value of an empty string + services.AddDaprJobsClient(clientBuilder); //Sets the API token value + + var serviceProvider = services.BuildServiceProvider(); + var daprJobClient = serviceProvider.GetService() as DaprJobsGrpcClient; + + Assert.Null(daprJobClient!.apiTokenHeader); + Assert.False(daprJobClient.httpClient.DefaultRequestHeaders.TryGetValues("dapr-api-token", out var _)); + } + + [Fact] + public void AddDaprJobsClient_RegistersIHttpClientFactory() + { + var services = new ServiceCollection(); + + services.AddDaprJobsClient(); + + var serviceProvider = services.BuildServiceProvider(); + + var httpClientFactory = serviceProvider.GetService(); + Assert.NotNull(httpClientFactory); + + var daprJobsClient = serviceProvider.GetService(); + Assert.NotNull(daprJobsClient); + } + + [Fact] + public void AddDaprJobsClient_RegistersUsingDependencyFromIServiceProvider() + { + var services = new ServiceCollection(); + services.AddSingleton(); + services.AddDaprJobsClient((provider, builder) => + { + var configProvider = provider.GetRequiredService(); + var daprApiToken = configProvider.GetApiTokenValue(); + + builder.UseDaprApiToken(daprApiToken); + }); + + var serviceProvider = services.BuildServiceProvider(); + var client = serviceProvider.GetRequiredService() as DaprJobsGrpcClient; + + //Validate it's set on the GrpcClient - note that it doesn't get set on the HttpClient + Assert.NotNull(client); + Assert.NotNull(client.apiTokenHeader); + Assert.True(client.apiTokenHeader.HasValue); + Assert.Equal("dapr-api-token", client.apiTokenHeader.Value.Key); + Assert.Equal("abcdef", client.apiTokenHeader.Value.Value); + } + + private class TestSecretRetriever + { + public string GetApiTokenValue() => "abcdef"; + } +} diff --git a/test/Dapr.Jobs.Test/Extensions/EndpointRouteBuilderExtensionsTest.cs b/test/Dapr.Jobs.Test/Extensions/EndpointRouteBuilderExtensionsTest.cs new file mode 100644 index 000000000..17a9ed19c --- /dev/null +++ b/test/Dapr.Jobs.Test/Extensions/EndpointRouteBuilderExtensionsTest.cs @@ -0,0 +1,41 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +namespace Dapr.Jobs.Test.Extensions; + +public class EndpointRouteBuilderExtensionsTest +{ + //The following won't work because Moq can't test static methods - leaving here should the project ever adopt some sort of commercial tool that enables such testing + //[Fact] + //public void MapDaprScheduledJobs_ValidateRegisteredRoute() + //{ + // var endpoints = new Mock(); + // endpoints.Setup(a => a.DataSources).Returns(new List()); //While .NET 9 changed this behavior, .NET 8 and lower will throw if this isn't setup on the mock + + // const string jobName = "test-job"; + // var handler = (JobDetails details) => + // { + // //Doesn't matter what it does here + // }; + + // var result = endpoints.Object.MapDaprScheduledJob(jobName, handler); + + // var registeredRoutes = endpoints.Invocations + // .Where(invocation => invocation.Method.Name == "MapPost") + // .ToList(); + + // Assert.Single(registeredRoutes); //One MapPost call + // Assert.Equal($"job/{jobName}", registeredRoutes[0].Arguments[0]); //Validate the route + // Assert.Equal(handler, registeredRoutes[0].Arguments[1]); //Validate the handler delegate + //} +} diff --git a/test/Dapr.Jobs.Test/Extensions/StringExtensionsTests.cs b/test/Dapr.Jobs.Test/Extensions/StringExtensionsTests.cs new file mode 100644 index 000000000..8c25de115 --- /dev/null +++ b/test/Dapr.Jobs.Test/Extensions/StringExtensionsTests.cs @@ -0,0 +1,46 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.Collections.Generic; +using Dapr.Jobs.Extensions; +using Xunit; + +namespace Dapr.Jobs.Test.Extensions; + +public class StringExtensionsTests +{ + [Fact] + public void EndsWithAny_ContainsMatch() + { + const string testValue = "@weekly"; + var result = testValue.EndsWithAny(new List + { + "every", + "monthly", + "weekly", + "daily", + "midnight", + "hourly" + }, StringComparison.InvariantCulture); + Assert.True(result); + } + + [Fact] + public void EndsWithAny_DoesNotContainMatch() + { + const string testValue = "@weekly"; + var result = testValue.EndsWithAny(new List { "every", "monthly", "daily", "midnight", "hourly" }, StringComparison.InvariantCulture); + Assert.False(result); + } +} diff --git a/test/Dapr.Jobs.Test/Extensions/TimeSpanExtensionsTest.cs b/test/Dapr.Jobs.Test/Extensions/TimeSpanExtensionsTest.cs new file mode 100644 index 000000000..1e888a841 --- /dev/null +++ b/test/Dapr.Jobs.Test/Extensions/TimeSpanExtensionsTest.cs @@ -0,0 +1,146 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using Xunit; + +namespace Dapr.Jobs.Test.Extensions; + +public class TimeSpanExtensionsTest +{ + [Theory] + [InlineData("5h", true)] + [InlineData("5m", true)] + [InlineData("10s", true)] + [InlineData("30q", true)] + [InlineData("5h2m", true)] + [InlineData("2m44s", true)] + [InlineData("49s28q", true)] + [InlineData("21m2s9q", true)] + [InlineData("9h17m10s55q", true)] + [InlineData("12z", false)] + [InlineData("60ms", true)] + [InlineData("", false)] + public void IsDurationString_Validate(string original, bool expectedResult) + { + var actualResult = original.IsDurationString(); + Assert.Equal(expectedResult, actualResult); + } + + [Fact] + public void ToDurationString_ValidateHours() + { + var fourHours = TimeSpan.FromHours(4); + var result = fourHours.ToDurationString(); + + Assert.Equal("4h", result); + } + + [Fact] + public void ToDurationString_ValidateMinutes() + { + var elevenMinutes = TimeSpan.FromMinutes(11); + var result = elevenMinutes.ToDurationString(); + + Assert.Equal("11m", result); + } + + [Fact] + public void ToDurationString_ValidateSeconds() + { + var fortySeconds = TimeSpan.FromSeconds(40); + var result = fortySeconds.ToDurationString(); + + Assert.Equal("40s", result); + } + + [Fact] + public void ToDurationString_ValidateMilliseconds() + { + var tenMilliseconds = TimeSpan.FromMilliseconds(10); + var result = tenMilliseconds.ToDurationString(); + + Assert.Equal("10ms", result); + } + + [Fact] + public void ToDurationString_HoursAndMinutes() + { + var ninetyMinutes = TimeSpan.FromMinutes(90); + var result = ninetyMinutes.ToDurationString(); + + Assert.Equal("1h30m", result); + } + + [Fact] + public void ToDurationString_Combined() + { + var time = TimeSpan.FromHours(2) + TimeSpan.FromMinutes(4) + TimeSpan.FromSeconds(24) + + TimeSpan.FromMilliseconds(28); + var result = time.ToDurationString(); + + Assert.Equal("2h4m24s28ms", result); + } + + [Fact] + public void FromDurationString_AllSegments() + { + const string interval = "13h57m4s10ms"; + var result = interval.FromDurationString(); + + Assert.Equal(13, result.Hours); + Assert.Equal(57, result.Minutes); + Assert.Equal(4, result.Seconds); + Assert.Equal(10, result.Milliseconds); + } + + [Fact] + public void FromDurationString_LimitedSegments1() + { + const string interval = "5h12ms"; + var result = interval.FromDurationString(); + + Assert.Equal(5, result.Hours); + Assert.Equal(12, result.Milliseconds); + } + + [Fact] + public void FromDurationString_LimitedSegments2() + { + const string interval = "5m"; + var result = interval.FromDurationString(); + + Assert.Equal(5, result.Minutes); + } + + [Fact] + public void FromDurationString_LimitedSegments3() + { + const string interval = "16s43ms"; + var result = interval.FromDurationString(); + + Assert.Equal(16, result.Seconds); + Assert.Equal(43, result.Milliseconds); + } + + [Fact] + public void FromDurationString_LimitedSegments4() + { + const string interval = "4h32m16s"; + var result = interval.FromDurationString(); + + Assert.Equal(4, result.Hours); + Assert.Equal(32, result.Minutes); + Assert.Equal(16, result.Seconds); + } +} diff --git a/test/Dapr.Jobs.Test/Models/DaprJobScheduleTests.cs b/test/Dapr.Jobs.Test/Models/DaprJobScheduleTests.cs new file mode 100644 index 000000000..192f0659c --- /dev/null +++ b/test/Dapr.Jobs.Test/Models/DaprJobScheduleTests.cs @@ -0,0 +1,159 @@ +using System; +using Dapr.Jobs.Models; +using Xunit; + +namespace Dapr.Jobs.Test.Models; + +public sealed class DaprJobScheduleTests +{ + [Fact] + public void FromDuration_Validate() + { + var schedule = DaprJobSchedule.FromDuration(new TimeSpan(12, 8, 16)); + Assert.Equal("12h8m16s", schedule.ExpressionValue); + } + + [Fact] + public void FromExpression_Cron() + { + const string cronExpression = "*/5 1-5 * * JAN,FEB WED-SAT"; + + var schedule = DaprJobSchedule.FromExpression(cronExpression); + Assert.Equal(cronExpression, schedule.ExpressionValue); + } + + [Fact] + public void FromExpression_PrefixedPeriod_Yearly() + { + var schedule = DaprJobSchedule.Yearly; + Assert.Equal("@yearly", schedule.ExpressionValue); + } + + [Fact] + public void FromExpression_PrefixedPeriod_Monthly() + { + var schedule = DaprJobSchedule.Monthly; + Assert.Equal("@monthly", schedule.ExpressionValue); + } + + [Fact] + public void FromExpression_PrefixedPeriod_Weekly() + { + var schedule = DaprJobSchedule.Weekly; + Assert.Equal("@weekly", schedule.ExpressionValue); + } + + [Fact] + public void FromExpression_PrefixedPeriod_Daily() + { + var schedule = DaprJobSchedule.Daily; + Assert.Equal("@daily", schedule.ExpressionValue); + } + + [Fact] + public void FromExpression_PrefixedPeriod_Midnight() + { + var schedule = DaprJobSchedule.Midnight; + Assert.Equal("@midnight", schedule.ExpressionValue); + } + + [Fact] + public void FromExpression_PrefixedPeriod_Hourly() + { + var schedule = DaprJobSchedule.Hourly; + Assert.Equal("@hourly", schedule.ExpressionValue); + } + + [Fact] + public void FromCronExpression() + { + var schedule = DaprJobSchedule.FromCronExpression(new CronExpressionBuilder() + .On(OnCronPeriod.Second, 15) + .Every(EveryCronPeriod.Minute, 2) + .Every(EveryCronPeriod.Hour, 4) + .Through(ThroughCronPeriod.DayOfMonth, 2, 13) + .Through(DayOfWeek.Monday, DayOfWeek.Saturday) + .On(MonthOfYear.June, MonthOfYear.August, MonthOfYear.January)); + + Assert.Equal("15 */2 */4 2-13 JAN,JUN,AUG MON-SAT", schedule.ExpressionValue); + } + + [Fact] + public void IsPointInTimeExpression() + { + var schedule = DaprJobSchedule.FromDateTime(DateTimeOffset.UtcNow.AddDays(2)); + Assert.True(schedule.IsPointInTimeExpression); + Assert.False(schedule.IsDurationExpression); + Assert.False(schedule.IsCronExpression); + Assert.False(schedule.IsPrefixedPeriodExpression); + } + + [Fact] + public void IsDurationExpression() + { + var schedule = DaprJobSchedule.FromDuration(TimeSpan.FromHours(2)); + Assert.True(schedule.IsDurationExpression); + Assert.False(schedule.IsPointInTimeExpression); + Assert.False(schedule.IsCronExpression); + Assert.False(schedule.IsPrefixedPeriodExpression); + } + + [Fact] + public void IsPrefixedPeriodExpression() + { + var schedule = DaprJobSchedule.Weekly; + Assert.True(schedule.IsPrefixedPeriodExpression); + Assert.False(schedule.IsCronExpression); + Assert.False(schedule.IsPointInTimeExpression); + Assert.False(schedule.IsDurationExpression); + } + + [Theory] + [InlineData("5h")] + [InlineData("5h5m")] + [InlineData("5h2m12s")] + [InlineData("5h9m22s27ms")] + [InlineData("42m12s28ms")] + [InlineData("19s2ms")] + [InlineData("292ms")] + [InlineData("5h23s")] + [InlineData("25m192ms")] + public void ValidateEveryExpression(string testValue) + { + var schedule = DaprJobSchedule.FromExpression($"@every {testValue}"); + Assert.True(schedule.IsPrefixedPeriodExpression); + } + + [Theory] + [InlineData("* * * * * *")] + [InlineData("5 12 16 7 * *")] + [InlineData("5 12 16 7 FEB *")] + [InlineData("5 12 16 7 * MON")] + [InlineData("5 12 16 7 JAN SAT")] + [InlineData("5 * * * FEB SUN")] + [InlineData("30 * * * * *")] + [InlineData("*/5 * * * * *")] + [InlineData("* */12 * * * *")] + [InlineData("* * */2 * * *")] + [InlineData("* * * */5 * *")] + [InlineData("30 15 6 */4 JAN,APR,AUG WED-FRI")] + [InlineData("*/10 */8 */2 */5 */3 */2")] + [InlineData("0 0 0 * * TUE,FRI")] + [InlineData("0 0 0 * * TUE-FRI")] + [InlineData("0 0 0 * OCT SAT")] + [InlineData("0 0 0 * OCT,DEC WED,SAT")] + [InlineData("0 0 0 * OCT-DEC WED-SAT")] + [InlineData("0-15 * * * * *")] + [InlineData("0-15 02-59 * * * *")] + [InlineData("1-14 2-59 20-23 * * *")] + [InlineData("0-59 0-59 0-23 1-31 1-12 0-6")] + [InlineData("*/1 2,4,5 * 2-9 JAN,FEB,DEC MON-WED")] + public void IsCronExpression(string testValue) + { + var schedule = DaprJobSchedule.FromExpression(testValue); + Assert.True(schedule.IsCronExpression); + Assert.False(schedule.IsPrefixedPeriodExpression); + Assert.False(schedule.IsPointInTimeExpression); + Assert.False(schedule.IsDurationExpression); + } +} diff --git a/test/Dapr.Jobs.Test/Models/Responses/DaprJobDetailsTests.cs b/test/Dapr.Jobs.Test/Models/Responses/DaprJobDetailsTests.cs new file mode 100644 index 000000000..cf369c404 --- /dev/null +++ b/test/Dapr.Jobs.Test/Models/Responses/DaprJobDetailsTests.cs @@ -0,0 +1,49 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +using System; +using System.Text.Json; +using Dapr.Jobs.Models; +using Dapr.Jobs.Models.Responses; +using Xunit; + +namespace Dapr.Jobs.Test.Models.Responses; + +public sealed class DaprJobDetailsTests +{ + [Fact] + public void ValidatePropertiesAreAsSet() + { + var payload = new TestPayload("Dapr", "Red"); + var payloadBytes = JsonSerializer.SerializeToUtf8Bytes(payload); + + var dueTime = DateTimeOffset.UtcNow.AddDays(2); + var ttl = DateTimeOffset.UtcNow.AddMonths(3); + const int repeatCount = 15; + + var details = new DaprJobDetails(DaprJobSchedule.Midnight) + { + RepeatCount = repeatCount, + DueTime = dueTime, + Payload = payloadBytes, + Ttl = ttl + }; + + Assert.Equal(repeatCount, details.RepeatCount); + Assert.Equal(dueTime, details.DueTime); + Assert.Equal(ttl, details.Ttl); + Assert.Equal(payloadBytes, details.Payload); + } + + private sealed record TestPayload(string Name, string Color); +}