mirror of https://github.com/dapr/dotnet-sdk.git
199 lines
7.6 KiB
C#
199 lines
7.6 KiB
C#
using Dapr.Client;
|
|
using Dapr.Workflow;
|
|
using WorkflowConsoleApp.Activities;
|
|
using WorkflowConsoleApp.Models;
|
|
using WorkflowConsoleApp.Workflows;
|
|
using Microsoft.Extensions.Hosting;
|
|
using Microsoft.Extensions.DependencyInjection;
|
|
|
|
const string storeName = "statestore";
|
|
|
|
// The workflow host is a background service that connects to the sidecar over gRPC
|
|
var builder = Host.CreateDefaultBuilder(args).ConfigureServices(services =>
|
|
{
|
|
services.AddDaprWorkflow(options =>
|
|
{
|
|
// Note that it's also possible to register a lambda function as the workflow
|
|
// or activity implementation instead of a class.
|
|
options.RegisterWorkflow<OrderProcessingWorkflow>();
|
|
|
|
// These are the activities that get invoked by the workflow(s).
|
|
options.RegisterActivity<NotifyActivity>();
|
|
options.RegisterActivity<ReserveInventoryActivity>();
|
|
options.RegisterActivity<ProcessPaymentActivity>();
|
|
options.RegisterActivity<UpdateInventoryActivity>();
|
|
});
|
|
});
|
|
|
|
// Dapr uses a random port for gRPC by default. If we don't know what that port
|
|
// is (because this app was started separate from dapr), then assume 4001.
|
|
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable("DAPR_GRPC_PORT")))
|
|
{
|
|
Environment.SetEnvironmentVariable("DAPR_GRPC_PORT", "4001");
|
|
}
|
|
|
|
Console.ForegroundColor = ConsoleColor.White;
|
|
Console.WriteLine("*** Welcome to the Dapr Workflow console app sample!");
|
|
Console.WriteLine("*** Using this app, you can place orders that start workflows.");
|
|
Console.WriteLine("*** Ensure that Dapr is running in a separate terminal window using the following command:");
|
|
Console.ForegroundColor = ConsoleColor.Green;
|
|
Console.WriteLine(" dapr run --dapr-grpc-port 4001 --app-id wfapp");
|
|
Console.WriteLine();
|
|
Console.ResetColor();
|
|
|
|
// Start the app - this is the point where we connect to the Dapr sidecar to
|
|
// listen for workflow work-items to execute.
|
|
using var host = builder.Build();
|
|
host.Start();
|
|
|
|
using var daprClient = new DaprClientBuilder().Build();
|
|
|
|
// Wait for the sidecar to become available
|
|
while (!await daprClient.CheckHealthAsync())
|
|
{
|
|
Thread.Sleep(TimeSpan.FromSeconds(5));
|
|
}
|
|
|
|
// Wait one more second for the workflow engine to finish initializing.
|
|
// This is just to make the log output look a little nicer.
|
|
Thread.Sleep(TimeSpan.FromSeconds(1));
|
|
|
|
// NOTE: WorkflowEngineClient will be replaced with a richer version of DaprClient
|
|
// in a subsequent SDK release. This is a temporary workaround.
|
|
WorkflowEngineClient workflowClient = host.Services.GetRequiredService<WorkflowEngineClient>();
|
|
|
|
var baseInventory = new List<InventoryItem>
|
|
{
|
|
new InventoryItem(Name: "Paperclips", PerItemCost: 5, Quantity: 100),
|
|
new InventoryItem(Name: "Cars", PerItemCost: 15000, Quantity: 100),
|
|
new InventoryItem(Name: "Computers", PerItemCost: 500, Quantity: 100),
|
|
};
|
|
|
|
// Populate the store with items
|
|
await RestockInventory(daprClient, baseInventory);
|
|
|
|
// Start the input loop
|
|
while (true)
|
|
{
|
|
// Get the name of the item to order and make sure we have inventory
|
|
string items = string.Join(", ", baseInventory.Select(i => i.Name));
|
|
Console.WriteLine($"Enter the name of one of the following items to order [{items}].");
|
|
Console.WriteLine("To restock items, type 'restock'.");
|
|
string itemName = Console.ReadLine()?.Trim();
|
|
if (string.IsNullOrEmpty(itemName))
|
|
{
|
|
continue;
|
|
}
|
|
else if (string.Equals("restock", itemName, StringComparison.OrdinalIgnoreCase))
|
|
{
|
|
await RestockInventory(daprClient, baseInventory);
|
|
continue;
|
|
}
|
|
|
|
InventoryItem item = baseInventory.FirstOrDefault(item => string.Equals(item.Name, itemName, StringComparison.OrdinalIgnoreCase));
|
|
if (item == null)
|
|
{
|
|
Console.ForegroundColor = ConsoleColor.Yellow;
|
|
Console.WriteLine($"We don't have {itemName}!");
|
|
Console.ResetColor();
|
|
continue;
|
|
}
|
|
|
|
Console.WriteLine($"How many {itemName} would you like to purchase?");
|
|
string amountStr = Console.ReadLine().Trim();
|
|
if (!int.TryParse(amountStr, out int amount) || amount <= 0)
|
|
{
|
|
Console.ForegroundColor = ConsoleColor.Yellow;
|
|
Console.WriteLine($"Invalid input. Assuming you meant to type '1'.");
|
|
Console.ResetColor();
|
|
amount = 1;
|
|
}
|
|
|
|
// Construct the order with a unique order ID
|
|
string orderId = $"{itemName.ToLowerInvariant()}-{Guid.NewGuid().ToString()[..8]}";
|
|
double totalCost = amount * item.PerItemCost;
|
|
var orderInfo = new OrderPayload(itemName.ToLowerInvariant(), totalCost, amount);
|
|
|
|
// Start the workflow using the order ID as the workflow ID
|
|
Console.WriteLine($"Starting order workflow '{orderId}' purchasing {amount} {itemName}");
|
|
await workflowClient.ScheduleNewWorkflowAsync(
|
|
name: nameof(OrderProcessingWorkflow),
|
|
instanceId: orderId,
|
|
input: orderInfo);
|
|
|
|
// Wait for the workflow to complete
|
|
WorkflowState state = await workflowClient.GetWorkflowStateAsync(
|
|
instanceId: orderId,
|
|
getInputsAndOutputs: true);
|
|
while (!state.IsWorkflowCompleted)
|
|
{
|
|
Thread.Sleep(TimeSpan.FromSeconds(1));
|
|
state = await workflowClient.GetWorkflowStateAsync(
|
|
instanceId: orderId,
|
|
getInputsAndOutputs: true);
|
|
}
|
|
|
|
if (state.RuntimeStatus == WorkflowRuntimeStatus.Completed)
|
|
{
|
|
OrderResult result = state.ReadOutputAs<OrderResult>();
|
|
if (result.Processed)
|
|
{
|
|
Console.ForegroundColor = ConsoleColor.Green;
|
|
Console.WriteLine($"Order workflow is {state.RuntimeStatus} and the order was processed successfully.");
|
|
Console.ResetColor();
|
|
}
|
|
else
|
|
{
|
|
Console.WriteLine($"Order workflow is {state.RuntimeStatus} but the order was not processed.");
|
|
}
|
|
}
|
|
else if (state.RuntimeStatus == WorkflowRuntimeStatus.Failed)
|
|
{
|
|
// WorkflowEngineClient doesn't expose a way to get error information.
|
|
// For that, we resort to DaprClient. The experience will be improved in the next release.
|
|
GetWorkflowResponse response = await daprClient.GetWorkflowAsync(
|
|
instanceId: orderId,
|
|
workflowComponent: "dapr",
|
|
workflowName: nameof(OrderProcessingWorkflow),
|
|
CancellationToken.None);
|
|
|
|
string failureDetails = await GetWorkflowFailureDetails(daprClient, orderId);
|
|
Console.ForegroundColor = ConsoleColor.Red;
|
|
Console.WriteLine($"The workflow failed - {failureDetails}");
|
|
Console.ResetColor();
|
|
}
|
|
|
|
Console.WriteLine();
|
|
}
|
|
|
|
static async Task RestockInventory(DaprClient daprClient, List<InventoryItem> inventory)
|
|
{
|
|
Console.WriteLine("*** Restocking inventory...");
|
|
foreach (var item in inventory)
|
|
{
|
|
Console.WriteLine($"*** \t{item.Name}: {item.Quantity}");
|
|
await daprClient.SaveStateAsync(storeName, item.Name.ToLowerInvariant(), item);
|
|
}
|
|
}
|
|
|
|
static async Task<string> GetWorkflowFailureDetails(DaprClient daprClient, string orderId)
|
|
{
|
|
// Use DaprClient to get the error details
|
|
GetWorkflowResponse response = await daprClient.GetWorkflowAsync(
|
|
instanceId: orderId,
|
|
workflowComponent: "dapr",
|
|
workflowName: nameof(OrderProcessingWorkflow),
|
|
CancellationToken.None);
|
|
|
|
// Available metadata fields: https://github.com/dapr/dapr/blob/ad4c38756fa08dda5def8f0a7a6d672feb37be91/pkg/runtime/wfengine/component.go#L146-L161
|
|
if (response.metadata.TryGetValue("dapr.workflow.failure.error_type", out string errorType) &&
|
|
response.metadata.TryGetValue("dapr.workflow.failure.error_message", out string errorMessage))
|
|
{
|
|
return $"{errorType}: {errorMessage}";
|
|
}
|
|
else
|
|
{
|
|
return ":(";
|
|
}
|
|
}
|