From 4404175eb3642796fa88be362ea27211912c0f87 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 27 Oct 2024 17:07:10 -0500 Subject: [PATCH 01/18] Removed obsolete type Signed-off-by: Whit Waldo --- src/Dapr.Workflow/WorkflowEngineClient.cs | 34 ------------------- .../WorkflowServiceCollectionExtensions.cs | 3 -- 2 files changed, 37 deletions(-) delete mode 100644 src/Dapr.Workflow/WorkflowEngineClient.cs diff --git a/src/Dapr.Workflow/WorkflowEngineClient.cs b/src/Dapr.Workflow/WorkflowEngineClient.cs deleted file mode 100644 index c5869b3c6..000000000 --- a/src/Dapr.Workflow/WorkflowEngineClient.cs +++ /dev/null @@ -1,34 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2023 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -namespace Dapr.Workflow -{ - using System; - using Microsoft.DurableTask.Client; - - /// - /// Deprecated. Use instead. - /// - [Obsolete($"Deprecated. Use {nameof(DaprWorkflowClient)} instead.")] - public sealed class WorkflowEngineClient : DaprWorkflowClient - { - /// - /// Deprecated. Use instead. - /// - /// - public WorkflowEngineClient(DurableTaskClient innerClient) - : base(innerClient) - { - } - } -} diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index 3c19583aa..a88a08e24 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -39,9 +39,6 @@ public static IServiceCollection AddDaprWorkflow( serviceCollection.TryAddSingleton(); serviceCollection.AddHttpClient(); -#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient - serviceCollection.TryAddSingleton(); -#pragma warning restore CS0618 // Type or member is obsolete serviceCollection.AddHostedService(); serviceCollection.TryAddSingleton(); serviceCollection.AddDaprClient(); From 662b620bbc159b2783642bd91583eff92f3c178a Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 27 Oct 2024 17:07:33 -0500 Subject: [PATCH 02/18] Added missing using Signed-off-by: Whit Waldo --- src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index a88a08e24..5224799d5 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Microsoft.Extensions.Logging; + namespace Dapr.Workflow { using System; From cf876c008355f1f7aba4802c6f3e035237176284 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 27 Oct 2024 17:09:07 -0500 Subject: [PATCH 03/18] Adding interface for IWorkflowContext for replayability concerns Signed-off-by: Whit Waldo --- src/Dapr.Workflow/IWorkflowContext.cs | 21 +++++++++++++++++++++ src/Dapr.Workflow/WorkflowContext.cs | 4 ++-- 2 files changed, 23 insertions(+), 2 deletions(-) create mode 100644 src/Dapr.Workflow/IWorkflowContext.cs diff --git a/src/Dapr.Workflow/IWorkflowContext.cs b/src/Dapr.Workflow/IWorkflowContext.cs new file mode 100644 index 000000000..7bbbb4f94 --- /dev/null +++ b/src/Dapr.Workflow/IWorkflowContext.cs @@ -0,0 +1,21 @@ +namespace Dapr.Workflow; + +/// +/// Provides functionality available to orchestration code. +/// +public interface IWorkflowContext +{ + /// + /// Gets a value indicating whether the orchestration or operation is currently replaying itself. + /// + /// + /// This property is useful when there is logic that needs to run only when *not* replaying. For example, + /// certain types of application logging may become too noisy when duplicated as part of replay. The + /// application code could check to see whether the function is being replayed and then issue + /// the log statements when this value is false. + /// + /// + /// true if the orchestration or operation is currently being replayed; otherwise false. + /// + bool IsReplaying { get; } +} diff --git a/src/Dapr.Workflow/WorkflowContext.cs b/src/Dapr.Workflow/WorkflowContext.cs index 98b8be96b..ebc12b097 100644 --- a/src/Dapr.Workflow/WorkflowContext.cs +++ b/src/Dapr.Workflow/WorkflowContext.cs @@ -21,13 +21,13 @@ namespace Dapr.Workflow /// Context object used by workflow implementations to perform actions such as scheduling activities, durable timers, waiting for /// external events, and for getting basic information about the current workflow instance. /// - public abstract class WorkflowContext + public abstract class WorkflowContext : IWorkflowContext { /// /// Gets the name of the current workflow. /// public abstract string Name { get; } - + /// /// Gets the instance ID of the current workflow. /// From be5340a734ad5578eaacbb684413f99211ea7258 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 27 Oct 2024 17:09:24 -0500 Subject: [PATCH 04/18] Removed unused IConfiguration Signed-off-by: Whit Waldo --- src/Dapr.Workflow/WorkflowLoggingService.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Dapr.Workflow/WorkflowLoggingService.cs b/src/Dapr.Workflow/WorkflowLoggingService.cs index 331156f3e..115db817f 100644 --- a/src/Dapr.Workflow/WorkflowLoggingService.cs +++ b/src/Dapr.Workflow/WorkflowLoggingService.cs @@ -29,10 +29,9 @@ internal sealed class WorkflowLoggingService : IHostedService private static readonly HashSet registeredWorkflows = new(); private static readonly HashSet registeredActivities = new(); - public WorkflowLoggingService(ILogger logger, IConfiguration configuration) + public WorkflowLoggingService(ILogger logger) { this.logger = logger; - } public Task StartAsync(CancellationToken cancellationToken) { From fcdeadff71c18063f8c56415b5b38debebfc6bd0 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 27 Oct 2024 17:09:49 -0500 Subject: [PATCH 05/18] Added ReplaySafeLogger type Signed-off-by: Whit Waldo --- src/Dapr.Workflow/ReplaySafeLogger.cs | 45 +++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 src/Dapr.Workflow/ReplaySafeLogger.cs diff --git a/src/Dapr.Workflow/ReplaySafeLogger.cs b/src/Dapr.Workflow/ReplaySafeLogger.cs new file mode 100644 index 000000000..020656b82 --- /dev/null +++ b/src/Dapr.Workflow/ReplaySafeLogger.cs @@ -0,0 +1,45 @@ +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE in the project root for license information. + +using System; +using Microsoft.Extensions.Logging; + +namespace Dapr.Workflow; + +internal class ReplaySafeLogger : ILogger +{ + private readonly IWorkflowContext context; + private readonly ILogger logger; + + public ReplaySafeLogger(ILogger logger, IWorkflowContext context) + { + this.context = context; + this.logger = logger; + } + + public IDisposable BeginScope(TState state) => this.logger.BeginScope(state); + + public bool IsEnabled(LogLevel logLevel) => this.logger.IsEnabled(logLevel); + + public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, + Func formatter) + { + if (!this.context.IsReplaying) + { + this.logger.Log(logLevel, eventId, state, exception, formatter); + } + } +} From bcea8442e7858521c43c2262ab10aff83f99fad9 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Sun, 27 Oct 2024 17:46:33 -0500 Subject: [PATCH 06/18] Building out functionality to expose ReplayLogger in workflow context Signed-off-by: Whit Waldo --- .../DaprWorkflowContextExtensions.cs | 19 +++++++++++++++++++ .../WorkflowServiceCollectionExtensions.cs | 2 ++ 2 files changed, 21 insertions(+) create mode 100644 src/Dapr.Workflow/DaprWorkflowContextExtensions.cs diff --git a/src/Dapr.Workflow/DaprWorkflowContextExtensions.cs b/src/Dapr.Workflow/DaprWorkflowContextExtensions.cs new file mode 100644 index 000000000..8b8c5f16a --- /dev/null +++ b/src/Dapr.Workflow/DaprWorkflowContextExtensions.cs @@ -0,0 +1,19 @@ +using Microsoft.Extensions.Logging; + +namespace Dapr.Workflow; + +/// +/// Defines convenient overloads for calling context methods. +/// +public static class DaprWorkflowContextExtensions +{ + /// + /// Returns an instance of that is replay safe, ensuring the logger logs only + /// when the orchestrator is not replaying that line of code. + /// + /// The workflow context. + /// An instance of . + /// An instance of a replay-safe . + public static ILogger CreateReplaySafeLogger(this IWorkflowContext context, ILogger logger) => + new ReplaySafeLogger(logger, context); +} diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index 5224799d5..0d3052f5d 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -42,6 +42,8 @@ public static IServiceCollection AddDaprWorkflow( serviceCollection.AddHttpClient(); serviceCollection.AddHostedService(); + + serviceCollection.TryAddSingleton(); serviceCollection.TryAddSingleton(); serviceCollection.AddDaprClient(); From 0a871f5f5865549f569487bd448794aafb3d50f0 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Wed, 18 Dec 2024 23:38:52 -0600 Subject: [PATCH 07/18] Added license information to file Signed-off-by: Whit Waldo --- .../DaprWorkflowContextExtensions.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/src/Dapr.Workflow/DaprWorkflowContextExtensions.cs b/src/Dapr.Workflow/DaprWorkflowContextExtensions.cs index 8b8c5f16a..32cfcdd91 100644 --- a/src/Dapr.Workflow/DaprWorkflowContextExtensions.cs +++ b/src/Dapr.Workflow/DaprWorkflowContextExtensions.cs @@ -1,4 +1,21 @@ -using Microsoft.Extensions.Logging; +// ------------------------------------------------------------------------ +// Copyright 2024 The Dapr Authors +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// http://www.apache.org/licenses/LICENSE-2.0 +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. +// ------------------------------------------------------------------------ + +// Copyright (c) .NET Foundation. All rights reserved. +// Licensed under the MIT License. See LICENSE at +// https://github.com/Azure/azure-functions-durable-extension/blob/dev/LICENSE for license information. + +using Microsoft.Extensions.Logging; namespace Dapr.Workflow; From 2a779084d2227c0ec1166dc4a255472ca03faaf3 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 19 Dec 2024 00:04:58 -0600 Subject: [PATCH 08/18] Removed unnecessary file Signed-off-by: Whit Waldo --- .../DaprWorkflowContextExtensions.cs | 36 ------------------- 1 file changed, 36 deletions(-) delete mode 100644 src/Dapr.Workflow/DaprWorkflowContextExtensions.cs diff --git a/src/Dapr.Workflow/DaprWorkflowContextExtensions.cs b/src/Dapr.Workflow/DaprWorkflowContextExtensions.cs deleted file mode 100644 index 32cfcdd91..000000000 --- a/src/Dapr.Workflow/DaprWorkflowContextExtensions.cs +++ /dev/null @@ -1,36 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2024 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See LICENSE at -// https://github.com/Azure/azure-functions-durable-extension/blob/dev/LICENSE for license information. - -using Microsoft.Extensions.Logging; - -namespace Dapr.Workflow; - -/// -/// Defines convenient overloads for calling context methods. -/// -public static class DaprWorkflowContextExtensions -{ - /// - /// Returns an instance of that is replay safe, ensuring the logger logs only - /// when the orchestrator is not replaying that line of code. - /// - /// The workflow context. - /// An instance of . - /// An instance of a replay-safe . - public static ILogger CreateReplaySafeLogger(this IWorkflowContext context, ILogger logger) => - new ReplaySafeLogger(logger, context); -} From 1d11d4038230380abbd79fe332e98f6c7f400d72 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 19 Dec 2024 00:05:25 -0600 Subject: [PATCH 09/18] Updated copyright header for different project, made some tweaks for nullability errors Signed-off-by: Whit Waldo --- src/Dapr.Workflow/ReplaySafeLogger.cs | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/Dapr.Workflow/ReplaySafeLogger.cs b/src/Dapr.Workflow/ReplaySafeLogger.cs index 020656b82..2ee327559 100644 --- a/src/Dapr.Workflow/ReplaySafeLogger.cs +++ b/src/Dapr.Workflow/ReplaySafeLogger.cs @@ -11,8 +11,9 @@ // limitations under the License. // ------------------------------------------------------------------------ -// Copyright (c) .NET Foundation. All rights reserved. -// Licensed under the MIT License. See LICENSE in the project root for license information. +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License at +// https://github.com/microsoft/durabletask-dotnet/blob/main/LICENSE using System; using Microsoft.Extensions.Logging; @@ -24,13 +25,17 @@ internal class ReplaySafeLogger : ILogger private readonly IWorkflowContext context; private readonly ILogger logger; - public ReplaySafeLogger(ILogger logger, IWorkflowContext context) + public ReplaySafeLogger(IWorkflowContext context, ILogger logger) { - this.context = context; - this.logger = logger; + this.context = context ?? throw new ArgumentNullException(nameof(context)); + this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); } - public IDisposable BeginScope(TState state) => this.logger.BeginScope(state); + IDisposable ILogger.BeginScope(TState state) + { + ArgumentNullException.ThrowIfNull(state, nameof(state)); + return this.logger.BeginScope(state)!; + } public bool IsEnabled(LogLevel logLevel) => this.logger.IsEnabled(logLevel); @@ -39,7 +44,7 @@ public void Log(LogLevel logLevel, EventId eventId, TState state, Except { if (!this.context.IsReplaying) { - this.logger.Log(logLevel, eventId, state, exception, formatter); + this.logger.Log(logLevel, eventId, state, exception, formatter); } } } From ed19f04b660b5f21ed0e5dddd82ffd1177ae5289 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 19 Dec 2024 00:06:33 -0600 Subject: [PATCH 10/18] Added virtual methods that use the already-available ILoggerFactory to create the ReplaySafeLogger on the WorkflowContext Signed-off-by: Whit Waldo --- src/Dapr.Workflow/DaprWorkflowContext.cs | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/src/Dapr.Workflow/DaprWorkflowContext.cs b/src/Dapr.Workflow/DaprWorkflowContext.cs index be08ef421..a24b89bd7 100644 --- a/src/Dapr.Workflow/DaprWorkflowContext.cs +++ b/src/Dapr.Workflow/DaprWorkflowContext.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Microsoft.Extensions.Logging; + namespace Dapr.Workflow { using System; @@ -95,6 +97,25 @@ public override Guid NewGuid() return this.innerContext.NewGuid(); } + /// + /// Returns an instance of that is replay-safe, meaning that the logger only + /// writes logs when the orchestrator is not replaying previous history. + /// + /// The logger's category name. + /// An instance of that is replay-safe. + public virtual ILogger CreateReplaySafeLogger(string categoryName) => + new ReplaySafeLogger(this, this.innerContext.CreateReplaySafeLogger(categoryName)); + + /// + /// The type to derive the category name from. + public virtual ILogger CreateReplaySafeLogger(Type type) => + new ReplaySafeLogger(this, this.innerContext.CreateReplaySafeLogger(type)); + + /// + /// The type to derive category name from. + public virtual ILogger CreateReplaySafeLogger() => + new ReplaySafeLogger(this, this.innerContext.CreateReplaySafeLogger()); + static async Task WrapExceptions(Task task) { try From 04a77a24af72a6ae4c29676352ed5cec2988c3ba Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 19 Dec 2024 00:07:20 -0600 Subject: [PATCH 11/18] Removed unnecessary registration Signed-off-by: Whit Waldo --- src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index 0d3052f5d..2e8d78b4f 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -11,8 +11,6 @@ // limitations under the License. // ------------------------------------------------------------------------ -using Microsoft.Extensions.Logging; - namespace Dapr.Workflow { using System; @@ -43,7 +41,6 @@ public static IServiceCollection AddDaprWorkflow( serviceCollection.AddHostedService(); - serviceCollection.TryAddSingleton(); serviceCollection.TryAddSingleton(); serviceCollection.AddDaprClient(); From 283e98d8deefa380e409d17a816e5ca8ce49c3e6 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 19 Dec 2024 00:20:45 -0600 Subject: [PATCH 12/18] Updated example to demonstrate using ReplaySafeLogger in the orchestration context Signed-off-by: Whit Waldo --- .../Workflows/OrderProcessingWorkflow.cs | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs index 3b8af5951..8c5e9d133 100644 --- a/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs +++ b/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs @@ -1,4 +1,5 @@ using Dapr.Workflow; +using Microsoft.Extensions.Logging; using WorkflowConsoleApp.Activities; namespace WorkflowConsoleApp.Workflows @@ -16,7 +17,10 @@ public class OrderProcessingWorkflow : Workflow public override async Task RunAsync(WorkflowContext context, OrderPayload order) { string orderId = context.InstanceId; + var logger = context.CreateReplaySafeLogger(); + logger.LogInformation("Received order {orderId} for {quantity} {name} at ${totalCost}", orderId, order.Quantity, order.Name, order.TotalCost); + // Notify the user that an order has come through await context.CallActivityAsync( nameof(NotifyActivity), @@ -31,6 +35,8 @@ await context.CallActivityAsync( // If there is insufficient inventory, fail and let the user know if (!result.Success) { + logger.LogError("Insufficient inventory for {orderName}", order.Name); + // End the workflow here since we don't have sufficient inventory await context.CallActivityAsync( nameof(NotifyActivity), @@ -39,8 +45,10 @@ await context.CallActivityAsync( } // Require orders over a certain threshold to be approved - if (order.TotalCost > 50000) + const int threshold = 50000; + if (order.TotalCost > threshold) { + logger.LogInformation("Requesting manager approval since total cost {totalCost} exceeds threshold {threshold}", order.TotalCost, threshold); // Request manager approval for the order await context.CallActivityAsync(nameof(RequestApprovalActivity), order); @@ -51,9 +59,13 @@ await context.CallActivityAsync( ApprovalResult approvalResult = await context.WaitForExternalEventAsync( eventName: "ManagerApproval", timeout: TimeSpan.FromSeconds(30)); + + logger.LogInformation("Approval result: {approvalResult}", approvalResult); context.SetCustomStatus($"Approval result: {approvalResult}"); if (approvalResult == ApprovalResult.Rejected) { + logger.LogWarning("Order was rejected by approver"); + // The order was rejected, end the workflow here await context.CallActivityAsync( nameof(NotifyActivity), @@ -63,6 +75,8 @@ await context.CallActivityAsync( } catch (TaskCanceledException) { + logger.LogError("Cancelling order because it didn't receive an approval"); + // An approval timeout results in automatic order cancellation await context.CallActivityAsync( nameof(NotifyActivity), @@ -72,6 +86,7 @@ await context.CallActivityAsync( } // There is enough inventory available so the user can purchase the item(s). Process their payment + logger.LogInformation("Processing payment as sufficient inventory is available"); await context.CallActivityAsync( nameof(ProcessPaymentActivity), new PaymentRequest(RequestId: orderId, order.Name, order.Quantity, order.TotalCost), @@ -88,6 +103,7 @@ await context.CallActivityAsync( catch (WorkflowTaskFailedException e) { // Let them know their payment processing failed + logger.LogError("Order {orderId} failed! Details: {errorMessage}", orderId, e.FailureDetails.ErrorMessage); await context.CallActivityAsync( nameof(NotifyActivity), new Notification($"Order {orderId} Failed! Details: {e.FailureDetails.ErrorMessage}")); @@ -95,6 +111,7 @@ await context.CallActivityAsync( } // Let them know their payment was processed + logger.LogError("Order {orderId} has completed!", orderId); await context.CallActivityAsync( nameof(NotifyActivity), new Notification($"Order {orderId} has completed!")); From 182d58e99bb2113773134fb1854496ede100f341 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 19 Dec 2024 00:21:30 -0600 Subject: [PATCH 13/18] Tweaks on visibility and abstraction so that the methods are available in the context made visible to workflow developers Signed-off-by: Whit Waldo --- src/Dapr.Workflow/DaprWorkflowContext.cs | 12 ++++++------ src/Dapr.Workflow/WorkflowContext.cs | 18 ++++++++++++++++++ 2 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/Dapr.Workflow/DaprWorkflowContext.cs b/src/Dapr.Workflow/DaprWorkflowContext.cs index a24b89bd7..c745a69ad 100644 --- a/src/Dapr.Workflow/DaprWorkflowContext.cs +++ b/src/Dapr.Workflow/DaprWorkflowContext.cs @@ -96,24 +96,24 @@ public override Guid NewGuid() { return this.innerContext.NewGuid(); } - + /// /// Returns an instance of that is replay-safe, meaning that the logger only /// writes logs when the orchestrator is not replaying previous history. /// /// The logger's category name. /// An instance of that is replay-safe. - public virtual ILogger CreateReplaySafeLogger(string categoryName) => + public override ILogger CreateReplaySafeLogger(string categoryName) => new ReplaySafeLogger(this, this.innerContext.CreateReplaySafeLogger(categoryName)); - + /// /// The type to derive the category name from. - public virtual ILogger CreateReplaySafeLogger(Type type) => + public override ILogger CreateReplaySafeLogger(Type type) => new ReplaySafeLogger(this, this.innerContext.CreateReplaySafeLogger(type)); - + /// /// The type to derive category name from. - public virtual ILogger CreateReplaySafeLogger() => + public override ILogger CreateReplaySafeLogger() => new ReplaySafeLogger(this, this.innerContext.CreateReplaySafeLogger()); static async Task WrapExceptions(Task task) diff --git a/src/Dapr.Workflow/WorkflowContext.cs b/src/Dapr.Workflow/WorkflowContext.cs index ebc12b097..34755fb32 100644 --- a/src/Dapr.Workflow/WorkflowContext.cs +++ b/src/Dapr.Workflow/WorkflowContext.cs @@ -11,6 +11,8 @@ // limitations under the License. // ------------------------------------------------------------------------ +using Microsoft.Extensions.Logging; + namespace Dapr.Workflow { using System; @@ -272,6 +274,22 @@ public virtual Task CallChildWorkflowAsync( return this.CallChildWorkflowAsync(workflowName, input, options); } + /// + /// Returns an instance of that is replay-safe, meaning that the logger only + /// writes logs when the orchestrator is not replaying previous history. + /// + /// The logger's category name. + /// An instance of that is replay-safe. + public abstract ILogger CreateReplaySafeLogger(string categoryName); + + /// + /// The type to derive the category name from. + public abstract ILogger CreateReplaySafeLogger(Type type); + + /// + /// The type to derive category name from. + public abstract ILogger CreateReplaySafeLogger(); + /// /// Restarts the workflow with a new input and clears its history. /// From 3d6641ef848191879338bf032e2943fa00b5b846 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 19 Dec 2024 00:35:50 -0600 Subject: [PATCH 14/18] Removed obsolete type registrations Signed-off-by: Whit Waldo --- .../WorkflowServiceCollectionExtensions.cs | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index 994f587ac..7ae838849 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -43,42 +43,33 @@ public static IServiceCollection AddDaprWorkflow( serviceCollection.AddDaprClient(lifetime: lifetime); serviceCollection.AddHttpClient(); serviceCollection.AddHostedService(); - + switch (lifetime) { case ServiceLifetime.Singleton: -#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient - serviceCollection.TryAddSingleton(); -#pragma warning restore CS0618 // Type or member is obsolete serviceCollection.TryAddSingleton(); serviceCollection.TryAddSingleton(); break; case ServiceLifetime.Scoped: -#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient - serviceCollection.TryAddScoped(); -#pragma warning restore CS0618 // Type or member is obsolete serviceCollection.TryAddScoped(); serviceCollection.TryAddScoped(); break; case ServiceLifetime.Transient: -#pragma warning disable CS0618 // Type or member is obsolete - keeping around temporarily - replaced by DaprWorkflowClient - serviceCollection.TryAddTransient(); -#pragma warning restore CS0618 // Type or member is obsolete serviceCollection.TryAddTransient(); serviceCollection.TryAddTransient(); break; default: throw new ArgumentOutOfRangeException(nameof(lifetime), lifetime, null); } - + serviceCollection.AddOptions().Configure(configure); - + //Register the factory and force resolution so the Durable Task client and worker can be registered using (var scope = serviceCollection.BuildServiceProvider().CreateScope()) { var httpClientFactory = scope.ServiceProvider.GetRequiredService(); var configuration = scope.ServiceProvider.GetService(); - + var factory = new DaprWorkflowClientBuilderFactory(configuration, httpClientFactory); factory.CreateClientBuilder(serviceCollection, configure); } From 78ad0da77afda4f22eccd9d76da197a119705c01 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 19 Dec 2024 00:36:31 -0600 Subject: [PATCH 15/18] Simplified argument null check Signed-off-by: Whit Waldo --- src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index 7ae838849..342034f24 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -35,10 +35,7 @@ public static IServiceCollection AddDaprWorkflow( Action configure, ServiceLifetime lifetime = ServiceLifetime.Singleton) { - if (serviceCollection == null) - { - throw new ArgumentNullException(nameof(serviceCollection)); - } + ArgumentNullException.ThrowIfNull(serviceCollection, nameof(serviceCollection)); serviceCollection.AddDaprClient(lifetime: lifetime); serviceCollection.AddHttpClient(); From 1d3f06f1ea172fe8444445267e5a079b100965f6 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 19 Dec 2024 00:37:22 -0600 Subject: [PATCH 16/18] Removed since-removed code leftover from merge Signed-off-by: Whit Waldo --- .../WorkflowServiceCollectionExtensions.cs | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs index 342034f24..f45d21efa 100644 --- a/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs +++ b/src/Dapr.Workflow/WorkflowServiceCollectionExtensions.cs @@ -70,22 +70,6 @@ public static IServiceCollection AddDaprWorkflow( var factory = new DaprWorkflowClientBuilderFactory(configuration, httpClientFactory); factory.CreateClientBuilder(serviceCollection, configure); } - serviceCollection.TryAddSingleton(); - serviceCollection.AddHttpClient(); - - serviceCollection.AddHostedService(); - - serviceCollection.TryAddSingleton(); - serviceCollection.AddDaprClient(); - - serviceCollection.AddOptions().Configure(configure); - - serviceCollection.AddSingleton(c => - { - var factory = c.GetRequiredService(); - factory.CreateClientBuilder(configure); - return new object(); //Placeholder as actual registration is performed inside factory - }); return serviceCollection; } From 4f32540470da7d05086c2c5ca11ee17517828242 Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 19 Dec 2024 01:01:58 -0600 Subject: [PATCH 17/18] Added documentation demonstrating how to access the replay-safe logger Signed-off-by: Whit Waldo --- .../dotnet-workflowclient-usage.md | 62 ++++++++++++++++++- 1 file changed, 61 insertions(+), 1 deletion(-) diff --git a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md index ac6a0f189..a376e6acb 100644 --- a/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md +++ b/daprdocs/content/en/dotnet-sdk-docs/dotnet-workflow/dotnet-workflowclient-usage.md @@ -74,4 +74,64 @@ builder.Services.AddDaprWorkflow(options => { var app = builder.Build(); await app.RunAsync(); -``` \ No newline at end of file +``` + +## Injecting Services into Workflow Activities +Workflow activities support the same dependency injection that developers have come to expect of modern C# applications. Assuming a proper +registration at startup, any such type can be injected into the constructor of the workflow activity and available to utilize during +the execution of the workflow. This makes it simple to add logging via an injected `ILogger` or access to other Dapr +building blocks by injecting `DaprClient` or `DaprJobsClient`, for example. + +```csharp +internal sealed class SquareNumberActivity : WorkflowActivity +{ + private readonly ILogger _logger; + + public MyActivity(ILogger logger) + { + this._logger = logger; + } + + public override Task RunAsync(WorkflowActivityContext context, int input) + { + this._logger.LogInformation("Squaring the value {number}", input); + var result = input * input; + this._logger.LogInformation("Got a result of {squareResult}", result); + + return Task.FromResult(result); + } +} +``` + +### Using ILogger in Workflow +Because workflows must be deterministic, it is not possible to inject arbitrary services into them. For example, +if you were able to inject a standard `ILogger` into a workflow and it needed to be replayed because of an error, +subsequent replay from the event source log would result in the log recording additional operations that didn't actually +take place a second or third time because their results were sourced from the log. This has the potential to introduce +a significant amount of confusion. Rather, a replay-safe logger is made available for use within workflows. It will only +log events the first time the workflow runs and will not log anything whenever the workflow is being replaced. + +This logger can be retrieved from a method present on the `WorkflowContext` available on your workflow instance and +otherwise used precisely as you might otherwise use an `ILogger` instance. + +An end-to-end sample demonstrating this can be seen in the +[.NET SDK repository](https://github.com/dapr/dotnet-sdk/blob/master/examples/Workflow/WorkflowConsoleApp/Workflows/OrderProcessingWorkflow.cs) +but a brief extraction of this sample is available below. + +```csharp +public class OrderProcessingWorkflow : Workflow +{ + public override async Task RunAsync(WorkflowContext context, OrderPayload order) + { + string orderId = context.InstanceId; + var logger = context.CreateReplaySafeLogger(); //Use this method to access the logger instance + + logger.LogInformation("Received order {orderId} for {quantity} {name} at ${totalCost}", orderId, order.Quantity, order.Name, order.TotalCost); + + //... + } +} +``` + + + \ No newline at end of file From 53da1d87bac161f508962de83f016dd9f739cb3d Mon Sep 17 00:00:00 2001 From: Whit Waldo Date: Thu, 19 Dec 2024 14:43:29 -0600 Subject: [PATCH 18/18] Removed unnecessary and separate ReplaySafeLogger in favor of method to create it off the TaskOrchestrationContext (innerContext) Signed-off-by: Whit Waldo --- src/Dapr.Workflow/DaprWorkflowContext.cs | 14 +++---- src/Dapr.Workflow/ReplaySafeLogger.cs | 50 ------------------------ src/Dapr.Workflow/WorkflowContext.cs | 2 +- 3 files changed, 8 insertions(+), 58 deletions(-) delete mode 100644 src/Dapr.Workflow/ReplaySafeLogger.cs diff --git a/src/Dapr.Workflow/DaprWorkflowContext.cs b/src/Dapr.Workflow/DaprWorkflowContext.cs index c745a69ad..55c965955 100644 --- a/src/Dapr.Workflow/DaprWorkflowContext.cs +++ b/src/Dapr.Workflow/DaprWorkflowContext.cs @@ -36,7 +36,7 @@ internal DaprWorkflowContext(TaskOrchestrationContext innerContext) public override DateTime CurrentUtcDateTime => this.innerContext.CurrentUtcDateTime; public override bool IsReplaying => this.innerContext.IsReplaying; - + public override Task CallActivityAsync(string name, object? input = null, WorkflowTaskOptions? options = null) { return WrapExceptions(this.innerContext.CallActivityAsync(name, input, options?.ToDurableTaskOptions())); @@ -96,7 +96,7 @@ public override Guid NewGuid() { return this.innerContext.NewGuid(); } - + /// /// Returns an instance of that is replay-safe, meaning that the logger only /// writes logs when the orchestrator is not replaying previous history. @@ -104,17 +104,17 @@ public override Guid NewGuid() /// The logger's category name. /// An instance of that is replay-safe. public override ILogger CreateReplaySafeLogger(string categoryName) => - new ReplaySafeLogger(this, this.innerContext.CreateReplaySafeLogger(categoryName)); - + this.innerContext.CreateReplaySafeLogger(categoryName); + /// /// The type to derive the category name from. public override ILogger CreateReplaySafeLogger(Type type) => - new ReplaySafeLogger(this, this.innerContext.CreateReplaySafeLogger(type)); - + this.innerContext.CreateReplaySafeLogger(type); + /// /// The type to derive category name from. public override ILogger CreateReplaySafeLogger() => - new ReplaySafeLogger(this, this.innerContext.CreateReplaySafeLogger()); + this.innerContext.CreateReplaySafeLogger(); static async Task WrapExceptions(Task task) { diff --git a/src/Dapr.Workflow/ReplaySafeLogger.cs b/src/Dapr.Workflow/ReplaySafeLogger.cs deleted file mode 100644 index 2ee327559..000000000 --- a/src/Dapr.Workflow/ReplaySafeLogger.cs +++ /dev/null @@ -1,50 +0,0 @@ -// ------------------------------------------------------------------------ -// Copyright 2024 The Dapr Authors -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// http://www.apache.org/licenses/LICENSE-2.0 -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. -// ------------------------------------------------------------------------ - -// Copyright (c) Microsoft Corporation. -// Licensed under the MIT License at -// https://github.com/microsoft/durabletask-dotnet/blob/main/LICENSE - -using System; -using Microsoft.Extensions.Logging; - -namespace Dapr.Workflow; - -internal class ReplaySafeLogger : ILogger -{ - private readonly IWorkflowContext context; - private readonly ILogger logger; - - public ReplaySafeLogger(IWorkflowContext context, ILogger logger) - { - this.context = context ?? throw new ArgumentNullException(nameof(context)); - this.logger = logger ?? throw new ArgumentNullException(nameof(logger)); - } - - IDisposable ILogger.BeginScope(TState state) - { - ArgumentNullException.ThrowIfNull(state, nameof(state)); - return this.logger.BeginScope(state)!; - } - - public bool IsEnabled(LogLevel logLevel) => this.logger.IsEnabled(logLevel); - - public void Log(LogLevel logLevel, EventId eventId, TState state, Exception? exception, - Func formatter) - { - if (!this.context.IsReplaying) - { - this.logger.Log(logLevel, eventId, state, exception, formatter); - } - } -} diff --git a/src/Dapr.Workflow/WorkflowContext.cs b/src/Dapr.Workflow/WorkflowContext.cs index 34755fb32..afc544ed5 100644 --- a/src/Dapr.Workflow/WorkflowContext.cs +++ b/src/Dapr.Workflow/WorkflowContext.cs @@ -273,7 +273,7 @@ public virtual Task CallChildWorkflowAsync( { return this.CallChildWorkflowAsync(workflowName, input, options); } - + /// /// Returns an instance of that is replay-safe, meaning that the logger only /// writes logs when the orchestrator is not replaying previous history.