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 061dc5a28..834445005 100644
--- a/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs
+++ b/ArchUnitNET/Loader/MonoCecilMemberExtensions.cs
@@ -295,8 +295,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..d8b25ea59
--- /dev/null
+++ b/ArchUnitNET/Loader/MonoCecilResolveExtensions.cs
@@ -0,0 +1,85 @@
+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 MethodDefinition TryResolve([CanBeNull] this MethodReference methodReference)
+ {
+ if (methodReference == null)
+ {
+ return null;
+ }
+
+ try
+ {
+ return methodReference.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 c765cdd8a..45c470511 100644
--- a/ArchUnitNET/Loader/TypeProcessor.cs
+++ b/ArchUnitNET/Loader/TypeProcessor.cs
@@ -797,15 +797,7 @@ var calledMethodReference in calledMethodReferences.Except(visitedMethodReferenc
if (calledMethodReference.IsCompilerGenerated())
{
- MethodDefinition calledMethodDefinition;
- try
- {
- calledMethodDefinition = calledMethodReference.Resolve();
- }
- catch (AssemblyResolutionException)
- {
- calledMethodDefinition = null;
- }
+ var calledMethodDefinition = calledMethodReference.TryResolve();
if (calledMethodDefinition?.Body == null)
{
@@ -866,7 +858,7 @@ DomainResolver domainResolver
{
var compilerGeneratedGeneratorObject = methodBody
.Instructions.Where(inst => inst.IsNewObjectOp())
- .Select(inst => ((MethodReference)inst.Operand).DeclaringType.Resolve())
+ .Select(inst => ((MethodReference)inst.Operand).DeclaringType.TryResolve())
.FirstOrDefault(type =>
type != null
&& type.Methods.Any(method => method.Name == nameof(IEnumerator.MoveNext))
@@ -911,7 +903,7 @@ DomainResolver domainResolver
{
var compilerGeneratedGeneratorObject = methodBody
.Instructions.Where(inst => inst.IsNewObjectOp())
- .Select(inst => ((MethodReference)inst.Operand).DeclaringType.Resolve())
+ .Select(inst => ((MethodReference)inst.Operand).DeclaringType.TryResolve())
.FirstOrDefault(type =>
type != null
&& type.Methods.Any(method =>
@@ -1083,7 +1075,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/UnresolvableAssemblyTests.cs b/ArchUnitNETTests/Loader/UnresolvableAssemblyTests.cs
new file mode 100644
index 000000000..2ae0fb8d3
--- /dev/null
+++ b/ArchUnitNETTests/Loader/UnresolvableAssemblyTests.cs
@@ -0,0 +1,72 @@
+using System;
+using System.IO;
+using System.Linq;
+using Mono.Cecil;
+using Xunit;
+using static ArchUnitNET.Loader.MonoCecilTypeExtensions;
+
+namespace ArchUnitNETTests.Loader
+{
+ ///
+ /// Tests that the loader 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 FilteredDirectoryLoaderTestAssemblyPath =>
+ Directory
+ .EnumerateFiles(
+ Path.Combine(RepoRoot, "TestAssemblies", "FilteredDirectoryLoaderTestAssembly"),
+ "FilteredDirectoryLoaderTestAssembly.dll",
+ SearchOption.AllDirectories
+ )
+ .First();
+
+ ///
+ /// 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.
+ ///
+ [Fact]
+ public void IsAttributeHandlesUnresolvableBaseTypeAssembly()
+ {
+ 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 }
+ );
+
+ try
+ {
+ var derivedAttribute = module.Types.FirstOrDefault(t =>
+ t.FullName == "FilteredDirectoryLoaderTestAssembly.DerivedAttribute"
+ );
+ Assert.NotNull(derivedAttribute);
+ Assert.Equal(
+ "FilteredDirectoryUnavailableTypesAssembly.BaseAttribute",
+ derivedAttribute.BaseType.FullName
+ );
+
+ Assert.False(derivedAttribute.IsAttribute());
+ }
+ finally
+ {
+ module.Dispose();
+ resolver.Dispose();
+ }
+ }
+ }
+}