HTTP csharp jobs quickstart

Signed-off-by: Alice Gibbons <alice@diagrid.io>
This commit is contained in:
Alice Gibbons 2025-01-20 22:05:15 +00:00
parent b8ef1c01cf
commit dcf1f9fb80
10 changed files with 425 additions and 0 deletions

181
jobs/csharp/http/README.md Normal file
View File

@ -0,0 +1,181 @@
# Dapr Jobs API (HTTP Client)
In this quickstart, you'll schedule, get, and delete a job using Dapr's Job API. This API is responsible for scheduling and running jobs at a specific time or interval.
Visit [this](https://docs.dapr.io/developing-applications/building-blocks/jobs/) link for more information about Dapr and the Jobs API.
> **Note:** This example leverages HTTP requests only. If you are looking for the example using the Dapr Client SDK (recommended) [click here](../sdk/).
This quickstart includes two apps:
- Jobs Scheduler, responsible for scheduling, retrieving and deleting jobs.
- Jobs Service, responsible for handling the triggered jobs.
## Run all apps with multi-app run template file
This section shows how to run both applications at once using [multi-app run template files](https://docs.dapr.io/developing-applications/local-development/multi-app-dapr-run/multi-app-overview/) with `dapr run -f .`. This enables to you test the interactions between multiple applications and will `schedule`, `run`, `get`, and `delete` jobs within a single process.
1. Build the apps:
<!-- STEP
name: Build dependencies for job-service
sleep: 1
-->
```bash
cd ./job-service
dotnet build
```
<!-- END_STEP -->
<!-- STEP
name: Build dependencies for job-scheduler
sleep: 1
-->
```bash
cd ./job-scheduler
dotnet build
```
<!-- END_STEP -->
2. Run the multi app run template:
<!-- STEP
name: Run multi app run template
expected_stdout_lines:
- '== APP - job-scheduler == Job Scheduled: R2-D2'
- '== APP - job-scheduler == Job Scheduled: C-3PO'
- '== APP - job-service == Received job request...'
- '== APP - job-service == Starting droid: R2-D2'
- '== APP - job-service == Executing maintenance job: Oil Change'
- '== APP - job-service == Received job request...'
- '== APP - job-service == Starting droid: C-3PO'
- '== APP - job-service == Executing maintenance job: Limb Calibration'
expected_stderr_lines:
output_match_mode: substring
match_order: none
background: false
sleep: 60
timeout_seconds: 120
-->
```bash
dapr run -f .
```
The terminal console output should look similar to this, where:
- The `R2-D2` job is being scheduled.
- The `R2-D2` job is being retrieved.
- The `C-3PO` job is being scheduled.
- The `C-3PO` job is being retrieved.
- The `R2-D2` job is being executed after 15 seconds.
- The `C-3PO` job is being executed after 20 seconds.
```text
== APP - job-scheduler == Job Scheduled: R2-D2
== APP - job-scheduler == Job details: {"name":"R2-D2", "dueTime":"15s", "data":{"@type":"type.googleapis.com/google.protobuf.Value", "value":{"Value":"R2-D2:Oil Change"}}}
== APP - job-scheduler == Job Scheduled: C-3PO
== APP - job-scheduler == Job details: {"name":"C-3PO", "dueTime":"20s", "data":{"@type":"type.googleapis.com/google.protobuf.Value", "value":{"Value":"C-3PO:Limb Calibration"}}}
== APP - job-service == Received job request...
== APP - job-service == Starting droid: R2-D2
== APP - job-service == Executing maintenance job: Oil Change
```
After 20 seconds, the terminal output should present the `C-3PO` job being processed:
```text
== APP - job-service == Received job request...
== APP - job-service == Starting droid: C-3PO
== APP - job-service == Executing maintenance job: Limb Calibration
```
<!-- END_STEP -->
## Run apps individually
### Schedule Jobs
1. Open a terminal and run the `job-service` app. Build the dependencies if you haven't already.
```bash
cd ./job-service
dotnet build
```
```bash
dapr run --app-id job-service --app-port 6200 --dapr-http-port 6280 -- dotnet run
```
2. In a new terminal window, schedule the `R2-D2` Job using the Jobs API.
```bash
curl -X POST \
http://localhost:6280/v1.0-alpha1/jobs/r2-d2 \
-H "Content-Type: application/json" \
-d '{
"data": {
"Value": "R2-D2:Oil Change"
},
"dueTime": "2s"
}'
```
In the `job-service` terminal window, the output should be:
```text
== APP - job-app == Received job request...
== APP - job-app == Starting droid: R2-D2
== APP - job-app == Executing maintenance job: Oil Change
```
3. On the same terminal window, schedule the `C-3PO` Job using the Jobs API.
```bash
curl -X POST \
http://localhost:6280/v1.0-alpha1/jobs/c-3po \
-H "Content-Type: application/json" \
-d '{
"data": {
"Value": "C-3PO:Limb Calibration"
},
"dueTime": "30s"
}'
```
### Get a scheduled job
1. On the same terminal window, run the command below to get the recently scheduled `C-3PO` job.
```bash
curl -X GET http://localhost:6280/v1.0-alpha1/jobs/c-3po -H "Content-Type: application/json"
```
You should see the following:
```text
{"name":"c-3po", "dueTime":"30s", "data":{"@type":"type.googleapis.com/google.protobuf.Value", "value":{"Value":"C-3PO:Limb Calibration"}}
```
### Delete a scheduled job
1. On the same terminal window, run the command below to deleted the recently scheduled `C-3PO` job.
```bash
curl -X DELETE http://localhost:6280/v1.0-alpha1/jobs/c-3po -H "Content-Type: application/json"
```
2. Run the command below to attempt to retrieve the deleted job:
```bash
curl -X GET http://localhost:6280/v1.0-alpha1/jobs/c-3po -H "Content-Type: application/json"
```
In the `job-service` terminal window, the output should be similar to the following:
```text
ERRO[0568] Error getting job c-3po due to: rpc error: code = Unknown desc = job not found: c-3po instance=local scope=dapr.api type=log ver=1.15.0
```

View File

@ -0,0 +1,13 @@
version: 1
apps:
- appDirPath: ./job-service/
appID: job-service
appPort: 6200
daprHTTPPort: 6280
schedulerHostAddress: localhost
command: ["dotnet", "run"]
- appDirPath: ./job-scheduler/
appID: job-scheduler
appPort: 6300
daprHTTPPort: 6380
command: ["dotnet", "run"]

View File

@ -0,0 +1,78 @@
using System;
using System.Net.Http;
using System.Text;
using System.Text.Json;
using System.Threading.Tasks;
// Job request bodies
var c3poJobBody = new
{
data = new { Value = "C-3PO:Limb Calibration" },
dueTime = "20s"
};
var r2d2JobBody = new
{
data = new { Value = "R2-D2:Oil Change" },
dueTime = "15s"
};
var daprHost = Environment.GetEnvironmentVariable("DAPR_HOST") ?? "http://localhost";
var schedulerDaprHttpPort = "6280";
var httpClient = new HttpClient();
await Task.Delay(5000); // Wait for job-service to start
try
{
// Schedule R2-D2 job
await ScheduleJob("R2-D2", r2d2JobBody);
await Task.Delay(5000);
// Get R2-D2 job details
await GetJobDetails("R2-D2");
// Schedule C-3PO job
await ScheduleJob("C-3PO", c3poJobBody);
await Task.Delay(5000);
// Get C-3PO job details
await GetJobDetails("C-3PO");
await Task.Delay(30000); // Allow time for jobs to complete
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error: {ex.Message}");
Environment.Exit(1);
}
async Task ScheduleJob(string jobName, object jobBody)
{
var reqURL = $"{daprHost}:{schedulerDaprHttpPort}/v1.0-alpha1/jobs/{jobName}";
var jsonBody = JsonSerializer.Serialize(jobBody);
var content = new StringContent(jsonBody, Encoding.UTF8, "application/json");
var response = await httpClient.PostAsync(reqURL, content);
if (response.StatusCode != System.Net.HttpStatusCode.NoContent)
{
throw new Exception($"Failed to register job event handler. Status code: {response.StatusCode}");
}
Console.WriteLine($"Job Scheduled: {jobName}");
}
async Task GetJobDetails(string jobName)
{
var reqURL = $"{daprHost}:{schedulerDaprHttpPort}/v1.0-alpha1/jobs/{jobName}";
var response = await httpClient.GetAsync(reqURL);
if (!response.IsSuccessStatusCode)
{
throw new Exception($"HTTP error! Status: {response.StatusCode}");
}
var jobDetails = await response.Content.ReadAsStringAsync();
Console.WriteLine($"Job details: {jobDetails}");
}

View File

@ -0,0 +1,11 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net8.0</TargetFramework>
<RootNamespace>jobs_scheduler</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,75 @@
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Hosting;
using Microsoft.AspNetCore.Http.Json;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using System.Text.Json;
var builder = WebApplication.CreateBuilder(args);
builder.Services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.PropertyNamingPolicy = JsonNamingPolicy.CamelCase;
});
var app = builder.Build();
var appPort = Environment.GetEnvironmentVariable("APP_PORT") ?? "6200";
//Job handler route
app.MapPost("/job/{*path}", async (HttpRequest request, HttpResponse response) =>
{
Console.WriteLine("Received job request...");
try
{
// Parse the incoming JSON body
var jobData = await JsonSerializer.DeserializeAsync<JobData>(request.Body);
if (jobData == null || string.IsNullOrEmpty(jobData.Value))
{
throw new Exception("Invalid job data. 'value' field is required.");
}
// Creating Droid Job from decoded value
var droidJob = SetDroidJob(jobData.Value);
Console.WriteLine($"Starting droid: {droidJob.Droid}");
Console.WriteLine($"Executing maintenance job: {droidJob.Task}");
response.StatusCode = 200;
}
catch (Exception ex)
{
Console.Error.WriteLine($"Error processing job: {ex.Message}");
response.StatusCode = 400; // Bad Request
var errorResponse = new { error = $"Error processing request: {ex.Message}" };
await response.WriteAsJsonAsync(errorResponse);
}
});
// Start the server
app.Run($"http://localhost:{appPort}");
static DroidJob SetDroidJob(string droidStr)
{
var parts = droidStr.Split(":");
if (parts.Length != 2)
{
throw new Exception("Invalid droid job format. Expected format: 'Droid:Task'");
}
return new DroidJob
{
Droid = parts[0],
Task = parts[1]
};
}
// Classes for request and response models
public class JobData
{
public string? Value { get; set; }
}
public class DroidJob
{
public string? Droid { get; set; }
public string? Task { get; set; }
}

View File

@ -0,0 +1,38 @@
{
"$schema": "http://json.schemastore.org/launchsettings.json",
"iisSettings": {
"windowsAuthentication": false,
"anonymousAuthentication": true,
"iisExpress": {
"applicationUrl": "http://localhost:5305",
"sslPort": 44346
}
},
"profiles": {
"http": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "http://localhost:5023",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"https": {
"commandName": "Project",
"dotnetRunMessages": true,
"launchBrowser": true,
"applicationUrl": "https://localhost:7073;http://localhost:5023",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
},
"IIS Express": {
"commandName": "IISExpress",
"launchBrowser": true,
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}

View File

@ -0,0 +1,8 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
}
}

View File

@ -0,0 +1,9 @@
{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft.AspNetCore": "Warning"
}
},
"AllowedHosts": "*"
}

View File

@ -0,0 +1,10 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<Nullable>enable</Nullable>
<ImplicitUsings>enable</ImplicitUsings>
<RootNamespace>job_service</RootNamespace>
</PropertyGroup>
</Project>

View File

@ -0,0 +1,2 @@
include ../../../docker.mk
include ../../../validate.mk