diff --git a/src/DurableTask.Core/FailureDetails.cs b/src/DurableTask.Core/FailureDetails.cs index 4e2abaaaf..c4ad18895 100644 --- a/src/DurableTask.Core/FailureDetails.cs +++ b/src/DurableTask.Core/FailureDetails.cs @@ -197,18 +197,13 @@ 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(); - 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."); - } + .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. + return matchingExceptionTypes.Any(matchType => typeof(T).IsAssignableFrom(matchType)); } return exceptionType != null && typeof(T).IsAssignableFrom(exceptionType); 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(); + } } }