diff --git a/examples/Workflow/README.md b/examples/Workflow/README.md index b93af1bb5..e119465c3 100644 --- a/examples/Workflow/README.md +++ b/examples/Workflow/README.md @@ -9,6 +9,10 @@ This Dapr workflow example shows how to create a Dapr workflow (`Workflow`) and - [Initialized Dapr environment](https://docs.dapr.io/getting-started/install-dapr-selfhost/) - [Dapr .NET SDK](https://github.com/dapr/dotnet-sdk/) + +## Optional Setup +Dapr workflow, as well as this example program, now support authentication through the use of API tokens. For more information on this, view the following document: [API Token](https://github.com/dapr/dotnet-sdk/docs/api-token.md) + ## Projects in sample This sample contains a single [WorkflowConsoleApp](./WorkflowConsoleApp) .NET project. diff --git a/examples/Workflow/WorkflowConsoleApp/Program.cs b/examples/Workflow/WorkflowConsoleApp/Program.cs index a1189e70b..9aae2427e 100644 --- a/examples/Workflow/WorkflowConsoleApp/Program.cs +++ b/examples/Workflow/WorkflowConsoleApp/Program.cs @@ -47,7 +47,16 @@ using var host = builder.Build(); host.Start(); -using var daprClient = new DaprClientBuilder().Build(); +DaprClient daprClient; +string apiToken = Environment.GetEnvironmentVariable("DAPR_API_TOKEN"); +if (!string.IsNullOrEmpty(apiToken)) +{ + daprClient = new DaprClientBuilder().UseDaprApiToken(apiToken).Build(); +} +else +{ + daprClient = new DaprClientBuilder().Build(); +} // Wait for the sidecar to become available while (!await daprClient.CheckHealthAsync()) @@ -70,136 +79,138 @@ await RestockInventory(daprClient, baseInventory); // Start the input loop -while (true) +using (daprClient) { - // 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)) + while (true) { - await RestockInventory(daprClient, baseInventory); - continue; - } + // 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; - } + 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; - } + 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); + // 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 daprClient.StartWorkflowAsync( - workflowComponent: DaprWorkflowComponent, - workflowName: nameof(OrderProcessingWorkflow), - input: orderInfo, - instanceId: orderId); + // Start the workflow using the order ID as the workflow ID + Console.WriteLine($"Starting order workflow '{orderId}' purchasing {amount} {itemName}"); + await daprClient.StartWorkflowAsync( + workflowComponent: DaprWorkflowComponent, + workflowName: nameof(OrderProcessingWorkflow), + input: orderInfo, + instanceId: orderId); - // Wait for the workflow to start and confirm the input - GetWorkflowResponse state = await daprClient.WaitForWorkflowStartAsync( - instanceId: orderId, - workflowComponent: DaprWorkflowComponent); + // Wait for the workflow to start and confirm the input + GetWorkflowResponse state = await daprClient.WaitForWorkflowStartAsync( + instanceId: orderId, + workflowComponent: DaprWorkflowComponent); - Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) started successfully with {state.ReadInputAs()}"); + Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) started successfully with {state.ReadInputAs()}"); - // Wait for the workflow to complete - while (true) - { - using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); - try + // Wait for the workflow to complete + while (true) { - state = await daprClient.WaitForWorkflowCompletionAsync( - instanceId: orderId, - workflowComponent: DaprWorkflowComponent, - cancellationToken: cts.Token); - break; - } - catch (OperationCanceledException) - { - // Check to see if the workflow is blocked waiting for an approval - state = await daprClient.GetWorkflowAsync( - instanceId: orderId, - workflowComponent: DaprWorkflowComponent); - if (state.Properties.TryGetValue("dapr.workflow.custom_status", out string customStatus) && - customStatus.Contains("Waiting for approval")) + using var cts = new CancellationTokenSource(TimeSpan.FromSeconds(5)); + try { - Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) requires approval. Approve? [Y/N]"); - string approval = Console.ReadLine(); - ApprovalResult approvalResult = ApprovalResult.Unspecified; - if (string.Equals(approval, "Y", StringComparison.OrdinalIgnoreCase)) - { - Console.WriteLine("Approving order..."); - approvalResult = ApprovalResult.Approved; - } - else if (string.Equals(approval, "N", StringComparison.OrdinalIgnoreCase)) - { - Console.WriteLine("Rejecting order..."); - approvalResult = ApprovalResult.Rejected; - } - - if (approvalResult != ApprovalResult.Unspecified) + state = await daprClient.WaitForWorkflowCompletionAsync( + instanceId: orderId, + workflowComponent: DaprWorkflowComponent, + cancellationToken: cts.Token); + break; + } + catch (OperationCanceledException) + { + // Check to see if the workflow is blocked waiting for an approval + state = await daprClient.GetWorkflowAsync( + instanceId: orderId, + workflowComponent: DaprWorkflowComponent); + if (state.Properties.TryGetValue("dapr.workflow.custom_status", out string customStatus) && + customStatus.Contains("Waiting for approval")) { - // Raise the workflow event to the workflow - await daprClient.RaiseWorkflowEventAsync( - instanceId: orderId, - workflowComponent: DaprWorkflowComponent, - eventName: "ManagerApproval", - eventData: approvalResult); + Console.WriteLine($"{state.WorkflowName} (ID = {orderId}) requires approval. Approve? [Y/N]"); + string approval = Console.ReadLine(); + ApprovalResult approvalResult = ApprovalResult.Unspecified; + if (string.Equals(approval, "Y", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Approving order..."); + approvalResult = ApprovalResult.Approved; + } + else if (string.Equals(approval, "N", StringComparison.OrdinalIgnoreCase)) + { + Console.WriteLine("Rejecting order..."); + approvalResult = ApprovalResult.Rejected; + } + + if (approvalResult != ApprovalResult.Unspecified) + { + // Raise the workflow event to the workflow + await daprClient.RaiseWorkflowEventAsync( + instanceId: orderId, + workflowComponent: DaprWorkflowComponent, + eventName: "ManagerApproval", + eventData: approvalResult); + } + + // otherwise, keep waiting } - - // otherwise, keep waiting } } - } - if (state.RuntimeStatus == WorkflowRuntimeStatus.Completed) - { - OrderResult result = state.ReadOutputAs(); - if (result.Processed) + if (state.RuntimeStatus == WorkflowRuntimeStatus.Completed) { - Console.ForegroundColor = ConsoleColor.Green; - Console.WriteLine($"Order workflow is {state.RuntimeStatus} and the order was processed successfully ({result})."); - Console.ResetColor(); + OrderResult result = state.ReadOutputAs(); + if (result.Processed) + { + Console.ForegroundColor = ConsoleColor.Green; + Console.WriteLine($"Order workflow is {state.RuntimeStatus} and the order was processed successfully ({result})."); + Console.ResetColor(); + } + else + { + Console.WriteLine($"Order workflow is {state.RuntimeStatus} but the order was not processed."); + } } - else + else if (state.RuntimeStatus == WorkflowRuntimeStatus.Failed) { - Console.WriteLine($"Order workflow is {state.RuntimeStatus} but the order was not processed."); + Console.ForegroundColor = ConsoleColor.Red; + Console.WriteLine($"The workflow failed - {state.FailureDetails}"); + Console.ResetColor(); } - } - else if (state.RuntimeStatus == WorkflowRuntimeStatus.Failed) - { - Console.ForegroundColor = ConsoleColor.Red; - Console.WriteLine($"The workflow failed - {state.FailureDetails}"); - Console.ResetColor(); - } - Console.WriteLine(); + Console.WriteLine(); + } } - static async Task RestockInventory(DaprClient daprClient, List inventory) { Console.WriteLine("*** Restocking inventory..."); diff --git a/src/Dapr.Workflow/Dapr.Workflow.csproj b/src/Dapr.Workflow/Dapr.Workflow.csproj index 781e891d0..9d8ba1a4e 100644 --- a/src/Dapr.Workflow/Dapr.Workflow.csproj +++ b/src/Dapr.Workflow/Dapr.Workflow.csproj @@ -17,6 +17,10 @@ + + + + diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index c24265475..50880ab24 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -14,16 +14,20 @@ namespace Dapr.Workflow { using System; + using Grpc.Net.Client; using Microsoft.DurableTask.Client; using Microsoft.DurableTask.Worker; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; + using System.Net.Http; + using Dapr; /// /// Contains extension methods for using Dapr Workflow with dependency injection. /// public static class WorkflowServiceCollectionExtensions { + /// /// Adds Dapr Workflow support to the service collection. /// @@ -57,7 +61,18 @@ public static IServiceCollection AddDaprWorkflow( if (TryGetGrpcAddress(out string address)) { - builder.UseGrpc(address); + var daprApiToken = DaprDefaults.GetDefaultDaprApiToken(); + if (!string.IsNullOrEmpty(daprApiToken)) + { + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("Dapr-Api-Token", daprApiToken); + builder.UseGrpc(CreateChannel(address, client)); + } + else + { + builder.UseGrpc(address); + } + } else { @@ -85,7 +100,18 @@ public static IServiceCollection AddDaprWorkflowClient(this IServiceCollection s { if (TryGetGrpcAddress(out string address)) { - builder.UseGrpc(address); + var daprApiToken = DaprDefaults.GetDefaultDaprApiToken(); + if (!string.IsNullOrEmpty(daprApiToken)) + { + var client = new HttpClient(); + client.DefaultRequestHeaders.Add("Dapr-Api-Token", daprApiToken); + builder.UseGrpc(CreateChannel(address, client)); + } + else + { + builder.UseGrpc(address); + } + } else { @@ -104,13 +130,13 @@ static bool TryGetGrpcAddress(out string address) // 1. DaprDefaults.cs uses 127.0.0.1 instead of localhost, which prevents testing with Dapr on WSL2 and the app on Windows // 2. DaprDefaults.cs doesn't compile when the project has C# nullable reference types enabled. // If the above issues are fixed (ensuring we don't regress anything) we should switch to using the logic in DaprDefaults.cs. - string? daprEndpoint = Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT"); + var daprEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); if (!String.IsNullOrEmpty(daprEndpoint)) { address = daprEndpoint; return true; } - string? daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); + var daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); if (int.TryParse(daprPortStr, out int daprGrpcPort)) { // There is a bug in the Durable Task SDK that requires us to change the format of the address @@ -126,6 +152,33 @@ static bool TryGetGrpcAddress(out string address) address = string.Empty; return false; } + + static GrpcChannel CreateChannel(string address, HttpClient client) + { + + GrpcChannelOptions options = new() { HttpClient = client}; + var daprEndpoint = DaprDefaults.GetDefaultGrpcEndpoint(); + if (!String.IsNullOrEmpty(daprEndpoint)) { + return GrpcChannel.ForAddress(daprEndpoint, options); + } + + var daprPortStr = Environment.GetEnvironmentVariable("DAPR_GRPC_PORT"); + if (int.TryParse(daprPortStr, out int daprGrpcPort)) + { + // If there is no address passed in, we default to localhost + if (String.IsNullOrEmpty(address)) + { + // There is a bug in the Durable Task SDK that requires us to change the format of the address + // depending on the version of .NET that we're targeting. For now, we work around this manually. + #if NET6_0_OR_GREATER + address = $"http://localhost:{daprGrpcPort}"; + #else + address = $"localhost:{daprGrpcPort}"; + #endif + } + + } + return GrpcChannel.ForAddress(address, options); + } } } - diff --git a/src/Shared/DaprDefaults.cs b/src/Shared/DaprDefaults.cs index 1ddab49b0..b738de921 100644 --- a/src/Shared/DaprDefaults.cs +++ b/src/Shared/DaprDefaults.cs @@ -17,10 +17,10 @@ namespace Dapr { internal static class DaprDefaults { - private static string httpEndpoint; - private static string grpcEndpoint; - private static string daprApiToken; - private static string appApiToken; + private static string httpEndpoint = string.Empty; + private static string grpcEndpoint = string.Empty; + private static string daprApiToken = string.Empty; + private static string appApiToken = string.Empty; /// /// Get the value of environment variable DAPR_API_TOKEN @@ -31,11 +31,11 @@ public static string GetDefaultDaprApiToken() // Lazy-init is safe because this is just populating the default // We don't plan to support the case where the user changes environment variables // for a running process. - if (daprApiToken == null) + if (string.IsNullOrEmpty(daprApiToken)) { // Treat empty the same as null since it's an environment variable var value = Environment.GetEnvironmentVariable("DAPR_API_TOKEN"); - daprApiToken = (value == string.Empty) ? null : value; + daprApiToken = string.IsNullOrEmpty(value) ? string.Empty : value; } return daprApiToken; @@ -47,10 +47,10 @@ public static string GetDefaultDaprApiToken() /// The value of environment variable APP_API_TOKEN public static string GetDefaultAppApiToken() { - if (appApiToken == null) + if (string.IsNullOrEmpty(appApiToken)) { var value = Environment.GetEnvironmentVariable("APP_API_TOKEN"); - appApiToken = (value == string.Empty) ? null : value; + appApiToken = string.IsNullOrEmpty(value) ? string.Empty : value; } return appApiToken; @@ -62,7 +62,7 @@ public static string GetDefaultAppApiToken() /// The value of HTTP endpoint based off environment variables public static string GetDefaultHttpEndpoint() { - if (httpEndpoint == null) + if (string.IsNullOrEmpty(httpEndpoint)) { var endpoint = Environment.GetEnvironmentVariable("DAPR_HTTP_ENDPOINT"); if (!string.IsNullOrEmpty(endpoint)) { @@ -84,7 +84,7 @@ public static string GetDefaultHttpEndpoint() /// The value of gRPC endpoint based off environment variables public static string GetDefaultGrpcEndpoint() { - if (grpcEndpoint == null) + if (string.IsNullOrEmpty(grpcEndpoint)) { var endpoint = Environment.GetEnvironmentVariable("DAPR_GRPC_ENDPOINT"); if (!string.IsNullOrEmpty(endpoint)) {