Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,5 @@ This repository contains a samples that highlight the Dapr .NET SDK capabilities
|----------------------------------|------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
| [1. Client](./Client) | The client example shows how to make Dapr calls to publish events, save state, get state and delete state using a Dapr client.
| [2. Actor](./Actor) | Demonstrates creating virtual actors that encapsulate code and state. |
| [3. ASP.NET Core](./AspNetCore) | Demonstrates ASP.NET Core integration with Dapr by creating Controllers and Routes.
| [3. ASP.NET Core](./AspNetCore) | Demonstrates ASP.NET Core integration with Dapr by creating Controllers and Routes. |
| [4. Workflow](./Workflow) | Demonstrates creating durable, long-running Dapr workflows using code. |
115 changes: 115 additions & 0 deletions examples/Workflow/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# Dapr Workflow with ASP.NET Core sample

This Dapr workflow example shows how to create a Dapr workflow (`Workflow`) and invoke it using ASP.NET Core web APIs.

## 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 .NET SDK](https://github.com/dapr/dotnet-sdk/)

## Projects in sample

This sample contains a single [WorkflowWebApp](./WorkflowWebApp) ASP.NET Core project. It combines both the workflow implementations and the web APIs for starting and querying workflows instances.

The main `Program.cs` file contains the main setup of the app, including the registration of the web APIs and the registration of the workflow and workflow activities. The workflow definition is found in `Workflows` directory and the workflow activity definitions are found in the `Activities` directory.

## Running the example

To run the workflow web app locally, run this command in the `WorkflowWebApp` directory:

```sh
dapr run --app-id wfwebapp dotnet run
```

The application will listen for HTTP requests at `http://localhost:10080`.

To start a workflow, use the following command to send an HTTP POST request, which triggers an HTTP API that starts the workflow using the Dapr Workflow client. Two identical `curl` commands are shown, one for Linux/macOS (bash) and the other for Windows (PowerShell). The body of the request is used as the input of the workflow.

On Linux/macOS (bash):

```bash
curl -i -X POST http://localhost:10080/orders \
-H "Content-Type: application/json" \
-d '{"name": "Paperclips", "totalCost": 99.95, "quantity": 1}'
```

On Windows (PowerShell):

```powershell
curl -i -X POST http://localhost:10080/orders `
-H "Content-Type: application/json" `
-d '{"name": "Paperclips", "totalCost": 99.95, "quantity": 1}'
```

If successful, you should see a response like the following, which contains a `Location` header pointing to a status endpoint for the workflow that was created with a randomly generated 8-digit ID:

```http
HTTP/1.1 202 Accepted
Content-Length: 0
Date: Tue, 24 Jan 2023 00:02:02 GMT
Server: Kestrel
Location: http://localhost:10080/orders/cdcce425
```

Next, send an HTTP request to the URL in the `Location` header in the previous HTTP response, like in the following example:

```bash
curl -i http://localhost:10080/orders/cdcce425
```

If the workflow has completed running, you should see the following output (formatted for readability):

```http
HTTP/1.1 200 OK
Content-Type: application/json; charset=utf-8
Date: Tue, 24 Jan 2023 00:10:53 GMT
Server: Kestrel
Transfer-Encoding: chunked

{
"details": {
"name": "Paperclips",
"quantity": 1,
"totalCost": 99.95
},
"result": {
"processed": true
},
"status": "Completed"
}
```

If the workflow hasn't completed yet, you might instead see the following:

```http
HTTP/1.1 202 Accepted
Content-Type: application/json; charset=utf-8
Date: Tue, 24 Jan 2023 00:17:49 GMT
Location: http://localhost:10080/orders/cdcce425
Server: Kestrel
Transfer-Encoding: chunked

{
"details": {
"name": "Paperclips",
"quantity": 1,
"totalCost": 99.95
},
"status": "Running"
}
```

When the workflow has completed, the stdout of the web app should look like the following:

```log
info: WorkflowWebApp.Activities.NotifyActivity[0]
Received order cdcce425 for Paperclips at $99.95
info: WorkflowWebApp.Activities.ReserveInventoryActivity[0]
Reserving inventory: cdcce425, Paperclips, 1
info: WorkflowWebApp.Activities.ProcessPaymentActivity[0]
Processing payment: cdcce425, 99.95, USD
info: WorkflowWebApp.Activities.NotifyActivity[0]
Order cdcce425 processed successfully!
```
24 changes: 24 additions & 0 deletions examples/Workflow/WorkflowWebApp/Activities/NotifyActivity.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
namespace WorkflowWebApp.Activities
{
using System.Threading.Tasks;
using Dapr.Workflow;

record Notification(string Message);

class NotifyActivity : WorkflowActivity<Notification, object>
{
readonly ILogger logger;

public NotifyActivity(ILoggerFactory loggerFactory)
{
this.logger = loggerFactory.CreateLogger<NotifyActivity>();
}

public override Task<object> RunAsync(WorkflowActivityContext context, Notification notification)
{
this.logger.LogInformation(notification.Message);

return Task.FromResult<object>(null);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
namespace WorkflowWebApp.Activities
{
using System.Threading.Tasks;
using Dapr.Workflow;

record PaymentRequest(string RequestId, double Amount, string Currency);

class ProcessPaymentActivity : WorkflowActivity<PaymentRequest, object>
{
readonly ILogger logger;

public ProcessPaymentActivity(ILoggerFactory loggerFactory)
{
this.logger = loggerFactory.CreateLogger<ProcessPaymentActivity>();
}

public override async Task<object> RunAsync(WorkflowActivityContext context, PaymentRequest req)
{
this.logger.LogInformation(
"Processing payment: {requestId}, {amount}, {currency}",
req.RequestId,
req.Amount,
req.Currency);

// Simulate slow processing
await Task.Delay(TimeSpan.FromSeconds(7));

this.logger.LogInformation(
"Payment for request ID '{requestId}' processed successfully",
req.RequestId);

return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
namespace WorkflowWebApp.Activities
{
using System.Threading.Tasks;
using Dapr.Workflow;

record InventoryRequest(string RequestId, string Name, int Quantity);
record InventoryResult(bool Success);

class ReserveInventoryActivity : WorkflowActivity<InventoryRequest, InventoryResult>
{
readonly ILogger logger;

public ReserveInventoryActivity(ILoggerFactory loggerFactory)
{
this.logger = loggerFactory.CreateLogger<ReserveInventoryActivity>();
}

public override async Task<InventoryResult> RunAsync(WorkflowActivityContext context, InventoryRequest req)
{
this.logger.LogInformation(
"Reserving inventory: {requestId}, {name}, {quantity}",
req.RequestId,
req.Name,
req.Quantity);

// Simulate slow processing
await Task.Delay(TimeSpan.FromSeconds(2));

return new InventoryResult(true);
}
}
}
81 changes: 56 additions & 25 deletions examples/Workflow/WorkflowWebApp/Program.cs
Original file line number Diff line number Diff line change
@@ -1,51 +1,82 @@
using Dapr.Workflow;
using System.Text.Json.Serialization;
using Dapr.Workflow;
using Microsoft.AspNetCore.Mvc;
using WorkflowWebApp.Activities;
using WorkflowWebApp.Workflows;
using JsonOptions = Microsoft.AspNetCore.Http.Json.JsonOptions;

// The workflow host is a background service that connects to the sidecar over gRPC
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);

// Configure HTTP JSON options.
builder.Services.Configure<JsonOptions>(options =>
{
options.SerializerOptions.Converters.Add(new JsonStringEnumConverter());
options.SerializerOptions.DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull;
});

// Dapr workflows are registered as part of the service configuration
builder.Services.AddDaprWorkflow(options =>
{
// Example of registering a "PlaceOrder" workflow function
options.RegisterWorkflow<string, string>("PlaceOrder", implementation: async (context, input) =>
{
// In real life there are other steps related to placing an order, like reserving
// inventory and charging the customer credit card etc. But let's keep it simple ;)
return await context.CallActivityAsync<string>("ShipProduct", "Coffee Beans");
});
// Note that it's also possible to register a lambda function as the workflow
// or activity implementation instead of a class.
options.RegisterWorkflow<OrderProcessingWorkflow>();
Comment thread
cgillum marked this conversation as resolved.

// Example of registering a "ShipProduct" workflow activity function
options.RegisterActivity<string, string>("ShipProduct", implementation: (context, input) =>
{
return Task.FromResult($"We are shipping {input} to the customer using our hoard of drones!");
});
// These are the activities that get invoked by the workflow(s).
options.RegisterActivity<NotifyActivity>();
options.RegisterActivity<ReserveInventoryActivity>();
options.RegisterActivity<ProcessPaymentActivity>();
});

WebApplication app = builder.Build();

// POST starts new workflow instances
app.MapPost("/order", async (HttpContext context, WorkflowClient client) =>
// POST starts new order workflow instance
app.MapPost("/orders", async (WorkflowEngineClient client, [FromBody] OrderPayload orderInfo) =>
{
string id = Guid.NewGuid().ToString()[..8];
await client.ScheduleNewWorkflowAsync("PlaceOrder", id);
if (orderInfo?.Name == null)
{
return Results.BadRequest(new
{
message = "Order data was missing from the request",
example = new OrderPayload("Paperclips", 99.95),
});
}

// Randomly generated order ID that is 8 characters long.
string orderId = Guid.NewGuid().ToString()[..8];
await client.ScheduleNewWorkflowAsync(nameof(OrderProcessingWorkflow), orderId, orderInfo);

// return an HTTP 202 and a Location header to be used for status query
return Results.AcceptedAtRoute("GetOrderEndpoint", new { id });
return Results.AcceptedAtRoute("GetOrderInfoEndpoint", new { orderId });
});

// GET fetches metadata for specific order workflow instances
app.MapGet("/order/{id}", async (string id, WorkflowClient client) =>
// GET fetches state for order workflow to report status
app.MapGet("/orders/{orderId}", async (string orderId, WorkflowEngineClient client) =>
{
WorkflowMetadata metadata = await client.GetWorkflowMetadataAsync(id, getInputsAndOutputs: true);
if (metadata.Exists)
WorkflowState state = await client.GetWorkflowStateAsync(orderId, true);
if (!state.Exists)
{
return Results.NotFound($"No order with ID = '{orderId}' was found.");
}

var httpResponsePayload = new
{
details = state.ReadInputAs<OrderPayload>(),
status = state.RuntimeStatus.ToString(),
result = state.ReadOutputAs<OrderResult>(),
};

if (state.IsWorkflowRunning)
{
return Results.Ok(metadata);
// HTTP 202 Accepted - async polling clients should keep polling for status
return Results.AcceptedAtRoute("GetOrderInfoEndpoint", new { orderId }, httpResponsePayload);
}
else
{
return Results.NotFound($"No workflow created for order with ID = '{id}' was found.");
// HTTP 200 OK
return Results.Ok(httpResponsePayload);
}
}).WithName("GetOrderEndpoint");
}).WithName("GetOrderInfoEndpoint");
Comment thread
cgillum marked this conversation as resolved.

app.Run();

Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "https://localhost:10080"
"applicationUrl": "http://localhost:10080"
}
}
}
4 changes: 2 additions & 2 deletions examples/Workflow/WorkflowWebApp/WorkflowWebApp.csproj
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Project Sdk="Microsoft.NET.Sdk.Web">

<ItemGroup>
<ProjectReference Include="..\..\..\src\Dapr.Workflow\Dapr.Workflow.csproj" />
</ItemGroup>

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net6</TargetFrameworks>
<TargetFramework>net6</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<LangVersion>latest</LangVersion>
</PropertyGroup>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
namespace WorkflowWebApp.Workflows
{
using System.Threading.Tasks;
using Dapr.Workflow;
using WorkflowWebApp.Activities;

record OrderPayload(string Name, double TotalCost, int Quantity = 1);
record OrderResult(bool Processed);

class OrderProcessingWorkflow : Workflow<OrderPayload, OrderResult>
{
public override async Task<OrderResult> RunAsync(WorkflowContext context, OrderPayload order)
{
string orderId = context.InstanceId;

await context.CallActivityAsync(
nameof(NotifyActivity),
new Notification($"Received order {orderId} for {order.Name} at {order.TotalCost:c}"));
Comment thread
cgillum marked this conversation as resolved.

string requestId = context.InstanceId;

InventoryResult result = await context.CallActivityAsync<InventoryResult>(
nameof(ReserveInventoryActivity),
new InventoryRequest(RequestId: orderId, order.Name, order.Quantity));
if (!result.Success)
{
// End the workflow here since we don't have sufficient inventory
context.SetCustomStatus($"Insufficient inventory for {order.Name}");
return new OrderResult(Processed: false);
}

await context.CallActivityAsync(
nameof(ProcessPaymentActivity),
new PaymentRequest(RequestId: orderId, order.TotalCost, "USD"));

await context.CallActivityAsync(
nameof(NotifyActivity),
new Notification($"Order {orderId} processed successfully!"));

// End the workflow with a success result
return new OrderResult(Processed: true);
}
}
}
Loading