Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 6 additions & 11 deletions src/DurableTask.Core/FailureDetails.cs
Original file line number Diff line number Diff line change
Expand Up @@ -197,18 +197,13 @@ public bool IsCausedBy<T>() 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<Type> 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);
Expand Down
34 changes: 34 additions & 0 deletions test/DurableTask.Core.Tests/ExceptionHandlingIntegrationTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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.
Comment on lines +562 to +564
Copy link

Copilot AI Mar 12, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The test comment says the old implementation would "either throw AmbiguousMatchException or return false" when multiple assemblies contained the same type. In the fallback path being exercised here, the previous implementation would always throw when more than one matching type was found, not return false. Please update the comment to accurately describe the prior behavior to avoid confusion for future readers.

Suggested change
// 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.
// In this AppDomain fallback path, the old implementation would always throw
// AmbiguousMatchException when more than one matching type was found. The fix uses
// Any() so this should succeed without throwing.

Copilot uses AI. Check for mistakes.
bool result = details.IsCausedBy<Exception>();

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();
}
}
}
Loading