Skip to content
46 changes: 46 additions & 0 deletions src/DurableTask.Core/ExceptionPropertiesProviderExtensions.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// Extension methods for <see cref="IExceptionPropertiesProvider"/>.
/// </summary>
public static class ExceptionPropertiesProviderExtensions
{
/// <summary>
/// Extracts properties of the exception specified at provider.
/// </summary>
public static IDictionary<string, object?>? 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);
}
}
}


66 changes: 62 additions & 4 deletions src/DurableTask.Core/FailureDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,14 +38,29 @@ public class FailureDetails : IEquatable<FailureDetails>
/// <param name="stackTrace">The exception stack trace.</param>
/// <param name="innerFailure">The inner cause of the failure.</param>
/// <param name="isNonRetriable">Whether the failure is non-retriable.</param>
/// <param name="properties">Additional properties associated with the failure.</param>
[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<string, object?>? properties = null)
{
this.ErrorType = errorType;
this.ErrorMessage = errorMessage;
this.StackTrace = stackTrace;
this.InnerFailure = innerFailure;
this.IsNonRetriable = isNonRetriable;
this.Properties = properties;
}

/// <summary>
/// Initializes a new instance of the <see cref="FailureDetails"/> class.
/// </summary>
/// <param name="errorType">The name of the error, which is expected to the the namespace-qualified name of the exception type.</param>
/// <param name="errorMessage">The message associated with the error, which is expected to be the exception's <see cref="Exception.Message"/> property.</param>
/// <param name="stackTrace">The exception stack trace.</param>
/// <param name="innerFailure">The inner cause of the failure.</param>
/// <param name="isNonRetriable">Whether the failure is non-retriable.</param>
public FailureDetails(string errorType, string errorMessage, string? stackTrace, FailureDetails? innerFailure, bool isNonRetriable)
: this(errorType, errorMessage, stackTrace, innerFailure, isNonRetriable, properties:null)
{
}

/// <summary>
Expand All @@ -54,7 +69,7 @@ public FailureDetails(string errorType, string errorMessage, string? stackTrace,
/// <param name="e">The exception used to generate the failure details.</param>
/// <param name="innerFailure">The inner cause of the failure.</param>
public FailureDetails(Exception e, FailureDetails innerFailure)
: this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, innerFailure, false)
: this(e, innerFailure, properties: null)
{
}

Expand All @@ -63,7 +78,28 @@ public FailureDetails(Exception e, FailureDetails innerFailure)
/// </summary>
/// <param name="e">The exception used to generate the failure details.</param>
public FailureDetails(Exception e)
: this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, FromException(e.InnerException), false)
: this(e, properties: null)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="FailureDetails"/> class from an exception object.
/// </summary>
/// <param name="e">The exception used to generate the failure details.</param>
/// <param name="properties">The exception properties to include in failure details.</param>
public FailureDetails(Exception e, IDictionary<string, object?>? properties)
: this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, FromException(e.InnerException), false, properties)
{
}

/// <summary>
/// Initializes a new instance of the <see cref="FailureDetails"/> class from an exception object.
/// </summary>
/// <param name="e">The exception used to generate the failure details.</param>
/// <param name="innerFailure">The inner cause of the failure.</param>
/// <param name="properties">The exception properties to include in failure details.</param>
public FailureDetails(Exception e, FailureDetails innerFailure, IDictionary<string, object?>? properties)
: this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, innerFailure, false, properties)
{
}

Expand All @@ -74,6 +110,7 @@ public FailureDetails()
{
this.ErrorType = "None";
this.ErrorMessage = string.Empty;
this.Properties = null;
}

/// <summary>
Expand All @@ -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<string, object?>?)info.GetValue(nameof(this.Properties), typeof(IDictionary<string, object?>));
}
catch (SerializationException)
{
// Default to null for backward compatibility
this.Properties = null;
}
}

/// <summary>
Expand Down Expand Up @@ -112,6 +159,11 @@ protected FailureDetails(SerializationInfo info, StreamingContext context)
/// </summary>
public bool IsNonRetriable { get; }

/// <summary>
/// Gets additional properties associated with the failure.
/// </summary>
public IDictionary<string, object?>? Properties { get; }

/// <summary>
/// Gets a debug-friendly description of the failure information.
/// </summary>
Expand Down Expand Up @@ -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<string, object?>? properties)
{
return e == null ? null : new FailureDetails(e, properties : properties);
}

}
}
33 changes: 33 additions & 0 deletions src/DurableTask.Core/IExceptionPropertiesProvider.cs
Original file line number Diff line number Diff line change
@@ -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;

/// <summary>
/// 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.
/// </summary>
public interface IExceptionPropertiesProvider
{
/// <summary>
/// Extracts custom properties from an exception.
/// </summary>
/// <param name="exception">The exception to extract properties from.</param>
/// <returns>A dictionary of custom properties to include in the FailureDetails, or null if no properties should be added.</returns>
IDictionary<string, object?>? GetExceptionProperties(Exception exception);
}
}
5 changes: 5 additions & 0 deletions src/DurableTask.Core/OrchestrationContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,11 @@ public abstract class OrchestrationContext
/// </summary>
internal ErrorPropagationMode ErrorPropagationMode { get; set; }

/// <summary>
/// Gets or sets the exception properties provider that extracts custom properties from exceptions
/// </summary>
internal IExceptionPropertiesProvider ExceptionPropertiesProvider { get;set; }

/// <summary>
/// Information about backend entity support, or null if the configured backend does not support entities.
/// </summary>
Expand Down
3 changes: 2 additions & 1 deletion src/DurableTask.Core/ReflectionBasedTaskActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -140,7 +140,8 @@ public override async Task<string> 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)
Expand Down
15 changes: 12 additions & 3 deletions src/DurableTask.Core/TaskActivity.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;

/// <summary>
/// Base class for TaskActivity.
Expand Down Expand Up @@ -142,7 +142,16 @@ public override async Task<string> 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)
Expand Down
15 changes: 14 additions & 1 deletion src/DurableTask.Core/TaskActivityDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -36,19 +36,31 @@ public sealed class TaskActivityDispatcher
readonly DispatchMiddlewarePipeline dispatchPipeline;
readonly LogHelper logHelper;
readonly ErrorPropagationMode errorPropagationMode;
readonly IExceptionPropertiesProvider? exceptionPropertiesProvider;

/// <summary>
/// Initializes a new instance of the <see cref="TaskActivityDispatcher"/> class with an exception properties provider.
/// </summary>
/// <param name="orchestrationService">The orchestration service implementation</param>
/// <param name="objectManager">The object manager for activities</param>
/// <param name="dispatchPipeline">The dispatch middleware pipeline</param>
/// <param name="logHelper">The log helper</param>
/// <param name="errorPropagationMode">The error propagation mode</param>
/// <param name="exceptionPropertiesProvider">The exception properties provider for extracting custom properties from exceptions</param>
internal TaskActivityDispatcher(
IOrchestrationService orchestrationService,
INameVersionObjectManager<TaskActivity> 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<TaskActivityWorkItem>(
"TaskActivityDispatcher",
Expand Down Expand Up @@ -190,6 +202,7 @@ await this.dispatchPipeline.RunAsync(dispatchContext, async _ =>
scheduledEvent.Version,
scheduledEvent.EventId);
context.ErrorPropagationMode = this.errorPropagationMode;
context.ExceptionPropertiesProvider = this.exceptionPropertiesProvider;

HistoryEvent? responseEvent;

Expand Down
7 changes: 6 additions & 1 deletion src/DurableTask.Core/TaskContext.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,10 @@ public TaskContext(OrchestrationInstance orchestrationInstance, string name, str
/// Gets or sets a value indicating how to propagate unhandled exception metadata.
/// </summary>
internal ErrorPropagationMode ErrorPropagationMode { get; set; }

/// <summary>
/// Gets or sets the properties of exceptions with the provider.
/// </summary>
public IExceptionPropertiesProvider? ExceptionPropertiesProvider { get; set; }
}
}
}
14 changes: 13 additions & 1 deletion src/DurableTask.Core/TaskEntityDispatcher.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,19 +42,31 @@ public class TaskEntityDispatcher
readonly LogHelper logHelper;
readonly ErrorPropagationMode errorPropagationMode;
readonly TaskOrchestrationDispatcher.NonBlockingCountdownLock concurrentSessionLock;
readonly IExceptionPropertiesProvider exceptionPropertiesProvider;

/// <summary>
/// Initializes a new instance of the <see cref="TaskEntityDispatcher"/> class with an exception properties provider.
/// </summary>
/// <param name="orchestrationService">The orchestration service implementation</param>
/// <param name="entityObjectManager">The object manager for entities</param>
/// <param name="entityDispatchPipeline">The dispatch middleware pipeline</param>
/// <param name="logHelper">The log helper</param>
/// <param name="errorPropagationMode">The error propagation mode</param>
/// <param name="exceptionPropertiesProvider">The exception properties provider for extracting custom properties from exceptions</param>
internal TaskEntityDispatcher(
IOrchestrationService orchestrationService,
INameVersionObjectManager<TaskEntity> 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;

Expand Down
15 changes: 12 additions & 3 deletions src/DurableTask.Core/TaskHubWorker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -247,6 +247,12 @@ public TaskHubWorker(
/// </remarks>
public ErrorPropagationMode ErrorPropagationMode { get; set; }

/// <summary>
/// Gets or sets the exception properties provider that extracts custom properties from exceptions
/// when creating FailureDetails objects.
/// </summary>
public IExceptionPropertiesProvider ExceptionPropertiesProvider { get; set; }

/// <summary>
/// Adds a middleware delegate to the orchestration dispatch pipeline.
/// </summary>
Expand Down Expand Up @@ -296,13 +302,15 @@ public async Task<TaskHubWorker> 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)
{
Expand All @@ -311,7 +319,8 @@ public async Task<TaskHubWorker> StartAsync()
this.entityManager,
this.entityDispatchPipeline,
this.logHelper,
this.ErrorPropagationMode);
this.ErrorPropagationMode,
this.ExceptionPropertiesProvider);
}

await this.orchestrationService.StartAsync();
Expand Down
3 changes: 2 additions & 1 deletion src/DurableTask.Core/TaskOrchestration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ public override async Task<string> 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)
Expand Down
Loading
Loading