diff --git a/src/DurableTask.Core/ExceptionPropertiesProviderExtensions.cs b/src/DurableTask.Core/ExceptionPropertiesProviderExtensions.cs
new file mode 100644
index 000000000..08b2d9692
--- /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 .
+ ///
+ public static class ExceptionPropertiesProviderExtensions
+ {
+ ///
+ /// Extracts properties of the exception specified at 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 5dfd02ab9..4e2abaaaf 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.GetType().FullName, GetErrorMessage(e), e.StackTrace, innerFailure, false)
+ : this(e, innerFailure, properties: null)
{
}
@@ -63,7 +78,28 @@ 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, properties: null)
+ {
+ }
+
+ ///
+ /// Initializes a new instance of the class from an exception object.
+ ///
+ /// The exception used to generate the failure details.
+ /// 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)
+ {
+ }
+
+ ///
+ /// 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 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)
{
}
@@ -74,6 +110,7 @@ public FailureDetails()
{
this.ErrorType = "None";
this.ErrorMessage = string.Empty;
+ this.Properties = null;
}
///
@@ -85,6 +122,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 +159,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.
///
@@ -204,7 +256,13 @@ static string GetErrorMessage(Exception e)
static FailureDetails? FromException(Exception? e)
{
- return e == null ? null : new FailureDetails(e);
+ return FromException(e, properties : null);
}
+
+ 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
new file mode 100644
index 000000000..e40bc1c3e
--- /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/src/DurableTask.Core/OrchestrationContext.cs b/src/DurableTask.Core/OrchestrationContext.cs
index 97c6f6f6d..642fa0c4d 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; }
+ ///
+ /// Gets or sets the exception properties provider that extracts custom properties from exceptions
+ ///
+ 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..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);
+ 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 b05f020eb..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.
@@ -142,7 +142,16 @@ public override async Task RunAsync(TaskContext context, string input)
}
else
{
- failureDetails = new FailureDetails(e);
+ if(context != null)
+ {
+ var props = context.ExceptionPropertiesProvider.ExtractProperties(e);
+ failureDetails = new FailureDetails(e, props);
+ }
+ else
+ {
+ // Handle case for TaskContext is null.
+ failureDetails = new FailureDetails(e);
+ }
}
throw new TaskFailureException(e.Message, e, details)
diff --git a/src/DurableTask.Core/TaskActivityDispatcher.cs b/src/DurableTask.Core/TaskActivityDispatcher.cs
index bdafffdd5..6a0a4b45f 100644
--- a/src/DurableTask.Core/TaskActivityDispatcher.cs
+++ b/src/DurableTask.Core/TaskActivityDispatcher.cs
@@ -36,19 +36,31 @@ public sealed class TaskActivityDispatcher
readonly DispatchMiddlewarePipeline dispatchPipeline;
readonly LogHelper logHelper;
readonly ErrorPropagationMode errorPropagationMode;
+ readonly IExceptionPropertiesProvider? exceptionPropertiesProvider;
+ ///
+ /// 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)
+ 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",
@@ -190,6 +202,7 @@ await this.dispatchPipeline.RunAsync(dispatchContext, async _ =>
scheduledEvent.Version,
scheduledEvent.EventId);
context.ErrorPropagationMode = this.errorPropagationMode;
+ context.ExceptionPropertiesProvider = this.exceptionPropertiesProvider;
HistoryEvent? responseEvent;
diff --git a/src/DurableTask.Core/TaskContext.cs b/src/DurableTask.Core/TaskContext.cs
index d8152976c..4fe4322f0 100644
--- a/src/DurableTask.Core/TaskContext.cs
+++ b/src/DurableTask.Core/TaskContext.cs
@@ -62,5 +62,10 @@ public TaskContext(OrchestrationInstance orchestrationInstance, string name, str
/// Gets or sets a value indicating how to propagate unhandled exception metadata.
///
internal ErrorPropagationMode ErrorPropagationMode { get; set; }
+
+ ///
+ /// Gets or sets the properties of exceptions with the provider.
+ ///
+ public 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..4595aae2a 100644
--- a/src/DurableTask.Core/TaskEntityDispatcher.cs
+++ b/src/DurableTask.Core/TaskEntityDispatcher.cs
@@ -42,19 +42,31 @@ public class TaskEntityDispatcher
readonly LogHelper logHelper;
readonly ErrorPropagationMode errorPropagationMode;
readonly TaskOrchestrationDispatcher.NonBlockingCountdownLock concurrentSessionLock;
+ readonly IExceptionPropertiesProvider exceptionPropertiesProvider;
+ ///
+ /// 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)
+ 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..65dfbb47e 100644
--- a/src/DurableTask.Core/TaskHubWorker.cs
+++ b/src/DurableTask.Core/TaskHubWorker.cs
@@ -247,6 +247,12 @@ public TaskHubWorker(
///
public ErrorPropagationMode ErrorPropagationMode { get; set; }
+ ///
+ /// Gets or sets the exception properties provider that extracts custom properties from exceptions
+ /// when creating FailureDetails objects.
+ ///
+ public IExceptionPropertiesProvider ExceptionPropertiesProvider { get; set; }
+
///
/// Adds a middleware delegate to the orchestration dispatch pipeline.
///
@@ -296,13 +302,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 +319,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..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);
+ 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 b12cb1b08..87fc435b3 100644
--- a/src/DurableTask.Core/TaskOrchestrationContext.cs
+++ b/src/DurableTask.Core/TaskOrchestrationContext.cs
@@ -51,7 +51,8 @@ public TaskOrchestrationContext(
OrchestrationInstance orchestrationInstance,
TaskScheduler taskScheduler,
TaskOrchestrationEntityParameters entityParameters = null,
- ErrorPropagationMode errorPropagationMode = ErrorPropagationMode.SerializeExceptions)
+ ErrorPropagationMode errorPropagationMode = ErrorPropagationMode.SerializeExceptions,
+ IExceptionPropertiesProvider exceptionPropertiesProvider = null)
{
Utils.UnusedParameter(taskScheduler);
@@ -66,6 +67,7 @@ public TaskOrchestrationContext(
ErrorPropagationMode = errorPropagationMode;
this.eventsWhileSuspended = new Queue();
this.suspendedActionsMap = new SortedDictionary();
+ this.ExceptionPropertiesProvider = exceptionPropertiesProvider;
}
public IEnumerable OrchestratorActions => this.orchestratorActionsMap.Values;
diff --git a/src/DurableTask.Core/TaskOrchestrationDispatcher.cs b/src/DurableTask.Core/TaskOrchestrationDispatcher.cs
index 44adbbbd7..c19e5bd80 100644
--- a/src/DurableTask.Core/TaskOrchestrationDispatcher.cs
+++ b/src/DurableTask.Core/TaskOrchestrationDispatcher.cs
@@ -49,14 +49,26 @@ public class TaskOrchestrationDispatcher
readonly EntityBackendProperties? entityBackendProperties;
readonly TaskOrchestrationEntityParameters? entityParameters;
readonly VersioningSettings? versioningSettings;
+ readonly IExceptionPropertiesProvider? exceptionPropertiesProvider;
+ ///
+ /// 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)
+ VersioningSettings versioningSettings,
+ IExceptionPropertiesProvider? exceptionPropertiesProvider)
{
this.objectManager = objectManager ?? throw new ArgumentNullException(nameof(objectManager));
this.orchestrationService = orchestrationService ?? throw new ArgumentNullException(nameof(orchestrationService));
@@ -67,6 +79,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",
@@ -757,7 +770,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 e3dc6fc49..540851e50 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;
}
///
diff --git a/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs b/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs
index 7fb2ef2f5..a480279d5 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,167 @@ 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
+ .AddTaskOrchestrations(typeof(ThrowCustomExceptionOrchestration))
+ .AddTaskActivities(typeof(ThrowCustomBusinessExceptionActivity))
+ .StartAsync();
+
+ var instance = await this.client.CreateOrchestrationInstanceAsync(typeof(ThrowCustomExceptionOrchestration), "test-input");
+ var result = await this.client.WaitForOrchestrationAsync(instance, DefaultTimeout);
+
+ // Check that custom properties were extracted
+ 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"]);
+ 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
+ {
+ 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()
+ {
+ try
+ {
+ this.worker.ErrorPropagationMode = ErrorPropagationMode.UseFailureDetails;
+ await this.worker
+ .AddTaskOrchestrations(typeof(ThrowInvalidOperationExceptionOrchestration))
+ .AddTaskActivities(typeof(ThrowInvalidOperationExceptionActivity))
+ .StartAsync();
+
+ var instance = await this.client.CreateOrchestrationInstanceAsync(typeof(ThrowInvalidOperationExceptionOrchestration), "test-input");
+ var result = await this.client.WaitForOrchestrationAsync(instance, DefaultTimeout);
+
+ // Properties should be null when no provider
+ Assert.AreEqual(OrchestrationStatus.Failed, result.OrchestrationStatus);
+ Assert.IsNotNull(result.FailureDetails);
+ Assert.IsNull(result.FailureDetails.Properties);
+ }
+ finally
+ {
+ 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 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)
+ : base(info, context)
+ {
+ 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)
+ {
+ base.GetObjectData(info, context);
+ info.AddValue(nameof(UserId), UserId);
+ info.AddValue(nameof(BusinessContext), BusinessContext);
+ info.AddValue(nameof(TestNullObject), TestNullObject);
+ }
+ }
+
+ // Test provider that includes null values in different ways
+ 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,
+ ["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
+ };
+ }
+ }
+
[Serializable]
class CustomException : Exception
{
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;
}