From 2aa7fcf031be23073c378840dfba37378c00bf9f Mon Sep 17 00:00:00 2001 From: Andy Staples Date: Fri, 29 Aug 2025 15:24:31 -0600 Subject: [PATCH 1/2] Loosen exception matching logic in IsCausedBy --- src/DurableTask.Core/FailureDetails.cs | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/DurableTask.Core/FailureDetails.cs b/src/DurableTask.Core/FailureDetails.cs index 5dfd02ab9..d6e1c7c30 100644 --- a/src/DurableTask.Core/FailureDetails.cs +++ b/src/DurableTask.Core/FailureDetails.cs @@ -149,14 +149,10 @@ public bool IsCausedBy() where T : Exception .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."); - } + + // Previously, this logic would only return true if matchingExceptionTypes found only one assembly with a type matching ErrorType. + // Now, it will return true if any matching assembly has a type that is assignable to T. + return matchingExceptionTypes.Any(matchType => typeof(T).IsAssignableFrom(matchType)); } return exceptionType != null && typeof(T).IsAssignableFrom(exceptionType); From f9487a9f05321d7436c91097ecf41b613f54b5be Mon Sep 17 00:00:00 2001 From: Andy Staples Date: Thu, 12 Mar 2026 14:11:21 -0600 Subject: [PATCH 2/2] PR feedback --- src/DurableTask.Core/FailureDetails.cs | 5 ++- .../ExceptionHandlingIntegrationTests.cs | 34 +++++++++++++++++++ 2 files changed, 36 insertions(+), 3 deletions(-) diff --git a/src/DurableTask.Core/FailureDetails.cs b/src/DurableTask.Core/FailureDetails.cs index 1aa15cd4f..c4ad18895 100644 --- a/src/DurableTask.Core/FailureDetails.cs +++ b/src/DurableTask.Core/FailureDetails.cs @@ -197,10 +197,9 @@ public bool IsCausedBy() where T : Exception { // 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() + var matchingExceptionTypes = AppDomain.CurrentDomain.GetAssemblies() .Select(a => a.GetType(this.ErrorType, throwOnError: false)) - .Where(t => t is not null) - .ToList(); + .Where(t => t is not null); // Previously, this logic would only return true if matchingExceptionTypes found only one assembly with a type matching ErrorType. // Now, it will return true if any matching assembly has a type that is assignable to T. diff --git a/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs b/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs index 8480418eb..a82897ca1 100644 --- a/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs +++ b/test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs @@ -16,6 +16,8 @@ namespace DurableTask.Core.Tests using System; using System.Collections.Generic; using System.Diagnostics; + using System.Reflection; + using System.Reflection.Emit; using System.Runtime.Serialization; using System.Threading.Tasks; using DurableTask.Core.Exceptions; @@ -541,5 +543,37 @@ protected CustomException(SerializationInfo info, StreamingContext context) { } } + + [TestMethod] + public void IsCausedBy_DoesNotThrow_WhenMultipleAssembliesDefineSameType() + { + // Create two dynamic assemblies, each containing an Exception-derived type with the + // same fully qualified name. This simulates the scenario where the same exception type + // is loaded from multiple assemblies (e.g. different NuGet package versions). + string typeName = "TestDynamic.DuplicateException"; + CreateDynamicAssemblyWithExceptionType(typeName, "DynAssembly1"); + CreateDynamicAssemblyWithExceptionType(typeName, "DynAssembly2"); + + // Create a FailureDetails whose ErrorType won't be resolved by Type.GetType(), + // typeof(T).Assembly, or the calling assembly, forcing the AppDomain fallback path. + var details = new FailureDetails( + typeName, "Test error", stackTrace: null, innerFailure: null, isNonRetriable: false); + + // The old implementation would either throw AmbiguousMatchException or return false + // when multiple assemblies contained the same type. The fix uses Any() so this should + // succeed without throwing. + bool result = details.IsCausedBy(); + + Assert.IsTrue(result); + } + + static void CreateDynamicAssemblyWithExceptionType(string typeName, string assemblyName) + { + var asmName = new AssemblyName(assemblyName); + var asmBuilder = AssemblyBuilder.DefineDynamicAssembly(asmName, AssemblyBuilderAccess.Run); + var modBuilder = asmBuilder.DefineDynamicModule(assemblyName); + var typeBuilder = modBuilder.DefineType(typeName, TypeAttributes.Public, typeof(Exception)); + typeBuilder.CreateType(); + } } }