From 3e6ca57fe95c1ea92c0bba1eb3ae068fcfe760ce Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Tue, 2 Sep 2025 09:43:51 -0700 Subject: [PATCH 01/10] initial commit --- .../ExceptionPropertiesProviderRegistry.cs | 29 ++++ src/DurableTask.Core/FailureDetails.cs | 43 ++++- .../IExceptionPropertiesProvider.cs | 33 ++++ .../ExceptionHandlingIntegrationTests.cs | 152 ++++++++++++++++++ 4 files changed, 254 insertions(+), 3 deletions(-) create mode 100644 src/DurableTask.Core/ExceptionPropertiesProviderRegistry.cs create mode 100644 src/DurableTask.Core/IExceptionPropertiesProvider.cs diff --git a/src/DurableTask.Core/ExceptionPropertiesProviderRegistry.cs b/src/DurableTask.Core/ExceptionPropertiesProviderRegistry.cs new file mode 100644 index 000000000..9d067f7fc --- /dev/null +++ b/src/DurableTask.Core/ExceptionPropertiesProviderRegistry.cs @@ -0,0 +1,29 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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. +// ---------------------------------------------------------------------------------- +#nullable enable +namespace DurableTask.Core +{ + /// + /// Registry for the global exception properties provider. + /// This registry is intended for use by the durabletask-dotnet layer to set a provider + /// that will be used when creating FailureDetails from exceptions. + /// + public static class ExceptionPropertiesProviderRegistry + { + /// + /// Gets or sets the global exception properties provider. + /// This will be set by the durabletask-dotnet layer. + /// + public static IExceptionPropertiesProvider? Provider { get; set; } + } +} diff --git a/src/DurableTask.Core/FailureDetails.cs b/src/DurableTask.Core/FailureDetails.cs index 5dfd02ab9..7b956251c 100644 --- a/src/DurableTask.Core/FailureDetails.cs +++ b/src/DurableTask.Core/FailureDetails.cs @@ -38,14 +38,16 @@ public class FailureDetails : IEquatable /// The exception stack trace. /// The inner cause of the failure. /// Whether the failure is non-retriable. + /// Additional properties associated with the failure. [JsonConstructor] - public FailureDetails(string errorType, string errorMessage, string? stackTrace, FailureDetails? innerFailure, bool isNonRetriable) + public FailureDetails(string errorType, string errorMessage, string? stackTrace, FailureDetails? innerFailure, bool isNonRetriable, IDictionary? properties = null) { this.ErrorType = errorType; this.ErrorMessage = errorMessage; this.StackTrace = stackTrace; this.InnerFailure = innerFailure; this.IsNonRetriable = isNonRetriable; + this.Properties = properties; } /// @@ -54,7 +56,7 @@ public FailureDetails(string errorType, string errorMessage, string? stackTrace, /// The exception used to generate the failure details. /// The inner cause of the failure. public FailureDetails(Exception e, FailureDetails innerFailure) - : this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, innerFailure, false) + : this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, innerFailure, false, GetExceptionProperties(e)) { } @@ -63,7 +65,7 @@ public FailureDetails(Exception e, FailureDetails innerFailure) /// /// The exception used to generate the failure details. public FailureDetails(Exception e) - : this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, FromException(e.InnerException), false) + : this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, FromException(e.InnerException), false, GetExceptionProperties(e)) { } @@ -74,6 +76,7 @@ public FailureDetails() { this.ErrorType = "None"; this.ErrorMessage = string.Empty; + this.Properties = null; } /// @@ -85,6 +88,16 @@ protected FailureDetails(SerializationInfo info, StreamingContext context) this.ErrorMessage = info.GetString(nameof(this.ErrorMessage)); this.StackTrace = info.GetString(nameof(this.StackTrace)); this.InnerFailure = (FailureDetails)info.GetValue(nameof(this.InnerFailure), typeof(FailureDetails)); + // Handle backward compatibility for Properties property - defaults to null + try + { + this.Properties = (IDictionary?)info.GetValue(nameof(this.Properties), typeof(IDictionary)); + } + catch (SerializationException) + { + // Default to null for backward compatibility + this.Properties = null; + } } /// @@ -112,6 +125,11 @@ protected FailureDetails(SerializationInfo info, StreamingContext context) /// public bool IsNonRetriable { get; } + /// + /// Gets additional properties associated with the failure. + /// + public IDictionary? Properties { get; } + /// /// Gets a debug-friendly description of the failure information. /// @@ -206,5 +224,24 @@ static string GetErrorMessage(Exception e) { return e == null ? null : new FailureDetails(e); } + + static IDictionary? GetExceptionProperties(Exception exception) + { + // If this is a TaskFailedException that already has FailureDetails with properties, + // use those properties instead of asking the provider + if (exception is OrchestrationException orchestrationException && + orchestrationException.FailureDetails?.Properties != null) + { + return orchestrationException.FailureDetails.Properties; + } + + if (ExceptionPropertiesProviderRegistry.Provider == null) + { + return null; + } + + return ExceptionPropertiesProviderRegistry.Provider.GetExceptionProperties(exception); + } + } } diff --git a/src/DurableTask.Core/IExceptionPropertiesProvider.cs b/src/DurableTask.Core/IExceptionPropertiesProvider.cs new file mode 100644 index 000000000..1ad2327ee --- /dev/null +++ b/src/DurableTask.Core/IExceptionPropertiesProvider.cs @@ -0,0 +1,33 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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. +// ---------------------------------------------------------------------------------- +#nullable enable +namespace DurableTask.Core +{ + using System; + using System.Collections.Generic; + + /// + /// Interface for providing custom properties from exceptions that will be included in FailureDetails. + /// This interface is intended for implementation by the durabletask-dotnet layer, which will + /// convert customer implementations to this interface and register them with DurableTask.Core. + /// + public interface IExceptionPropertiesProvider + { + /// + /// Extracts custom properties from an exception. + /// + /// The exception to extract properties from. + /// A dictionary of custom properties to include in the FailureDetails, or null if no properties should be added. + IDictionary? GetExceptionProperties(Exception exception); + } +} diff --git a/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs b/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs index 7fb2ef2f5..2f309f9e2 100644 --- a/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs +++ b/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs @@ -14,6 +14,7 @@ namespace DurableTask.Core.Tests { using System; + using System.Collections.Generic; using System.Diagnostics; using System.Runtime.Serialization; using System.Threading.Tasks; @@ -262,6 +263,157 @@ protected override Task ExecuteAsync(TaskContext context, string input) } } + [TestMethod] + public async Task ExceptionPropertiesProvider_ExtractsCustomProperties() + { + // Arrange - Set up a provider that extracts custom properties + var originalProvider = ExceptionPropertiesProviderRegistry.Provider; + try + { + ExceptionPropertiesProviderRegistry.Provider = new TestExceptionPropertiesProvider(); + + this.worker.ErrorPropagationMode = ErrorPropagationMode.UseFailureDetails; + await this.worker + .AddTaskOrchestrations(typeof(ThrowCustomExceptionOrchestration)) + .AddTaskActivities(typeof(ThrowCustomBusinessExceptionActivity)) + .StartAsync(); + + // Act - Start orchestration that will throw a custom exception + var instance = await this.client.CreateOrchestrationInstanceAsync(typeof(ThrowCustomExceptionOrchestration), "test-input"); + var result = await this.client.WaitForOrchestrationAsync(instance, DefaultTimeout); + + // Assert - Check that custom properties were extracted + Assert.AreEqual(OrchestrationStatus.Failed, result.OrchestrationStatus); + Assert.IsNotNull(result.FailureDetails); + Assert.IsNotNull(result.FailureDetails.Properties); + + // Verify the custom properties from our test provider + Assert.AreEqual("CustomBusinessException", result.FailureDetails.Properties["ExceptionTypeName"]); + Assert.AreEqual("user123", result.FailureDetails.Properties["UserId"]); + Assert.AreEqual("OrderProcessing", result.FailureDetails.Properties["BusinessContext"]); + Assert.IsTrue(result.FailureDetails.Properties.ContainsKey("Timestamp")); + } + finally + { + // Clean up - restore original provider + ExceptionPropertiesProviderRegistry.Provider = originalProvider; + await this.worker.StopAsync(); + } + } + + [TestMethod] + public async Task ExceptionPropertiesProvider_NullProvider_NoProperties() + { + // Arrange - Ensure no provider is set + var originalProvider = ExceptionPropertiesProviderRegistry.Provider; + try + { + ExceptionPropertiesProviderRegistry.Provider = null; + + this.worker.ErrorPropagationMode = ErrorPropagationMode.UseFailureDetails; + await this.worker + .AddTaskOrchestrations(typeof(ThrowInvalidOperationExceptionOrchestration)) + .AddTaskActivities(typeof(ThrowInvalidOperationExceptionActivity)) + .StartAsync(); + + // Act + var instance = await this.client.CreateOrchestrationInstanceAsync(typeof(ThrowInvalidOperationExceptionOrchestration), "test-input"); + var result = await this.client.WaitForOrchestrationAsync(instance, DefaultTimeout); + + // Assert - Properties should be null when no provider + Assert.AreEqual(OrchestrationStatus.Failed, result.OrchestrationStatus); + Assert.IsNotNull(result.FailureDetails); + Assert.IsNull(result.FailureDetails.Properties); + } + finally + { + ExceptionPropertiesProviderRegistry.Provider = originalProvider; + await this.worker.StopAsync(); + } + } + + class ThrowCustomExceptionOrchestration : TaskOrchestration + { + public override async Task RunTask(OrchestrationContext context, string input) + { + await context.ScheduleTask(typeof(ThrowCustomBusinessExceptionActivity), input); + return "This should never be reached"; + } + } + + class ThrowCustomBusinessExceptionActivity : TaskActivity + { + protected override string Execute(TaskContext context, string input) + { + throw new CustomBusinessException("Payment processing failed", "user123", "OrderProcessing"); + } + } + + class ThrowInvalidOperationExceptionOrchestration : TaskOrchestration + { + public override async Task RunTask(OrchestrationContext context, string input) + { + await context.ScheduleTask(typeof(ThrowInvalidOperationExceptionActivity), input); + return "This should never be reached"; + } + } + + class ThrowInvalidOperationExceptionActivity : TaskActivity + { + protected override string Execute(TaskContext context, string input) + { + throw new InvalidOperationException("This is a test exception"); + } + } + + // Test exception with custom properties + [Serializable] + class CustomBusinessException : Exception + { + public string UserId { get; } + public string BusinessContext { get; } + + public CustomBusinessException(string message, string userId, string businessContext) + : base(message) + { + UserId = userId; + BusinessContext = businessContext; + } + + protected CustomBusinessException(SerializationInfo info, StreamingContext context) + : base(info, context) + { + UserId = info.GetString(nameof(UserId)) ?? string.Empty; + BusinessContext = info.GetString(nameof(BusinessContext)) ?? string.Empty; + } + + public override void GetObjectData(SerializationInfo info, StreamingContext context) + { + base.GetObjectData(info, context); + info.AddValue(nameof(UserId), UserId); + info.AddValue(nameof(BusinessContext), BusinessContext); + } + } + + // Test provider similar to the one shown in the user's example + class TestExceptionPropertiesProvider : IExceptionPropertiesProvider + { + public IDictionary? GetExceptionProperties(Exception exception) + { + return exception switch + { + CustomBusinessException businessEx => new Dictionary + { + ["ExceptionTypeName"] = nameof(CustomBusinessException), + ["UserId"] = businessEx.UserId, + ["BusinessContext"] = businessEx.BusinessContext, + ["Timestamp"] = DateTime.UtcNow + }, + _ => null // No custom properties for other exceptions + }; + } + } + [Serializable] class CustomException : Exception { From f2f8875a06e9e5ae31745d1cecd6cccab35d00e0 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Tue, 2 Sep 2025 10:06:29 -0700 Subject: [PATCH 02/10] udpate test --- .../ExceptionHandlingIntegrationTests.cs | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs b/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs index 2f309f9e2..c7f39e436 100644 --- a/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs +++ b/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs @@ -266,7 +266,7 @@ protected override Task ExecuteAsync(TaskContext context, string input) [TestMethod] public async Task ExceptionPropertiesProvider_ExtractsCustomProperties() { - // Arrange - Set up a provider that extracts custom properties + // Set up a provider that extracts custom properties var originalProvider = ExceptionPropertiesProviderRegistry.Provider; try { @@ -278,16 +278,15 @@ await this.worker .AddTaskActivities(typeof(ThrowCustomBusinessExceptionActivity)) .StartAsync(); - // Act - Start orchestration that will throw a custom exception var instance = await this.client.CreateOrchestrationInstanceAsync(typeof(ThrowCustomExceptionOrchestration), "test-input"); var result = await this.client.WaitForOrchestrationAsync(instance, DefaultTimeout); - // Assert - Check that custom properties were extracted + // Check that custom properties were extracted Assert.AreEqual(OrchestrationStatus.Failed, result.OrchestrationStatus); Assert.IsNotNull(result.FailureDetails); Assert.IsNotNull(result.FailureDetails.Properties); - // Verify the custom properties from our test provider + // Check the properties match the exception. Assert.AreEqual("CustomBusinessException", result.FailureDetails.Properties["ExceptionTypeName"]); Assert.AreEqual("user123", result.FailureDetails.Properties["UserId"]); Assert.AreEqual("OrderProcessing", result.FailureDetails.Properties["BusinessContext"]); @@ -295,39 +294,33 @@ await this.worker } finally { - // Clean up - restore original provider ExceptionPropertiesProviderRegistry.Provider = originalProvider; await this.worker.StopAsync(); } } [TestMethod] + // Test that when no provider is provided by default, property at FailureDetails should be null. public async Task ExceptionPropertiesProvider_NullProvider_NoProperties() { - // Arrange - Ensure no provider is set - var originalProvider = ExceptionPropertiesProviderRegistry.Provider; try { - ExceptionPropertiesProviderRegistry.Provider = null; - this.worker.ErrorPropagationMode = ErrorPropagationMode.UseFailureDetails; await this.worker .AddTaskOrchestrations(typeof(ThrowInvalidOperationExceptionOrchestration)) .AddTaskActivities(typeof(ThrowInvalidOperationExceptionActivity)) .StartAsync(); - // Act var instance = await this.client.CreateOrchestrationInstanceAsync(typeof(ThrowInvalidOperationExceptionOrchestration), "test-input"); var result = await this.client.WaitForOrchestrationAsync(instance, DefaultTimeout); - // Assert - Properties should be null when no provider + // Properties should be null when no provider Assert.AreEqual(OrchestrationStatus.Failed, result.OrchestrationStatus); Assert.IsNotNull(result.FailureDetails); Assert.IsNull(result.FailureDetails.Properties); } finally { - ExceptionPropertiesProviderRegistry.Provider = originalProvider; await this.worker.StopAsync(); } } From df04526910ab4a7c4b55691d355596e11b9bb62e Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Mon, 29 Sep 2025 18:17:31 -0700 Subject: [PATCH 03/10] udpate by commit --- .../ExceptionPropertiesProviderRegistry.cs | 1 + src/DurableTask.Core/FailureDetails.cs | 45 +++++++++++++++---- src/DurableTask.Core/OrchestrationContext.cs | 5 +++ .../ReflectionBasedTaskActivity.cs | 2 +- src/DurableTask.Core/TaskActivity.cs | 2 +- .../TaskActivityDispatcher.cs | 27 ++++++++++- src/DurableTask.Core/TaskContext.cs | 2 + src/DurableTask.Core/TaskEntityDispatcher.cs | 22 +++++++++ src/DurableTask.Core/TaskHubWorker.cs | 20 +++++++-- src/DurableTask.Core/TaskOrchestration.cs | 2 +- .../TaskOrchestrationContext.cs | 14 +++++- .../TaskOrchestrationDispatcher.cs | 27 ++++++++++- .../TaskOrchestrationExecutor.cs | 43 +++++++++++++----- .../ExceptionHandlingIntegrationTests.cs | 10 ++--- 14 files changed, 187 insertions(+), 35 deletions(-) diff --git a/src/DurableTask.Core/ExceptionPropertiesProviderRegistry.cs b/src/DurableTask.Core/ExceptionPropertiesProviderRegistry.cs index 9d067f7fc..709555e34 100644 --- a/src/DurableTask.Core/ExceptionPropertiesProviderRegistry.cs +++ b/src/DurableTask.Core/ExceptionPropertiesProviderRegistry.cs @@ -27,3 +27,4 @@ public static class ExceptionPropertiesProviderRegistry public static IExceptionPropertiesProvider? Provider { get; set; } } } + diff --git a/src/DurableTask.Core/FailureDetails.cs b/src/DurableTask.Core/FailureDetails.cs index 7b956251c..baafd75fb 100644 --- a/src/DurableTask.Core/FailureDetails.cs +++ b/src/DurableTask.Core/FailureDetails.cs @@ -38,16 +38,14 @@ public class FailureDetails : IEquatable /// The exception stack trace. /// The inner cause of the failure. /// Whether the failure is non-retriable. - /// Additional properties associated with the failure. [JsonConstructor] - public FailureDetails(string errorType, string errorMessage, string? stackTrace, FailureDetails? innerFailure, bool isNonRetriable, IDictionary? properties = null) + public FailureDetails(string errorType, string errorMessage, string? stackTrace, FailureDetails? innerFailure, bool isNonRetriable) { this.ErrorType = errorType; this.ErrorMessage = errorMessage; this.StackTrace = stackTrace; this.InnerFailure = innerFailure; this.IsNonRetriable = isNonRetriable; - this.Properties = properties; } /// @@ -56,7 +54,7 @@ public FailureDetails(string errorType, string errorMessage, string? stackTrace, /// The exception used to generate the failure details. /// The inner cause of the failure. public FailureDetails(Exception e, FailureDetails innerFailure) - : this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, innerFailure, false, GetExceptionProperties(e)) + : this(e, innerFailure, null) { } @@ -65,10 +63,33 @@ public FailureDetails(Exception e, FailureDetails innerFailure) /// /// The exception used to generate the failure details. public FailureDetails(Exception e) - : this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, FromException(e.InnerException), false, GetExceptionProperties(e)) + : this(e, (IExceptionPropertiesProvider?)null) { } + /// + /// Initializes a new instance of the class from an exception object. + /// + /// The exception used to generate the failure details. + /// The provider to extract custom properties from the exception. + public FailureDetails(Exception e, IExceptionPropertiesProvider? exceptionPropertiesProvider) + : this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, FromException(e.InnerException, exceptionPropertiesProvider), false) + { + this.Properties = GetExceptionProperties(e, exceptionPropertiesProvider); + } + + /// + /// Initializes a new instance of the class from an exception object. + /// + /// The exception used to generate the failure details. + /// The inner cause of the failure. + /// The provider to extract custom properties from the exception. + public FailureDetails(Exception e, FailureDetails innerFailure, IExceptionPropertiesProvider? exceptionPropertiesProvider) + : this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, innerFailure, false) + { + this.Properties = GetExceptionProperties(e, exceptionPropertiesProvider); + } + /// /// For testing purposes only: Initializes a new, empty instance of the class. /// @@ -222,10 +243,15 @@ static string GetErrorMessage(Exception e) static FailureDetails? FromException(Exception? e) { - return e == null ? null : new FailureDetails(e); + return FromException(e, null); + } + + static FailureDetails? FromException(Exception? e, IExceptionPropertiesProvider? provider) + { + return e == null ? null : new FailureDetails(e, provider); } - static IDictionary? GetExceptionProperties(Exception exception) + static IDictionary? GetExceptionProperties(Exception exception, IExceptionPropertiesProvider? provider) { // If this is a TaskFailedException that already has FailureDetails with properties, // use those properties instead of asking the provider @@ -235,12 +261,13 @@ static string GetErrorMessage(Exception e) return orchestrationException.FailureDetails.Properties; } - if (ExceptionPropertiesProviderRegistry.Provider == null) + if (provider == null) { return null; } - return ExceptionPropertiesProviderRegistry.Provider.GetExceptionProperties(exception); + // If there is a provider provided, then extract exception properties with the provider. + return provider.GetExceptionProperties(exception); } } diff --git a/src/DurableTask.Core/OrchestrationContext.cs b/src/DurableTask.Core/OrchestrationContext.cs index 97c6f6f6d..395f343ae 100644 --- a/src/DurableTask.Core/OrchestrationContext.cs +++ b/src/DurableTask.Core/OrchestrationContext.cs @@ -73,6 +73,11 @@ public abstract class OrchestrationContext /// internal ErrorPropagationMode ErrorPropagationMode { get; set; } + /// + /// + /// + internal IExceptionPropertiesProvider ExceptionPropertiesProvider { get;set; } + /// /// Information about backend entity support, or null if the configured backend does not support entities. /// diff --git a/src/DurableTask.Core/ReflectionBasedTaskActivity.cs b/src/DurableTask.Core/ReflectionBasedTaskActivity.cs index b935f8c1c..f9784d0cd 100644 --- a/src/DurableTask.Core/ReflectionBasedTaskActivity.cs +++ b/src/DurableTask.Core/ReflectionBasedTaskActivity.cs @@ -140,7 +140,7 @@ public override async Task RunAsync(TaskContext context, string input) } else { - failureDetails = new FailureDetails(exception); + failureDetails = new FailureDetails(exception, context.ExceptionPropertiesProvider); } throw new TaskFailureException(exception.Message, exception, details) diff --git a/src/DurableTask.Core/TaskActivity.cs b/src/DurableTask.Core/TaskActivity.cs index b05f020eb..baa01fe92 100644 --- a/src/DurableTask.Core/TaskActivity.cs +++ b/src/DurableTask.Core/TaskActivity.cs @@ -142,7 +142,7 @@ public override async Task RunAsync(TaskContext context, string input) } else { - failureDetails = new FailureDetails(e); + failureDetails = new FailureDetails(e, context.ExceptionPropertiesProvider); } throw new TaskFailureException(e.Message, e, details) diff --git a/src/DurableTask.Core/TaskActivityDispatcher.cs b/src/DurableTask.Core/TaskActivityDispatcher.cs index bdafffdd5..6f50dfcd1 100644 --- a/src/DurableTask.Core/TaskActivityDispatcher.cs +++ b/src/DurableTask.Core/TaskActivityDispatcher.cs @@ -36,6 +36,7 @@ public sealed class TaskActivityDispatcher readonly DispatchMiddlewarePipeline dispatchPipeline; readonly LogHelper logHelper; readonly ErrorPropagationMode errorPropagationMode; + readonly IExceptionPropertiesProvider? exceptionPropertiesProvider; internal TaskActivityDispatcher( IOrchestrationService orchestrationService, @@ -43,12 +44,33 @@ internal TaskActivityDispatcher( DispatchMiddlewarePipeline dispatchPipeline, LogHelper logHelper, ErrorPropagationMode errorPropagationMode) + : this(orchestrationService, objectManager, dispatchPipeline, logHelper, errorPropagationMode, null) + { + } + + /// + /// Initializes a new instance of the class with an exception properties provider. + /// + /// The orchestration service implementation + /// The object manager for activities + /// The dispatch middleware pipeline + /// The log helper + /// The error propagation mode + /// The exception properties provider for extracting custom properties from exceptions + internal TaskActivityDispatcher( + IOrchestrationService orchestrationService, + INameVersionObjectManager objectManager, + DispatchMiddlewarePipeline dispatchPipeline, + LogHelper logHelper, + ErrorPropagationMode errorPropagationMode, + IExceptionPropertiesProvider? exceptionPropertiesProvider) { this.orchestrationService = orchestrationService ?? throw new ArgumentNullException(nameof(orchestrationService)); this.objectManager = objectManager ?? throw new ArgumentNullException(nameof(objectManager)); this.dispatchPipeline = dispatchPipeline ?? throw new ArgumentNullException(nameof(dispatchPipeline)); this.logHelper = logHelper; this.errorPropagationMode = errorPropagationMode; + this.exceptionPropertiesProvider = exceptionPropertiesProvider; this.dispatcher = new WorkItemDispatcher( "TaskActivityDispatcher", @@ -173,7 +195,7 @@ async Task OnProcessWorkItemAsync(TaskActivityWorkItem workItem) ActivityExecutionResult? result; try { - await this.dispatchPipeline.RunAsync(dispatchContext, async _ => + await this.dispatchPipeline.RunAsync(dispatchContext, async _ => { if (taskActivity == null) { @@ -190,6 +212,7 @@ await this.dispatchPipeline.RunAsync(dispatchContext, async _ => scheduledEvent.Version, scheduledEvent.EventId); context.ErrorPropagationMode = this.errorPropagationMode; + context.ExceptionPropertiesProvider = this.exceptionPropertiesProvider; HistoryEvent? responseEvent; @@ -207,7 +230,7 @@ await this.dispatchPipeline.RunAsync(dispatchContext, async _ => string? details = this.IncludeDetails ? $"Unhandled exception while executing task: {e}" : null; - responseEvent = new TaskFailedEvent(-1, scheduledEvent.EventId, e.Message, details, new FailureDetails(e)); + responseEvent = new TaskFailedEvent(-1, scheduledEvent.EventId, e.Message, details, new FailureDetails(e, this.exceptionPropertiesProvider)); traceActivity?.SetStatus(ActivityStatusCode.Error, e.Message); diff --git a/src/DurableTask.Core/TaskContext.cs b/src/DurableTask.Core/TaskContext.cs index d8152976c..06ef51f92 100644 --- a/src/DurableTask.Core/TaskContext.cs +++ b/src/DurableTask.Core/TaskContext.cs @@ -62,5 +62,7 @@ public TaskContext(OrchestrationInstance orchestrationInstance, string name, str /// Gets or sets a value indicating how to propagate unhandled exception metadata. /// internal ErrorPropagationMode ErrorPropagationMode { get; set; } + + internal IExceptionPropertiesProvider? ExceptionPropertiesProvider { get; set; } } } \ No newline at end of file diff --git a/src/DurableTask.Core/TaskEntityDispatcher.cs b/src/DurableTask.Core/TaskEntityDispatcher.cs index a91ae97e2..c88f683a7 100644 --- a/src/DurableTask.Core/TaskEntityDispatcher.cs +++ b/src/DurableTask.Core/TaskEntityDispatcher.cs @@ -42,6 +42,7 @@ public class TaskEntityDispatcher readonly LogHelper logHelper; readonly ErrorPropagationMode errorPropagationMode; readonly TaskOrchestrationDispatcher.NonBlockingCountdownLock concurrentSessionLock; + readonly IExceptionPropertiesProvider exceptionPropertiesProvider; internal TaskEntityDispatcher( IOrchestrationService orchestrationService, @@ -49,12 +50,33 @@ internal TaskEntityDispatcher( DispatchMiddlewarePipeline entityDispatchPipeline, LogHelper logHelper, ErrorPropagationMode errorPropagationMode) + : this(orchestrationService, entityObjectManager, entityDispatchPipeline, logHelper, errorPropagationMode, null) + { + } + + /// + /// Initializes a new instance of the class with an exception properties provider. + /// + /// The orchestration service implementation + /// The object manager for entities + /// The dispatch middleware pipeline + /// The log helper + /// The error propagation mode + /// The exception properties provider for extracting custom properties from exceptions + internal TaskEntityDispatcher( + IOrchestrationService orchestrationService, + INameVersionObjectManager entityObjectManager, + DispatchMiddlewarePipeline entityDispatchPipeline, + LogHelper logHelper, + ErrorPropagationMode errorPropagationMode, + IExceptionPropertiesProvider exceptionPropertiesProvider) { this.objectManager = entityObjectManager ?? throw new ArgumentNullException(nameof(entityObjectManager)); this.orchestrationService = orchestrationService ?? throw new ArgumentNullException(nameof(orchestrationService)); this.dispatchPipeline = entityDispatchPipeline ?? throw new ArgumentNullException(nameof(entityDispatchPipeline)); this.logHelper = logHelper ?? throw new ArgumentNullException(nameof(logHelper)); this.errorPropagationMode = errorPropagationMode; + this.exceptionPropertiesProvider = exceptionPropertiesProvider; this.entityOrchestrationService = (orchestrationService as IEntityOrchestrationService)!; this.entityBackendProperties = entityOrchestrationService.EntityBackendProperties; diff --git a/src/DurableTask.Core/TaskHubWorker.cs b/src/DurableTask.Core/TaskHubWorker.cs index 629453645..96a9f5015 100644 --- a/src/DurableTask.Core/TaskHubWorker.cs +++ b/src/DurableTask.Core/TaskHubWorker.cs @@ -247,6 +247,17 @@ public TaskHubWorker( /// public ErrorPropagationMode ErrorPropagationMode { get; set; } + /// + /// Gets or sets the exception properties provider that extracts custom properties from exceptions + /// when creating FailureDetails objects. + /// + /// + /// + /// This property must be set before the worker is started. Otherwise it will have no effect. + /// + /// + public IExceptionPropertiesProvider ExceptionPropertiesProvider { get; set; } + /// /// Adds a middleware delegate to the orchestration dispatch pipeline. /// @@ -296,13 +307,15 @@ public async Task StartAsync() this.orchestrationDispatchPipeline, this.logHelper, this.ErrorPropagationMode, - this.versioningSettings); + this.versioningSettings, + this.ExceptionPropertiesProvider); this.activityDispatcher = new TaskActivityDispatcher( this.orchestrationService, this.activityManager, this.activityDispatchPipeline, this.logHelper, - this.ErrorPropagationMode); + this.ErrorPropagationMode, + this.ExceptionPropertiesProvider); if (this.dispatchEntitiesSeparately) { @@ -311,7 +324,8 @@ public async Task StartAsync() this.entityManager, this.entityDispatchPipeline, this.logHelper, - this.ErrorPropagationMode); + this.ErrorPropagationMode, + this.ExceptionPropertiesProvider); } await this.orchestrationService.StartAsync(); diff --git a/src/DurableTask.Core/TaskOrchestration.cs b/src/DurableTask.Core/TaskOrchestration.cs index c198c0855..d30ac1878 100644 --- a/src/DurableTask.Core/TaskOrchestration.cs +++ b/src/DurableTask.Core/TaskOrchestration.cs @@ -105,7 +105,7 @@ public override async Task Execute(OrchestrationContext context, string } else { - failureDetails = new FailureDetails(e); + failureDetails = new FailureDetails(e, context.ExceptionPropertiesProvider); } throw new OrchestrationFailureException(e.Message, details) diff --git a/src/DurableTask.Core/TaskOrchestrationContext.cs b/src/DurableTask.Core/TaskOrchestrationContext.cs index b12cb1b08..67e5f275d 100644 --- a/src/DurableTask.Core/TaskOrchestrationContext.cs +++ b/src/DurableTask.Core/TaskOrchestrationContext.cs @@ -37,6 +37,7 @@ internal class TaskOrchestrationContext : OrchestrationContext private int idCounter; private readonly Queue eventsWhileSuspended; private readonly IDictionary suspendedActionsMap; + private readonly IExceptionPropertiesProvider exceptionPropertiesProvider; public bool IsSuspended { get; private set; } @@ -52,6 +53,16 @@ public TaskOrchestrationContext( TaskScheduler taskScheduler, TaskOrchestrationEntityParameters entityParameters = null, ErrorPropagationMode errorPropagationMode = ErrorPropagationMode.SerializeExceptions) + : this(orchestrationInstance, taskScheduler, entityParameters, errorPropagationMode, null) + { + } + + public TaskOrchestrationContext( + OrchestrationInstance orchestrationInstance, + TaskScheduler taskScheduler, + TaskOrchestrationEntityParameters entityParameters, + ErrorPropagationMode errorPropagationMode, + IExceptionPropertiesProvider exceptionPropertiesProvider) { Utils.UnusedParameter(taskScheduler); @@ -66,6 +77,7 @@ public TaskOrchestrationContext( ErrorPropagationMode = errorPropagationMode; this.eventsWhileSuspended = new Queue(); this.suspendedActionsMap = new SortedDictionary(); + this.exceptionPropertiesProvider = exceptionPropertiesProvider; } public IEnumerable OrchestratorActions => this.orchestratorActionsMap.Values; @@ -684,7 +696,7 @@ public void FailOrchestration(Exception failure, OrchestrationRuntimeState runti { if (this.ErrorPropagationMode == ErrorPropagationMode.UseFailureDetails) { - failureDetails = new FailureDetails(failure); + failureDetails = new FailureDetails(failure, this.exceptionPropertiesProvider); } else { diff --git a/src/DurableTask.Core/TaskOrchestrationDispatcher.cs b/src/DurableTask.Core/TaskOrchestrationDispatcher.cs index 42564cddf..550835e05 100644 --- a/src/DurableTask.Core/TaskOrchestrationDispatcher.cs +++ b/src/DurableTask.Core/TaskOrchestrationDispatcher.cs @@ -49,6 +49,7 @@ public class TaskOrchestrationDispatcher readonly EntityBackendProperties? entityBackendProperties; readonly TaskOrchestrationEntityParameters? entityParameters; readonly VersioningSettings? versioningSettings; + readonly IExceptionPropertiesProvider? exceptionPropertiesProvider; internal TaskOrchestrationDispatcher( IOrchestrationService orchestrationService, @@ -57,6 +58,28 @@ internal TaskOrchestrationDispatcher( LogHelper logHelper, ErrorPropagationMode errorPropagationMode, VersioningSettings versioningSettings) + : this(orchestrationService, objectManager, dispatchPipeline, logHelper, errorPropagationMode, versioningSettings, null) + { + } + + /// + /// Initializes a new instance of the class with an exception properties provider. + /// + /// The orchestration service implementation + /// The object manager for orchestrations + /// The dispatch middleware pipeline + /// The log helper + /// The error propagation mode + /// The versioning settings + /// The exception properties provider for extracting custom properties from exceptions + internal TaskOrchestrationDispatcher( + IOrchestrationService orchestrationService, + INameVersionObjectManager objectManager, + DispatchMiddlewarePipeline dispatchPipeline, + LogHelper logHelper, + ErrorPropagationMode errorPropagationMode, + VersioningSettings versioningSettings, + IExceptionPropertiesProvider? exceptionPropertiesProvider) { this.objectManager = objectManager ?? throw new ArgumentNullException(nameof(objectManager)); this.orchestrationService = orchestrationService ?? throw new ArgumentNullException(nameof(orchestrationService)); @@ -67,6 +90,7 @@ internal TaskOrchestrationDispatcher( this.entityBackendProperties = this.entityOrchestrationService?.EntityBackendProperties; this.entityParameters = TaskOrchestrationEntityParameters.FromEntityBackendProperties(this.entityBackendProperties); this.versioningSettings = versioningSettings; + this.exceptionPropertiesProvider = exceptionPropertiesProvider; this.dispatcher = new WorkItemDispatcher( "TaskOrchestrationDispatcher", @@ -763,7 +787,8 @@ await this.dispatchPipeline.RunAsync(dispatchContext, _ => taskOrchestration, this.orchestrationService.EventBehaviourForContinueAsNew, this.entityParameters, - this.errorPropagationMode); + this.errorPropagationMode, + this.exceptionPropertiesProvider); OrchestratorExecutionResult resultFromOrchestrator = executor.Execute(); dispatchContext.SetProperty(resultFromOrchestrator); diff --git a/src/DurableTask.Core/TaskOrchestrationExecutor.cs b/src/DurableTask.Core/TaskOrchestrationExecutor.cs index c5e100a2f..01b2294bd 100644 --- a/src/DurableTask.Core/TaskOrchestrationExecutor.cs +++ b/src/DurableTask.Core/TaskOrchestrationExecutor.cs @@ -35,6 +35,7 @@ public class TaskOrchestrationExecutor readonly OrchestrationRuntimeState orchestrationRuntimeState; readonly TaskOrchestration taskOrchestration; readonly bool skipCarryOverEvents; + readonly IExceptionPropertiesProvider? exceptionPropertiesProvider; Task? result; /// @@ -51,16 +52,8 @@ public TaskOrchestrationExecutor( BehaviorOnContinueAsNew eventBehaviourForContinueAsNew, TaskOrchestrationEntityParameters? entityParameters, ErrorPropagationMode errorPropagationMode = ErrorPropagationMode.SerializeExceptions) + : this(orchestrationRuntimeState, taskOrchestration, eventBehaviourForContinueAsNew, entityParameters, errorPropagationMode, null) { - this.decisionScheduler = new SynchronousTaskScheduler(); - this.context = new TaskOrchestrationContext( - orchestrationRuntimeState.OrchestrationInstance, - this.decisionScheduler, - entityParameters, - errorPropagationMode); - this.orchestrationRuntimeState = orchestrationRuntimeState; - this.taskOrchestration = taskOrchestration; - this.skipCarryOverEvents = eventBehaviourForContinueAsNew == BehaviorOnContinueAsNew.Ignore; } /// @@ -76,8 +69,38 @@ public TaskOrchestrationExecutor( TaskOrchestration taskOrchestration, BehaviorOnContinueAsNew eventBehaviourForContinueAsNew, ErrorPropagationMode errorPropagationMode = ErrorPropagationMode.SerializeExceptions) - : this(orchestrationRuntimeState, taskOrchestration, eventBehaviourForContinueAsNew, entityParameters: null, errorPropagationMode) + : this(orchestrationRuntimeState, taskOrchestration, eventBehaviourForContinueAsNew, entityParameters: null, errorPropagationMode, null) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// + /// + /// + /// + /// + /// + public TaskOrchestrationExecutor( + OrchestrationRuntimeState orchestrationRuntimeState, + TaskOrchestration taskOrchestration, + BehaviorOnContinueAsNew eventBehaviourForContinueAsNew, + TaskOrchestrationEntityParameters? entityParameters, + ErrorPropagationMode errorPropagationMode, + IExceptionPropertiesProvider? exceptionPropertiesProvider) { + this.decisionScheduler = new SynchronousTaskScheduler(); + this.context = new TaskOrchestrationContext( + orchestrationRuntimeState.OrchestrationInstance, + this.decisionScheduler, + entityParameters, + errorPropagationMode, + exceptionPropertiesProvider); + this.orchestrationRuntimeState = orchestrationRuntimeState; + this.taskOrchestration = taskOrchestration; + this.skipCarryOverEvents = eventBehaviourForContinueAsNew == BehaviorOnContinueAsNew.Ignore; + this.exceptionPropertiesProvider = exceptionPropertiesProvider; } internal bool IsCompleted => this.result != null && (this.result.IsCompleted || this.result.IsFaulted); diff --git a/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs b/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs index c7f39e436..0e7d8fb82 100644 --- a/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs +++ b/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs @@ -266,13 +266,12 @@ protected override Task ExecuteAsync(TaskContext context, string input) [TestMethod] public async Task ExceptionPropertiesProvider_ExtractsCustomProperties() { - // Set up a provider that extracts custom properties - var originalProvider = ExceptionPropertiesProviderRegistry.Provider; + // Set up a provider that extracts custom properties using the new TaskHubWorker property + this.worker.ExceptionPropertiesProvider = new TestExceptionPropertiesProvider(); + this.worker.ErrorPropagationMode = ErrorPropagationMode.UseFailureDetails; + try { - ExceptionPropertiesProviderRegistry.Provider = new TestExceptionPropertiesProvider(); - - this.worker.ErrorPropagationMode = ErrorPropagationMode.UseFailureDetails; await this.worker .AddTaskOrchestrations(typeof(ThrowCustomExceptionOrchestration)) .AddTaskActivities(typeof(ThrowCustomBusinessExceptionActivity)) @@ -294,7 +293,6 @@ await this.worker } finally { - ExceptionPropertiesProviderRegistry.Provider = originalProvider; await this.worker.StopAsync(); } } From 4c59e20e6b1b567414720305be0aef8ba175152e Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Mon, 29 Sep 2025 18:28:06 -0700 Subject: [PATCH 04/10] udpate --- .../ExceptionPropertiesProviderRegistry.cs | 30 ------------------- src/DurableTask.Core/OrchestrationContext.cs | 2 +- .../TaskActivityDispatcher.cs | 2 +- src/DurableTask.Core/TaskContext.cs | 2 +- src/DurableTask.Core/TaskHubWorker.cs | 5 ---- 5 files changed, 3 insertions(+), 38 deletions(-) delete mode 100644 src/DurableTask.Core/ExceptionPropertiesProviderRegistry.cs diff --git a/src/DurableTask.Core/ExceptionPropertiesProviderRegistry.cs b/src/DurableTask.Core/ExceptionPropertiesProviderRegistry.cs deleted file mode 100644 index 709555e34..000000000 --- a/src/DurableTask.Core/ExceptionPropertiesProviderRegistry.cs +++ /dev/null @@ -1,30 +0,0 @@ -// ---------------------------------------------------------------------------------- -// Copyright Microsoft Corporation -// 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. -// ---------------------------------------------------------------------------------- -#nullable enable -namespace DurableTask.Core -{ - /// - /// Registry for the global exception properties provider. - /// This registry is intended for use by the durabletask-dotnet layer to set a provider - /// that will be used when creating FailureDetails from exceptions. - /// - public static class ExceptionPropertiesProviderRegistry - { - /// - /// Gets or sets the global exception properties provider. - /// This will be set by the durabletask-dotnet layer. - /// - public static IExceptionPropertiesProvider? Provider { get; set; } - } -} - diff --git a/src/DurableTask.Core/OrchestrationContext.cs b/src/DurableTask.Core/OrchestrationContext.cs index 395f343ae..642fa0c4d 100644 --- a/src/DurableTask.Core/OrchestrationContext.cs +++ b/src/DurableTask.Core/OrchestrationContext.cs @@ -74,7 +74,7 @@ public abstract class OrchestrationContext internal ErrorPropagationMode ErrorPropagationMode { get; set; } /// - /// + /// Gets or sets the exception properties provider that extracts custom properties from exceptions /// internal IExceptionPropertiesProvider ExceptionPropertiesProvider { get;set; } diff --git a/src/DurableTask.Core/TaskActivityDispatcher.cs b/src/DurableTask.Core/TaskActivityDispatcher.cs index 6f50dfcd1..bd6a882ce 100644 --- a/src/DurableTask.Core/TaskActivityDispatcher.cs +++ b/src/DurableTask.Core/TaskActivityDispatcher.cs @@ -195,7 +195,7 @@ async Task OnProcessWorkItemAsync(TaskActivityWorkItem workItem) ActivityExecutionResult? result; try { - await this.dispatchPipeline.RunAsync(dispatchContext, async _ => + await this.dispatchPipeline.RunAsync(dispatchContext, async _ => { if (taskActivity == null) { diff --git a/src/DurableTask.Core/TaskContext.cs b/src/DurableTask.Core/TaskContext.cs index 06ef51f92..9a19c7497 100644 --- a/src/DurableTask.Core/TaskContext.cs +++ b/src/DurableTask.Core/TaskContext.cs @@ -65,4 +65,4 @@ public TaskContext(OrchestrationInstance orchestrationInstance, string name, str internal IExceptionPropertiesProvider? ExceptionPropertiesProvider { get; set; } } -} \ No newline at end of file +} diff --git a/src/DurableTask.Core/TaskHubWorker.cs b/src/DurableTask.Core/TaskHubWorker.cs index 96a9f5015..65dfbb47e 100644 --- a/src/DurableTask.Core/TaskHubWorker.cs +++ b/src/DurableTask.Core/TaskHubWorker.cs @@ -251,11 +251,6 @@ public TaskHubWorker( /// Gets or sets the exception properties provider that extracts custom properties from exceptions /// when creating FailureDetails objects. /// - /// - /// - /// This property must be set before the worker is started. Otherwise it will have no effect. - /// - /// public IExceptionPropertiesProvider ExceptionPropertiesProvider { get; set; } /// From cc4e4fdd44be476b9239170cf52462b0db74b62c Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Mon, 29 Sep 2025 18:44:54 -0700 Subject: [PATCH 05/10] add comments --- .../ExceptionHandlingIntegrationTests.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs b/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs index 0e7d8fb82..209ceead7 100644 --- a/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs +++ b/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs @@ -264,12 +264,13 @@ protected override Task ExecuteAsync(TaskContext context, string input) } [TestMethod] + // Test that when a provider is set, properties are extracted and stored in FailureDetails.Properties. public async Task ExceptionPropertiesProvider_ExtractsCustomProperties() { // Set up a provider that extracts custom properties using the new TaskHubWorker property this.worker.ExceptionPropertiesProvider = new TestExceptionPropertiesProvider(); this.worker.ErrorPropagationMode = ErrorPropagationMode.UseFailureDetails; - + try { await this.worker @@ -284,7 +285,7 @@ await this.worker Assert.AreEqual(OrchestrationStatus.Failed, result.OrchestrationStatus); Assert.IsNotNull(result.FailureDetails); Assert.IsNotNull(result.FailureDetails.Properties); - + // Check the properties match the exception. Assert.AreEqual("CustomBusinessException", result.FailureDetails.Properties["ExceptionTypeName"]); Assert.AreEqual("user123", result.FailureDetails.Properties["UserId"]); From 25bc44b8a6f78bdb9fc760a8f69a16c7128a87db Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Mon, 29 Sep 2025 19:11:11 -0700 Subject: [PATCH 06/10] add special case when taskcontext is null --- src/DurableTask.Core/TaskActivity.cs | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/DurableTask.Core/TaskActivity.cs b/src/DurableTask.Core/TaskActivity.cs index baa01fe92..50f106af2 100644 --- a/src/DurableTask.Core/TaskActivity.cs +++ b/src/DurableTask.Core/TaskActivity.cs @@ -142,7 +142,15 @@ public override async Task RunAsync(TaskContext context, string input) } else { - failureDetails = new FailureDetails(e, context.ExceptionPropertiesProvider); + if(context != null) + { + failureDetails = new FailureDetails(e, context.ExceptionPropertiesProvider); + } + else + { + // Handle case for TaskContext is null. + failureDetails = new FailureDetails(e); + } } throw new TaskFailureException(e.Message, e, details) From a0af07cd72cb5406089f29ae433a9b710606acd9 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Tue, 30 Sep 2025 09:46:54 -0700 Subject: [PATCH 07/10] update comment --- .../DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs b/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs index 209ceead7..571885a31 100644 --- a/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs +++ b/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs @@ -387,7 +387,7 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont } } - // Test provider similar to the one shown in the user's example + // Set a custom exception provider. class TestExceptionPropertiesProvider : IExceptionPropertiesProvider { public IDictionary? GetExceptionProperties(Exception exception) From 431dbace28c0c98abfbbeb3c7f95dae12c6e7e1f Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Wed, 1 Oct 2025 09:11:57 -0700 Subject: [PATCH 08/10] update by comments --- .../ExceptionPropertiesProviderExtensions.cs | 46 ++++++++++++++ src/DurableTask.Core/FailureDetails.cs | 60 +++++++++---------- .../ReflectionBasedTaskActivity.cs | 3 +- src/DurableTask.Core/TaskActivity.cs | 7 ++- .../TaskActivityDispatcher.cs | 12 +--- src/DurableTask.Core/TaskEntityDispatcher.cs | 10 ---- src/DurableTask.Core/TaskOrchestration.cs | 3 +- .../TaskOrchestrationContext.cs | 15 +---- .../TaskOrchestrationDispatcher.cs | 11 ---- .../TestTaskEntityDispatcher.cs | 2 +- 10 files changed, 86 insertions(+), 83 deletions(-) create mode 100644 src/DurableTask.Core/ExceptionPropertiesProviderExtensions.cs diff --git a/src/DurableTask.Core/ExceptionPropertiesProviderExtensions.cs b/src/DurableTask.Core/ExceptionPropertiesProviderExtensions.cs new file mode 100644 index 000000000..ddac543f7 --- /dev/null +++ b/src/DurableTask.Core/ExceptionPropertiesProviderExtensions.cs @@ -0,0 +1,46 @@ +// ---------------------------------------------------------------------------------- +// Copyright Microsoft Corporation +// 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. +// ---------------------------------------------------------------------------------- +#nullable enable +namespace DurableTask.Core +{ + using System; + using System.Collections.Generic; + using DurableTask.Core.Exceptions; + + /// + /// Extension methods for . + /// + internal static class ExceptionPropertiesProviderExtensions + { + /// + /// Extracts properties for the specified rules with the provider. + /// + public static IDictionary? ExtractProperties(this IExceptionPropertiesProvider? provider, Exception exception) + { + if (exception is OrchestrationException orchestrationException && + orchestrationException.FailureDetails?.Properties != null) + { + return orchestrationException.FailureDetails.Properties; + } + + if (provider == null) + { + return null; + } + + return provider.GetExceptionProperties(exception); + } + } +} + + diff --git a/src/DurableTask.Core/FailureDetails.cs b/src/DurableTask.Core/FailureDetails.cs index baafd75fb..35c596e12 100644 --- a/src/DurableTask.Core/FailureDetails.cs +++ b/src/DurableTask.Core/FailureDetails.cs @@ -38,14 +38,29 @@ public class FailureDetails : IEquatable /// The exception stack trace. /// The inner cause of the failure. /// Whether the failure is non-retriable. + /// Additional properties associated with the failure. [JsonConstructor] - public FailureDetails(string errorType, string errorMessage, string? stackTrace, FailureDetails? innerFailure, bool isNonRetriable) + public FailureDetails(string errorType, string errorMessage, string? stackTrace, FailureDetails? innerFailure, bool isNonRetriable, IDictionary? properties = null) { this.ErrorType = errorType; this.ErrorMessage = errorMessage; this.StackTrace = stackTrace; this.InnerFailure = innerFailure; this.IsNonRetriable = isNonRetriable; + this.Properties = properties; + } + + /// + /// Initializes a new instance of the class. + /// + /// The name of the error, which is expected to the the namespace-qualified name of the exception type. + /// The message associated with the error, which is expected to be the exception's property. + /// The exception stack trace. + /// The inner cause of the failure. + /// Whether the failure is non-retriable. + public FailureDetails(string errorType, string errorMessage, string? stackTrace, FailureDetails? innerFailure, bool isNonRetriable) + : this(errorType, errorMessage, stackTrace, innerFailure, isNonRetriable, properties:null) + { } /// @@ -54,7 +69,7 @@ public FailureDetails(string errorType, string errorMessage, string? stackTrace, /// The exception used to generate the failure details. /// The inner cause of the failure. public FailureDetails(Exception e, FailureDetails innerFailure) - : this(e, innerFailure, null) + : this(e, innerFailure, properties: null) { } @@ -63,7 +78,7 @@ public FailureDetails(Exception e, FailureDetails innerFailure) /// /// The exception used to generate the failure details. public FailureDetails(Exception e) - : this(e, (IExceptionPropertiesProvider?)null) + : this(e, properties: null) { } @@ -71,11 +86,10 @@ public FailureDetails(Exception e) /// Initializes a new instance of the class from an exception object. /// /// The exception used to generate the failure details. - /// The provider to extract custom properties from the exception. - public FailureDetails(Exception e, IExceptionPropertiesProvider? exceptionPropertiesProvider) - : this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, FromException(e.InnerException, exceptionPropertiesProvider), false) + /// The exception properties to include in failure details. + public FailureDetails(Exception e, IDictionary? properties) + : this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, FromException(e.InnerException), false, properties) { - this.Properties = GetExceptionProperties(e, exceptionPropertiesProvider); } /// @@ -83,11 +97,10 @@ public FailureDetails(Exception e, IExceptionPropertiesProvider? exceptionProper /// /// The exception used to generate the failure details. /// The inner cause of the failure. - /// The provider to extract custom properties from the exception. - public FailureDetails(Exception e, FailureDetails innerFailure, IExceptionPropertiesProvider? exceptionPropertiesProvider) - : this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, innerFailure, false) + /// The exception properties to include in failure details. + public FailureDetails(Exception e, FailureDetails innerFailure, IDictionary? properties) + : this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, innerFailure, false, properties) { - this.Properties = GetExceptionProperties(e, exceptionPropertiesProvider); } /// @@ -243,31 +256,12 @@ static string GetErrorMessage(Exception e) static FailureDetails? FromException(Exception? e) { - return FromException(e, null); - } - - static FailureDetails? FromException(Exception? e, IExceptionPropertiesProvider? provider) - { - return e == null ? null : new FailureDetails(e, provider); + return FromException(e, properties : null); } - static IDictionary? GetExceptionProperties(Exception exception, IExceptionPropertiesProvider? provider) + static FailureDetails? FromException(Exception? e, IDictionary? properties) { - // If this is a TaskFailedException that already has FailureDetails with properties, - // use those properties instead of asking the provider - if (exception is OrchestrationException orchestrationException && - orchestrationException.FailureDetails?.Properties != null) - { - return orchestrationException.FailureDetails.Properties; - } - - if (provider == null) - { - return null; - } - - // If there is a provider provided, then extract exception properties with the provider. - return provider.GetExceptionProperties(exception); + return e == null ? null : new FailureDetails(e, properties : properties); } } diff --git a/src/DurableTask.Core/ReflectionBasedTaskActivity.cs b/src/DurableTask.Core/ReflectionBasedTaskActivity.cs index f9784d0cd..0ecd717b4 100644 --- a/src/DurableTask.Core/ReflectionBasedTaskActivity.cs +++ b/src/DurableTask.Core/ReflectionBasedTaskActivity.cs @@ -140,7 +140,8 @@ public override async Task RunAsync(TaskContext context, string input) } else { - failureDetails = new FailureDetails(exception, context.ExceptionPropertiesProvider); + var props = context.ExceptionPropertiesProvider.ExtractProperties(exception); + failureDetails = new FailureDetails(exception, props); } throw new TaskFailureException(exception.Message, exception, details) diff --git a/src/DurableTask.Core/TaskActivity.cs b/src/DurableTask.Core/TaskActivity.cs index 50f106af2..f872c9bda 100644 --- a/src/DurableTask.Core/TaskActivity.cs +++ b/src/DurableTask.Core/TaskActivity.cs @@ -13,12 +13,12 @@ namespace DurableTask.Core { - using System; - using System.Threading.Tasks; using DurableTask.Core.Common; using DurableTask.Core.Exceptions; using DurableTask.Core.Serializing; using Newtonsoft.Json.Linq; + using System; + using System.Threading.Tasks; /// /// Base class for TaskActivity. @@ -144,7 +144,8 @@ public override async Task RunAsync(TaskContext context, string input) { if(context != null) { - failureDetails = new FailureDetails(e, context.ExceptionPropertiesProvider); + var props = context.ExceptionPropertiesProvider.ExtractProperties(e); + failureDetails = new FailureDetails(e, props); } else { diff --git a/src/DurableTask.Core/TaskActivityDispatcher.cs b/src/DurableTask.Core/TaskActivityDispatcher.cs index bd6a882ce..6a0a4b45f 100644 --- a/src/DurableTask.Core/TaskActivityDispatcher.cs +++ b/src/DurableTask.Core/TaskActivityDispatcher.cs @@ -38,16 +38,6 @@ public sealed class TaskActivityDispatcher readonly ErrorPropagationMode errorPropagationMode; readonly IExceptionPropertiesProvider? exceptionPropertiesProvider; - internal TaskActivityDispatcher( - IOrchestrationService orchestrationService, - INameVersionObjectManager objectManager, - DispatchMiddlewarePipeline dispatchPipeline, - LogHelper logHelper, - ErrorPropagationMode errorPropagationMode) - : this(orchestrationService, objectManager, dispatchPipeline, logHelper, errorPropagationMode, null) - { - } - /// /// Initializes a new instance of the class with an exception properties provider. /// @@ -230,7 +220,7 @@ await this.dispatchPipeline.RunAsync(dispatchContext, async _ => string? details = this.IncludeDetails ? $"Unhandled exception while executing task: {e}" : null; - responseEvent = new TaskFailedEvent(-1, scheduledEvent.EventId, e.Message, details, new FailureDetails(e, this.exceptionPropertiesProvider)); + responseEvent = new TaskFailedEvent(-1, scheduledEvent.EventId, e.Message, details, new FailureDetails(e)); traceActivity?.SetStatus(ActivityStatusCode.Error, e.Message); diff --git a/src/DurableTask.Core/TaskEntityDispatcher.cs b/src/DurableTask.Core/TaskEntityDispatcher.cs index c88f683a7..4595aae2a 100644 --- a/src/DurableTask.Core/TaskEntityDispatcher.cs +++ b/src/DurableTask.Core/TaskEntityDispatcher.cs @@ -44,16 +44,6 @@ public class TaskEntityDispatcher readonly TaskOrchestrationDispatcher.NonBlockingCountdownLock concurrentSessionLock; readonly IExceptionPropertiesProvider exceptionPropertiesProvider; - internal TaskEntityDispatcher( - IOrchestrationService orchestrationService, - INameVersionObjectManager entityObjectManager, - DispatchMiddlewarePipeline entityDispatchPipeline, - LogHelper logHelper, - ErrorPropagationMode errorPropagationMode) - : this(orchestrationService, entityObjectManager, entityDispatchPipeline, logHelper, errorPropagationMode, null) - { - } - /// /// Initializes a new instance of the class with an exception properties provider. /// diff --git a/src/DurableTask.Core/TaskOrchestration.cs b/src/DurableTask.Core/TaskOrchestration.cs index d30ac1878..d449d3584 100644 --- a/src/DurableTask.Core/TaskOrchestration.cs +++ b/src/DurableTask.Core/TaskOrchestration.cs @@ -105,7 +105,8 @@ public override async Task Execute(OrchestrationContext context, string } else { - failureDetails = new FailureDetails(e, context.ExceptionPropertiesProvider); + var props = context.ExceptionPropertiesProvider.ExtractProperties(e); + failureDetails = new FailureDetails(e, props); } throw new OrchestrationFailureException(e.Message, details) diff --git a/src/DurableTask.Core/TaskOrchestrationContext.cs b/src/DurableTask.Core/TaskOrchestrationContext.cs index 67e5f275d..da645c6bb 100644 --- a/src/DurableTask.Core/TaskOrchestrationContext.cs +++ b/src/DurableTask.Core/TaskOrchestrationContext.cs @@ -52,17 +52,8 @@ public TaskOrchestrationContext( OrchestrationInstance orchestrationInstance, TaskScheduler taskScheduler, TaskOrchestrationEntityParameters entityParameters = null, - ErrorPropagationMode errorPropagationMode = ErrorPropagationMode.SerializeExceptions) - : this(orchestrationInstance, taskScheduler, entityParameters, errorPropagationMode, null) - { - } - - public TaskOrchestrationContext( - OrchestrationInstance orchestrationInstance, - TaskScheduler taskScheduler, - TaskOrchestrationEntityParameters entityParameters, - ErrorPropagationMode errorPropagationMode, - IExceptionPropertiesProvider exceptionPropertiesProvider) + ErrorPropagationMode errorPropagationMode = ErrorPropagationMode.SerializeExceptions, + IExceptionPropertiesProvider exceptionPropertiesProvider = null) { Utils.UnusedParameter(taskScheduler); @@ -696,7 +687,7 @@ public void FailOrchestration(Exception failure, OrchestrationRuntimeState runti { if (this.ErrorPropagationMode == ErrorPropagationMode.UseFailureDetails) { - failureDetails = new FailureDetails(failure, this.exceptionPropertiesProvider); + failureDetails = new FailureDetails(failure); } else { diff --git a/src/DurableTask.Core/TaskOrchestrationDispatcher.cs b/src/DurableTask.Core/TaskOrchestrationDispatcher.cs index 4e423e33f..c19e5bd80 100644 --- a/src/DurableTask.Core/TaskOrchestrationDispatcher.cs +++ b/src/DurableTask.Core/TaskOrchestrationDispatcher.cs @@ -51,17 +51,6 @@ public class TaskOrchestrationDispatcher readonly VersioningSettings? versioningSettings; readonly IExceptionPropertiesProvider? exceptionPropertiesProvider; - internal TaskOrchestrationDispatcher( - IOrchestrationService orchestrationService, - INameVersionObjectManager objectManager, - DispatchMiddlewarePipeline dispatchPipeline, - LogHelper logHelper, - ErrorPropagationMode errorPropagationMode, - VersioningSettings versioningSettings) - : this(orchestrationService, objectManager, dispatchPipeline, logHelper, errorPropagationMode, versioningSettings, null) - { - } - /// /// Initializes a new instance of the class with an exception properties provider. /// diff --git a/test/DurableTask.Core.Tests/TestTaskEntityDispatcher.cs b/test/DurableTask.Core.Tests/TestTaskEntityDispatcher.cs index 3e5032156..6128c0465 100644 --- a/test/DurableTask.Core.Tests/TestTaskEntityDispatcher.cs +++ b/test/DurableTask.Core.Tests/TestTaskEntityDispatcher.cs @@ -30,7 +30,7 @@ private TaskEntityDispatcher GetTaskEntityDispatcher() var logger = new LogHelper(loggerFactory?.CreateLogger("DurableTask.Core")); TaskEntityDispatcher dispatcher = new TaskEntityDispatcher( - service, entityManager, entityMiddleware, logger, ErrorPropagationMode.UseFailureDetails); + service, entityManager, entityMiddleware, logger, ErrorPropagationMode.UseFailureDetails, null); return dispatcher; } From 928037d96231bd484dd53600b05c8e604ddc2320 Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Thu, 2 Oct 2025 08:22:02 -0700 Subject: [PATCH 09/10] make two internal property public and also update typo --- .../ExceptionPropertiesProviderExtensions.cs | 4 ++-- src/DurableTask.Core/TaskContext.cs | 5 ++++- src/DurableTask.Core/TaskOrchestrationContext.cs | 3 +-- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/DurableTask.Core/ExceptionPropertiesProviderExtensions.cs b/src/DurableTask.Core/ExceptionPropertiesProviderExtensions.cs index ddac543f7..26d879eda 100644 --- a/src/DurableTask.Core/ExceptionPropertiesProviderExtensions.cs +++ b/src/DurableTask.Core/ExceptionPropertiesProviderExtensions.cs @@ -20,10 +20,10 @@ namespace DurableTask.Core /// /// Extension methods for . /// - internal static class ExceptionPropertiesProviderExtensions + public static class ExceptionPropertiesProviderExtensions { /// - /// Extracts properties for the specified rules with the provider. + /// Extracts properties of the exception specified at provider. /// public static IDictionary? ExtractProperties(this IExceptionPropertiesProvider? provider, Exception exception) { diff --git a/src/DurableTask.Core/TaskContext.cs b/src/DurableTask.Core/TaskContext.cs index 9a19c7497..4fe4322f0 100644 --- a/src/DurableTask.Core/TaskContext.cs +++ b/src/DurableTask.Core/TaskContext.cs @@ -63,6 +63,9 @@ public TaskContext(OrchestrationInstance orchestrationInstance, string name, str /// internal ErrorPropagationMode ErrorPropagationMode { get; set; } - internal IExceptionPropertiesProvider? ExceptionPropertiesProvider { get; set; } + /// + /// Gets or sets the properties of exceptions with the provider. + /// + public IExceptionPropertiesProvider? ExceptionPropertiesProvider { get; set; } } } diff --git a/src/DurableTask.Core/TaskOrchestrationContext.cs b/src/DurableTask.Core/TaskOrchestrationContext.cs index da645c6bb..87fc435b3 100644 --- a/src/DurableTask.Core/TaskOrchestrationContext.cs +++ b/src/DurableTask.Core/TaskOrchestrationContext.cs @@ -37,7 +37,6 @@ internal class TaskOrchestrationContext : OrchestrationContext private int idCounter; private readonly Queue eventsWhileSuspended; private readonly IDictionary suspendedActionsMap; - private readonly IExceptionPropertiesProvider exceptionPropertiesProvider; public bool IsSuspended { get; private set; } @@ -68,7 +67,7 @@ public TaskOrchestrationContext( ErrorPropagationMode = errorPropagationMode; this.eventsWhileSuspended = new Queue(); this.suspendedActionsMap = new SortedDictionary(); - this.exceptionPropertiesProvider = exceptionPropertiesProvider; + this.ExceptionPropertiesProvider = exceptionPropertiesProvider; } public IEnumerable OrchestratorActions => this.orchestratorActionsMap.Values; From aa95f7412fe890a06e88d8027ba117382311f54b Mon Sep 17 00:00:00 2001 From: "naiyuantian@microsoft.com" Date: Thu, 2 Oct 2025 10:14:52 -0700 Subject: [PATCH 10/10] make properties object nullable and add test case --- .../ExceptionPropertiesProviderExtensions.cs | 2 +- src/DurableTask.Core/FailureDetails.cs | 12 ++++----- .../IExceptionPropertiesProvider.cs | 2 +- .../ExceptionHandlingIntegrationTests.cs | 26 ++++++++++++++++--- 4 files changed, 30 insertions(+), 12 deletions(-) diff --git a/src/DurableTask.Core/ExceptionPropertiesProviderExtensions.cs b/src/DurableTask.Core/ExceptionPropertiesProviderExtensions.cs index 26d879eda..08b2d9692 100644 --- a/src/DurableTask.Core/ExceptionPropertiesProviderExtensions.cs +++ b/src/DurableTask.Core/ExceptionPropertiesProviderExtensions.cs @@ -25,7 +25,7 @@ public static class ExceptionPropertiesProviderExtensions /// /// Extracts properties of the exception specified at provider. /// - public static IDictionary? ExtractProperties(this IExceptionPropertiesProvider? provider, Exception exception) + public static IDictionary? ExtractProperties(this IExceptionPropertiesProvider? provider, Exception exception) { if (exception is OrchestrationException orchestrationException && orchestrationException.FailureDetails?.Properties != null) diff --git a/src/DurableTask.Core/FailureDetails.cs b/src/DurableTask.Core/FailureDetails.cs index 35c596e12..4e2abaaaf 100644 --- a/src/DurableTask.Core/FailureDetails.cs +++ b/src/DurableTask.Core/FailureDetails.cs @@ -40,7 +40,7 @@ public class FailureDetails : IEquatable /// Whether the failure is non-retriable. /// Additional properties associated with the failure. [JsonConstructor] - public FailureDetails(string errorType, string errorMessage, string? stackTrace, FailureDetails? innerFailure, bool isNonRetriable, IDictionary? properties = null) + public FailureDetails(string errorType, string errorMessage, string? stackTrace, FailureDetails? innerFailure, bool isNonRetriable, IDictionary? properties = null) { this.ErrorType = errorType; this.ErrorMessage = errorMessage; @@ -87,7 +87,7 @@ public FailureDetails(Exception e) /// /// The exception used to generate the failure details. /// The exception properties to include in failure details. - public FailureDetails(Exception e, IDictionary? properties) + public FailureDetails(Exception e, IDictionary? properties) : this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, FromException(e.InnerException), false, properties) { } @@ -98,7 +98,7 @@ public FailureDetails(Exception e, IDictionary? properties) /// The exception used to generate the failure details. /// The inner cause of the failure. /// The exception properties to include in failure details. - public FailureDetails(Exception e, FailureDetails innerFailure, IDictionary? properties) + public FailureDetails(Exception e, FailureDetails innerFailure, IDictionary? properties) : this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, innerFailure, false, properties) { } @@ -125,7 +125,7 @@ protected FailureDetails(SerializationInfo info, StreamingContext context) // Handle backward compatibility for Properties property - defaults to null try { - this.Properties = (IDictionary?)info.GetValue(nameof(this.Properties), typeof(IDictionary)); + this.Properties = (IDictionary?)info.GetValue(nameof(this.Properties), typeof(IDictionary)); } catch (SerializationException) { @@ -162,7 +162,7 @@ protected FailureDetails(SerializationInfo info, StreamingContext context) /// /// Gets additional properties associated with the failure. /// - public IDictionary? Properties { get; } + public IDictionary? Properties { get; } /// /// Gets a debug-friendly description of the failure information. @@ -259,7 +259,7 @@ static string GetErrorMessage(Exception e) return FromException(e, properties : null); } - static FailureDetails? FromException(Exception? e, IDictionary? properties) + static FailureDetails? FromException(Exception? e, IDictionary? properties) { return e == null ? null : new FailureDetails(e, properties : properties); } diff --git a/src/DurableTask.Core/IExceptionPropertiesProvider.cs b/src/DurableTask.Core/IExceptionPropertiesProvider.cs index 1ad2327ee..e40bc1c3e 100644 --- a/src/DurableTask.Core/IExceptionPropertiesProvider.cs +++ b/src/DurableTask.Core/IExceptionPropertiesProvider.cs @@ -28,6 +28,6 @@ public interface IExceptionPropertiesProvider /// /// The exception to extract properties from. /// A dictionary of custom properties to include in the FailureDetails, or null if no properties should be added. - IDictionary? GetExceptionProperties(Exception exception); + IDictionary? GetExceptionProperties(Exception exception); } } diff --git a/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs b/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs index 571885a31..a480279d5 100644 --- a/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs +++ b/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs @@ -291,6 +291,17 @@ await this.worker Assert.AreEqual("user123", result.FailureDetails.Properties["UserId"]); Assert.AreEqual("OrderProcessing", result.FailureDetails.Properties["BusinessContext"]); Assert.IsTrue(result.FailureDetails.Properties.ContainsKey("Timestamp")); + + // Check that null values are properly handled + Assert.IsTrue(result.FailureDetails.Properties.ContainsKey("TestNullObject"), "TestNullObject key should be present"); + Assert.IsNull(result.FailureDetails.Properties["TestNullObject"], "TestNullObject should be null"); + + Assert.IsTrue(result.FailureDetails.Properties.ContainsKey("DirectNullValue"), "DirectNullValue key should be present"); + Assert.IsNull(result.FailureDetails.Properties["DirectNullValue"], "DirectNullValue should be null"); + + // Verify non-null values still work + Assert.IsTrue(result.FailureDetails.Properties.ContainsKey("EmptyString"), "EmptyString key should be present"); + Assert.AreEqual(string.Empty, result.FailureDetails.Properties["EmptyString"], "EmptyString should be empty string, not null"); } finally { @@ -364,12 +375,14 @@ class CustomBusinessException : Exception { public string UserId { get; } public string BusinessContext { get; } + public string? TestNullObject { get; } public CustomBusinessException(string message, string userId, string businessContext) : base(message) { UserId = userId; BusinessContext = businessContext; + TestNullObject = null; // Explicitly set to null for testing } protected CustomBusinessException(SerializationInfo info, StreamingContext context) @@ -377,6 +390,7 @@ protected CustomBusinessException(SerializationInfo info, StreamingContext conte { UserId = info.GetString(nameof(UserId)) ?? string.Empty; BusinessContext = info.GetString(nameof(BusinessContext)) ?? string.Empty; + TestNullObject = info.GetString(nameof(TestNullObject)); // This will be null } public override void GetObjectData(SerializationInfo info, StreamingContext context) @@ -384,22 +398,26 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont base.GetObjectData(info, context); info.AddValue(nameof(UserId), UserId); info.AddValue(nameof(BusinessContext), BusinessContext); + info.AddValue(nameof(TestNullObject), TestNullObject); } } - // Set a custom exception provider. + // Test provider that includes null values in different ways class TestExceptionPropertiesProvider : IExceptionPropertiesProvider { - public IDictionary? GetExceptionProperties(Exception exception) + public IDictionary? GetExceptionProperties(Exception exception) { return exception switch { - CustomBusinessException businessEx => new Dictionary + CustomBusinessException businessEx => new Dictionary { ["ExceptionTypeName"] = nameof(CustomBusinessException), ["UserId"] = businessEx.UserId, ["BusinessContext"] = businessEx.BusinessContext, - ["Timestamp"] = DateTime.UtcNow + ["Timestamp"] = DateTime.UtcNow, + ["TestNullObject"] = businessEx.TestNullObject, // This comes from the exception property (null) + ["DirectNullValue"] = null, // This is directly set to null + ["EmptyString"] = string.Empty // Non-null value for comparison }, _ => null // No custom properties for other exceptions };