-
Notifications
You must be signed in to change notification settings - Fork 371
Additional API surface area for Dapr Workflow authoring SDK #1012
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
6 commits
Select commit
Hold shift + click to select a range
c173ecc
Additional API surface area
cgillum 5df1c07
PR feedback
cgillum d0f62a9
Environment variable configuration
cgillum 448dd2e
Merge branch 'master' into alpha-updates
halspang 048522d
Fix .NET TFMs and complete the README.md contents
cgillum 0d2324c
Merge branch 'alpha-updates' of https://github.com/cgillum/dapr-dotne…
cgillum File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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
24
examples/Workflow/WorkflowWebApp/Activities/NotifyActivity.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } | ||
| } |
35 changes: 35 additions & 0 deletions
35
examples/Workflow/WorkflowWebApp/Activities/ProcessPaymentActivity.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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; | ||
| } | ||
| } | ||
| } |
32 changes: 32 additions & 0 deletions
32
examples/Workflow/WorkflowWebApp/Activities/ReserveInventoryActivity.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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); | ||
| } | ||
| } | ||
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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>(); | ||
|
|
||
| // 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"); | ||
|
cgillum marked this conversation as resolved.
|
||
|
|
||
| app.Run(); | ||
|
|
||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
44 changes: 44 additions & 0 deletions
44
examples/Workflow/WorkflowWebApp/Workflows/OrderProcessingWorkflow.cs
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| 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}")); | ||
|
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); | ||
| } | ||
| } | ||
| } | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.