diff --git a/Test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs b/Test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs index 83fa7f0d1..ed41ea931 100644 --- a/Test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs +++ b/Test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs @@ -15,6 +15,7 @@ namespace DurableTask.Core.Tests { using System; using System.Diagnostics; + using System.Runtime.Serialization; using System.Threading.Tasks; using DurableTask.Core.Exceptions; using DurableTask.Emulator; @@ -203,7 +204,10 @@ await context.ScheduleWithRetry( tfe.FailureDetails.ErrorMessage == "This is a test exception" && tfe.FailureDetails.StackTrace!.Contains(typeof(ThrowInvalidOperationException).Name) && tfe.FailureDetails.IsCausedBy() && - tfe.FailureDetails.IsCausedBy()) // check that base types work too + tfe.FailureDetails.IsCausedBy() && + tfe.FailureDetails.InnerFailure != null && + tfe.FailureDetails.InnerFailure.IsCausedBy() && + tfe.FailureDetails.InnerFailure.ErrorMessage == "And this is its custom inner exception") { // Stop retrying return false; @@ -235,7 +239,8 @@ class ThrowInvalidOperationException : TaskActivity { protected override string Execute(TaskContext context, string input) { - throw new InvalidOperationException("This is a test exception"); + throw new InvalidOperationException("This is a test exception", + new CustomException("And this is its custom inner exception")); } } @@ -243,7 +248,22 @@ class ThrowInvalidOperationExceptionAsync : AsyncTaskActivity { protected override Task ExecuteAsync(TaskContext context, string input) { - throw new InvalidOperationException("This is a test exception"); + throw new InvalidOperationException("This is a test exception", + new CustomException("And this is its custom inner exception")); + } + } + + [Serializable] + class CustomException : Exception + { + public CustomException(string message) + : base(message) + { + } + + protected CustomException(SerializationInfo info, StreamingContext context) + : base(info, context) + { } } } diff --git a/src/DurableTask.Core/DurableTask.Core.csproj b/src/DurableTask.Core/DurableTask.Core.csproj index 536dfc22b..f413cfab4 100644 --- a/src/DurableTask.Core/DurableTask.Core.csproj +++ b/src/DurableTask.Core/DurableTask.Core.csproj @@ -17,8 +17,8 @@ 2 - 16 - 2 + 17 + 0 $(MajorVersion).$(MinorVersion).$(PatchVersion) $(VersionPrefix).0 diff --git a/src/DurableTask.Core/Exceptions/OrchestrationException.cs b/src/DurableTask.Core/Exceptions/OrchestrationException.cs index 07e2ff9e1..8c8a144af 100644 --- a/src/DurableTask.Core/Exceptions/OrchestrationException.cs +++ b/src/DurableTask.Core/Exceptions/OrchestrationException.cs @@ -88,8 +88,8 @@ public override void GetObjectData(SerializationInfo info, StreamingContext cont public int EventId { get; set; } /// - /// Gets additional details about the failure. May be null if the failure details collection is not enabled. + /// Gets or sets additional details about the failure. May be null if the failure details collection is not enabled. /// - public FailureDetails? FailureDetails { get; internal set; } + public FailureDetails? FailureDetails { get; set; } } } \ No newline at end of file diff --git a/src/DurableTask.Core/FailureDetails.cs b/src/DurableTask.Core/FailureDetails.cs index ddf1f1a33..5dfd02ab9 100644 --- a/src/DurableTask.Core/FailureDetails.cs +++ b/src/DurableTask.Core/FailureDetails.cs @@ -14,10 +14,16 @@ namespace DurableTask.Core { using System; + using System.Collections.Generic; + using System.Linq; + using System.Reflection; using System.Runtime.Serialization; using DurableTask.Core.Exceptions; using Newtonsoft.Json; + // NOTE: This class is very similar to https://github.com/microsoft/durabletask-dotnet/blob/main/src/Abstractions/TaskFailureDetails.cs. + // Any functional changes to this class should be mirrored in that class and vice versa. + /// /// Details of an activity, orchestration, or entity operation failure. /// @@ -42,6 +48,16 @@ public FailureDetails(string errorType, string errorMessage, string? stackTrace, this.IsNonRetriable = isNonRetriable; } + /// + /// 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. + public FailureDetails(Exception e, FailureDetails innerFailure) + : this(e.GetType().FullName, GetErrorMessage(e), e.StackTrace, innerFailure, false) + { + } + /// /// Initializes a new instance of the class from an exception object. /// @@ -116,7 +132,33 @@ public override string ToString() /// Returns true if the value matches ; false otherwise. public bool IsCausedBy() where T : Exception { + // This check works for .NET exception types defined in System.Core.PrivateLib (aka mscorelib.dll) Type? exceptionType = Type.GetType(this.ErrorType, throwOnError: false); + + // For exception types defined in the same assembly as the target exception type. + exceptionType ??= typeof(T).Assembly.GetType(this.ErrorType, throwOnError: false); + + // For custom exception types defined in the app's assembly. + exceptionType ??= Assembly.GetCallingAssembly().GetType(this.ErrorType, throwOnError: false); + + if (exceptionType == null) + { + // This last check works for exception types defined in any loaded assembly (e.g. NuGet packages, etc.). + // This is a fallback that should rarely be needed except in obscure cases. + List matchingExceptionTypes = AppDomain.CurrentDomain.GetAssemblies() + .Select(a => a.GetType(this.ErrorType, throwOnError: false)) + .Where(t => t is not null) + .ToList(); + if (matchingExceptionTypes.Count == 1) + { + exceptionType = matchingExceptionTypes[0]; + } + else if (matchingExceptionTypes.Count > 1) + { + throw new AmbiguousMatchException($"Multiple exception types with the name '{this.ErrorType}' were found."); + } + } + return exceptionType != null && typeof(T).IsAssignableFrom(exceptionType); } diff --git a/src/DurableTask.Core/TaskOrchestrationContext.cs b/src/DurableTask.Core/TaskOrchestrationContext.cs index eb5b41760..3b2d5a797 100644 --- a/src/DurableTask.Core/TaskOrchestrationContext.cs +++ b/src/DurableTask.Core/TaskOrchestrationContext.cs @@ -638,6 +638,12 @@ public void FailOrchestration(Exception failure) details = orchestrationFailureException.Details; } } + else if (failure is TaskFailedException taskFailedException && + this.ErrorPropagationMode == ErrorPropagationMode.UseFailureDetails) + { + // Propagate the original FailureDetails + failureDetails = taskFailedException.FailureDetails; + } else { if (this.ErrorPropagationMode == ErrorPropagationMode.UseFailureDetails)