dotnet-sdk/examples/Workflow/WorkflowConsoleApp/Program.cs

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 ":(";
}
}