diff --git a/ArchUnitNET/Loader/DotNetCoreAssemblyResolver.cs b/ArchUnitNET/Loader/DotNetCoreAssemblyResolver.cs
index a4e798d8b..ee9e0eb2a 100644
--- a/ArchUnitNET/Loader/DotNetCoreAssemblyResolver.cs
+++ b/ArchUnitNET/Loader/DotNetCoreAssemblyResolver.cs
@@ -34,25 +34,42 @@ public AssemblyDefinition Resolve(AssemblyNameReference name, ReaderParameters p
throw new ArgumentNullException(nameof(name));
}
- if (
- _libraries.TryGetValue(name.FullName, out var assemblyDefinition)
- || string.IsNullOrEmpty(AssemblyPath)
- )
+ if (_libraries.TryGetValue(name.FullName, out var assemblyDefinition))
{
return assemblyDefinition;
}
- var file = Directory
- .EnumerateFiles(AssemblyPath, $"{name.Name}.dll", SearchOption.AllDirectories)
- .FirstOrDefault();
+ if (!string.IsNullOrEmpty(AssemblyPath))
+ {
+ var file = Directory
+ .EnumerateFiles(AssemblyPath, $"{name.Name}.dll", SearchOption.AllDirectories)
+ .FirstOrDefault();
+
+ if (file != null)
+ {
+ assemblyDefinition = AssemblyDefinition.ReadAssembly(file, parameters);
+ _libraries.Add(name.FullName, assemblyDefinition);
+ return assemblyDefinition;
+ }
+ }
- if (file == null)
+ // Fall back to DefaultAssemblyResolver for framework assemblies not found in
+ // AssemblyPath. Pass the original parameters so the loaded assembly keeps using
+ // this resolver (which returns null for unresolvable references) instead of the
+ // DefaultAssemblyResolver (which throws).
+ try
+ {
+ assemblyDefinition = _defaultAssemblyResolver.Resolve(name, parameters);
+ }
+ catch (AssemblyResolutionException)
{
return null;
}
- assemblyDefinition = AssemblyDefinition.ReadAssembly(file, parameters);
- _libraries.Add(name.FullName, assemblyDefinition);
+ if (assemblyDefinition != null)
+ {
+ _libraries.Add(name.FullName, assemblyDefinition);
+ }
return assemblyDefinition;
}
@@ -84,7 +101,7 @@ [NotNull] AssemblyDefinition moduleAssembly
public void AddLib(AssemblyNameReference name)
{
- var assembly = Resolve(name) ?? _defaultAssemblyResolver.Resolve(name);
+ var assembly = Resolve(name);
AddLib(name, assembly ?? throw new AssemblyResolutionException(name));
}
diff --git a/ArchUnitNET/Loader/InstructionExtensions.cs b/ArchUnitNET/Loader/InstructionExtensions.cs
index 3df361f31..bc413a3d6 100644
--- a/ArchUnitNET/Loader/InstructionExtensions.cs
+++ b/ArchUnitNET/Loader/InstructionExtensions.cs
@@ -46,7 +46,7 @@ [NotNull] this Instruction methodCallInstruction
&& methodCallAssignment.Operand is FieldReference fieldReference
)
{
- return fieldReference.Resolve();
+ return fieldReference.TryResolve();
}
return null;
diff --git a/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs b/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs
index 654a9c8eb..5f420d4ca 100644
--- a/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs
+++ b/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs
@@ -321,8 +321,9 @@ internal static bool IsCompilerGenerated(this MemberReference memberReference)
{
return true;
}
- var declaringType =
- memberReference.Resolve()?.DeclaringType ?? memberReference.DeclaringType;
+
+ var resolvedType = memberReference.TryResolve();
+ var declaringType = resolvedType?.DeclaringType ?? memberReference.DeclaringType;
return declaringType != null && declaringType.Name.HasCompilerGeneratedName();
}
diff --git a/ArchUnitNET/Loader/MonoCecilResolveExtensions.cs b/ArchUnitNET/Loader/MonoCecilResolveExtensions.cs
new file mode 100644
index 000000000..e05f7b41a
--- /dev/null
+++ b/ArchUnitNET/Loader/MonoCecilResolveExtensions.cs
@@ -0,0 +1,67 @@
+using System;
+using JetBrains.Annotations;
+using Mono.Cecil;
+
+namespace ArchUnitNET.Loader
+{
+ ///
+ /// Resolve extensions that return null instead of throwing
+ /// when an assembly cannot be located.
+ ///
+ internal static class MonoCecilResolveExtensions
+ {
+ [CanBeNull]
+ public static TypeDefinition TryResolve([CanBeNull] this TypeReference typeReference)
+ {
+ if (typeReference == null)
+ {
+ return null;
+ }
+
+ try
+ {
+ return typeReference.Resolve();
+ }
+ catch (AssemblyResolutionException)
+ {
+ return null;
+ }
+ }
+
+ [CanBeNull]
+ public static FieldDefinition TryResolve([CanBeNull] this FieldReference fieldReference)
+ {
+ if (fieldReference == null)
+ {
+ return null;
+ }
+
+ try
+ {
+ return fieldReference.Resolve();
+ }
+ catch (AssemblyResolutionException)
+ {
+ return null;
+ }
+ }
+
+ [CanBeNull]
+ public static IMemberDefinition TryResolve([CanBeNull] this MemberReference memberReference)
+ {
+ if (memberReference == null)
+ {
+ return null;
+ }
+
+ try
+ {
+ return memberReference.Resolve();
+ }
+ catch (AssemblyResolutionException)
+ {
+ return null;
+ }
+ }
+ }
+}
diff --git a/ArchUnitNET/Loader/MonoCecilTypeExtensions.cs b/ArchUnitNET/Loader/MonoCecilTypeExtensions.cs
index 83af639b3..b27d536f2 100644
--- a/ArchUnitNET/Loader/MonoCecilTypeExtensions.cs
+++ b/ArchUnitNET/Loader/MonoCecilTypeExtensions.cs
@@ -51,8 +51,13 @@ internal static bool IsAttribute([CanBeNull] this TypeDefinition typeDefinition)
{
if (typeDefinition?.BaseType != null)
{
- return typeDefinition.BaseType.FullName == "System.Attribute"
- || IsAttribute(typeDefinition.BaseType.Resolve());
+ if (typeDefinition.BaseType.FullName == "System.Attribute")
+ {
+ return true;
+ }
+
+ var resolvedBaseType = typeDefinition.BaseType.TryResolve();
+ return resolvedBaseType != null && IsAttribute(resolvedBaseType);
}
return false;
diff --git a/ArchUnitNET/Loader/TypeProcessor.cs b/ArchUnitNET/Loader/TypeProcessor.cs
index 193812176..a55c63d4f 100644
--- a/ArchUnitNET/Loader/TypeProcessor.cs
+++ b/ArchUnitNET/Loader/TypeProcessor.cs
@@ -797,15 +797,10 @@ var calledMethodReference in calledMethodReferences.Except(visitedMethodReferenc
if (calledMethodReference.IsCompilerGenerated())
{
- MethodDefinition calledMethodDefinition;
- try
- {
- calledMethodDefinition = calledMethodReference.Resolve();
- }
- catch (AssemblyResolutionException)
- {
- calledMethodDefinition = null;
- }
+ // Here an AssemblyResolutionException would only occur if the method is from a referenced assembly that is not available.
+ // However, since we are only resolving compiler-generated methods here, we can safely assume that the method is from an
+ // available assembly and don't need to handle the exception.
+ var calledMethodDefinition = calledMethodReference.Resolve();
if (calledMethodDefinition?.Body == null)
{
@@ -1071,7 +1066,7 @@ private static IEnumerable GetInterfacesImplementedByClass(
TypeDefinition typeDefinition
)
{
- var baseType = typeDefinition.BaseType?.Resolve();
+ var baseType = typeDefinition.BaseType.TryResolve();
var baseInterfaces =
baseType != null
? GetInterfacesImplementedByClass(baseType)
diff --git a/ArchUnitNETTests/Loader/ArchLoaderTests.cs b/ArchUnitNETTests/Loader/ArchLoaderTests.cs
index a5129e411..b8d44ffcb 100644
--- a/ArchUnitNETTests/Loader/ArchLoaderTests.cs
+++ b/ArchUnitNETTests/Loader/ArchLoaderTests.cs
@@ -327,7 +327,7 @@ public void UnavailableTypeTest()
System.IO.SearchOption.AllDirectories
)
.Build();
- Assert.Equal(3, architecture.Types.Count());
+ Assert.Equal(4, architecture.Types.Count());
var loggerType = architecture.ReferencedTypes.WhereFullNameIs("Serilog.ILogger");
Assert.NotNull(loggerType);
Assert.True(loggerType is UnavailableType);
diff --git a/ArchUnitNETTests/Loader/UnresolvableAssemblyTests.cs b/ArchUnitNETTests/Loader/UnresolvableAssemblyTests.cs
new file mode 100644
index 000000000..82d761f03
--- /dev/null
+++ b/ArchUnitNETTests/Loader/UnresolvableAssemblyTests.cs
@@ -0,0 +1,195 @@
+using System;
+using System.IO;
+using System.Linq;
+using ArchUnitNET.Loader;
+using Mono.Cecil;
+using Mono.Cecil.Cil;
+using Xunit;
+using static ArchUnitNET.Loader.MonoCecilMemberExtensions;
+using static ArchUnitNET.Loader.MonoCecilTypeExtensions;
+
+namespace ArchUnitNETTests.Loader
+{
+ ///
+ /// Tests that loader code handles assemblies whose dependencies cannot be resolved
+ /// (e.g. when a referenced assembly is not present in the output directory).
+ ///
+ public class UnresolvableAssemblyTests
+ {
+ private static string RepoRoot =>
+ AppDomain.CurrentDomain.BaseDirectory[
+ ..AppDomain.CurrentDomain.BaseDirectory.IndexOf(
+ @"ArchUnitNETTests",
+ StringComparison.InvariantCulture
+ )
+ ];
+
+ private static string FilteredDirectoryLoaderTestAssemblyDir =>
+ Path.Combine(RepoRoot, "TestAssemblies", "FilteredDirectoryLoaderTestAssembly");
+
+ private static string FilteredDirectoryLoaderTestAssemblyPath =>
+ Directory
+ .EnumerateFiles(
+ Path.Combine(RepoRoot, "TestAssemblies", "FilteredDirectoryLoaderTestAssembly"),
+ "FilteredDirectoryLoaderTestAssembly.dll",
+ SearchOption.AllDirectories
+ )
+ .First();
+
+ private static (DefaultAssemblyResolver resolver, ModuleDefinition module) LoadModule()
+ {
+ var assemblyPath = FilteredDirectoryLoaderTestAssemblyPath;
+ Assert.True(File.Exists(assemblyPath), $"Test assembly not found at {assemblyPath}");
+
+ // DefaultAssemblyResolver has no search directories, so resolving
+ // FilteredDirectoryUnavailableTypesAssembly will fail.
+ var resolver = new DefaultAssemblyResolver();
+ var module = ModuleDefinition.ReadModule(
+ assemblyPath,
+ new ReaderParameters { AssemblyResolver = resolver }
+ );
+ return (resolver, module);
+ }
+
+ private static TypeDefinition GetDerivedAttribute(ModuleDefinition module)
+ {
+ var derivedAttribute = module.Types.FirstOrDefault(t =>
+ t.FullName == "FilteredDirectoryLoaderTestAssembly.DerivedAttribute"
+ );
+ Assert.NotNull(derivedAttribute);
+ return derivedAttribute;
+ }
+
+ ///
+ /// DerivedAttribute extends BaseAttribute from FilteredDirectoryUnavailableTypesAssembly,
+ /// which is not copied to the output directory (Private=False). IsAttribute must not
+ /// throw when the base type's assembly is unavailable.
+ ///
+ /// Exercises →
+ /// .
+ ///
+ [Fact]
+ public void IsAttribute_HandlesUnresolvableBaseTypeAssembly()
+ {
+ var (resolver, module) = LoadModule();
+ try
+ {
+ var derivedAttribute = GetDerivedAttribute(module);
+ Assert.Equal(
+ "FilteredDirectoryUnavailableTypesAssembly.BaseAttribute",
+ derivedAttribute.BaseType.FullName
+ );
+
+ Assert.False(derivedAttribute.IsAttribute());
+ }
+ finally
+ {
+ module.Dispose();
+ resolver.Dispose();
+ }
+ }
+
+ ///
+ /// DerivedAttribute's constructor calls BaseAttribute..ctor(). The method reference
+ /// points into the unavailable assembly. IsCompilerGenerated must not throw when
+ /// resolving the member reference fails.
+ ///
+ /// Exercises →
+ /// .
+ ///
+ [Fact]
+ public void IsCompilerGenerated_HandlesUnresolvableMemberAssembly()
+ {
+ var (resolver, module) = LoadModule();
+ try
+ {
+ var derivedCtor = GetDerivedAttribute(module).Methods.First(m => m.IsConstructor);
+
+ // The constructor body contains a call to BaseAttribute..ctor()
+ var baseCtorRef = derivedCtor
+ .Body.Instructions.Where(i => i.OpCode == OpCodes.Call)
+ .Select(i => i.Operand as MethodReference)
+ .First(m =>
+ m != null
+ && m.DeclaringType.FullName
+ == "FilteredDirectoryUnavailableTypesAssembly.BaseAttribute"
+ );
+
+ Assert.False(baseCtorRef.IsCompilerGenerated());
+ }
+ finally
+ {
+ module.Dispose();
+ resolver.Dispose();
+ }
+ }
+
+ ///
+ /// UnavailableFieldAssigner.AssignUnavailableField assigns the result of a method call
+ /// to UnavailableFieldHolder.Value, a static field declared in the unavailable assembly.
+ /// GetAssigneeFieldDefinition must not throw when resolving the field reference fails.
+ ///
+ /// Exercises →
+ /// .
+ ///
+ [Fact]
+ public void GetAssigneeFieldDefinition_HandlesUnresolvableFieldAssembly()
+ {
+ var (resolver, module) = LoadModule();
+ try
+ {
+ var assigner = module.Types.FirstOrDefault(t =>
+ t.FullName == "FilteredDirectoryLoaderTestAssembly.UnavailableFieldAssigner"
+ );
+ Assert.NotNull(assigner);
+ var method = assigner.Methods.First(m => m.Name == "AssignUnavailableField");
+
+ var callInstruction = method.Body.Instructions.First(i =>
+ i.IsMethodCallOp()
+ && i.Next?.Operand is FieldReference f
+ && f.DeclaringType.FullName
+ == "FilteredDirectoryUnavailableTypesAssembly.UnavailableFieldHolder"
+ );
+
+ Assert.Null(callInstruction.GetAssigneeFieldDefinition());
+ }
+ finally
+ {
+ module.Dispose();
+ resolver.Dispose();
+ }
+ }
+
+ ///
+ /// End-to-end check that can load and build an architecture
+ /// from FilteredDirectoryLoaderTestAssembly, including its direct dependencies,
+ /// when one of those dependencies (FilteredDirectoryUnavailableTypesAssembly) cannot be
+ /// resolved. That assembly is not copied to the output directory (Private=False), so
+ /// base types such as BaseAttribute and member and field references into it cannot
+ /// be resolved during loading.
+ ///
+ /// The test passes if loading and building finish without throwing an exception.
+ ///
+ /// Exercises →
+ /// ... →
+ ///
+ [Fact]
+ public void ArchLoader_HandlesFallbackToDefaultAssemblyResolverForUnavailableAssemblyWithoutException()
+ {
+ var assemblyDir = FilteredDirectoryLoaderTestAssemblyDir;
+ Assert.True(
+ Directory.Exists(assemblyDir),
+ $"Test assembly directory not found at {assemblyDir}"
+ );
+
+ var architecture = new ArchLoader()
+ .LoadFilteredDirectoryIncludingDependencies(
+ assemblyDir,
+ "FilteredDirectoryLoaderTestAssembly.dll",
+ false,
+ SearchOption.AllDirectories
+ )
+ .Build();
+ }
+ }
+}
diff --git a/TestAssemblies/FilteredDirectoryLoaderTestAssembly/Class1.cs b/TestAssemblies/FilteredDirectoryLoaderTestAssembly/Class1.cs
index 8f69b7fd3..1d21c67c2 100644
--- a/TestAssemblies/FilteredDirectoryLoaderTestAssembly/Class1.cs
+++ b/TestAssemblies/FilteredDirectoryLoaderTestAssembly/Class1.cs
@@ -19,3 +19,13 @@ public class DerivedAttribute : BaseAttribute { }
[Derived]
public class ClassWithDerivedAttribute { }
+
+public class UnavailableFieldAssigner
+{
+ public void AssignUnavailableField()
+ {
+ UnavailableFieldHolder.Value = CreateValue();
+ }
+
+ private static int CreateValue() => 42;
+}
diff --git a/TestAssemblies/FilteredDirectoryUnavailableTypesAssembly/UnavailableTypes.cs b/TestAssemblies/FilteredDirectoryUnavailableTypesAssembly/UnavailableTypes.cs
index fe44501b3..bb98322f9 100644
--- a/TestAssemblies/FilteredDirectoryUnavailableTypesAssembly/UnavailableTypes.cs
+++ b/TestAssemblies/FilteredDirectoryUnavailableTypesAssembly/UnavailableTypes.cs
@@ -5,3 +5,8 @@ public class AssemblyAttribute : Attribute { }
[AttributeUsage(AttributeTargets.All)]
public class BaseAttribute : Attribute { }
+
+public static class UnavailableFieldHolder
+{
+ public static int Value;
+}