diff --git a/ArchUnitNET/Domain/ArchLoaderCacheConfig.cs b/ArchUnitNET/Domain/ArchLoaderCacheConfig.cs
new file mode 100644
index 00000000..69438dff
--- /dev/null
+++ b/ArchUnitNET/Domain/ArchLoaderCacheConfig.cs
@@ -0,0 +1,52 @@
+namespace ArchUnitNET.Domain
+{
+ ///
+ /// Configuration options for the ArchLoader caching mechanism
+ ///
+ public sealed class ArchLoaderCacheConfig
+ {
+ ///
+ /// Creates a new instance with default settings (caching enabled, file-based invalidation enabled)
+ ///
+ public ArchLoaderCacheConfig()
+ {
+ }
+
+ ///
+ /// Gets or sets whether caching is enabled. Default is true.
+ ///
+ public bool CachingEnabled { get; set; } = true;
+
+ ///
+ /// Gets or sets whether to use file-based invalidation (hash + timestamp + size checking).
+ /// Default is true. When false, only module names are used for cache keys.
+ ///
+ public bool UseFileBasedInvalidation { get; set; } = true;
+
+ ///
+ /// Gets or sets an optional user-provided cache key for fine-grained control.
+ /// When set, this key is included in the cache key computation.
+ ///
+ public string UserCacheKey { get; set; }
+
+ ///
+ /// Gets or sets whether to include the ArchUnitNET version in cache invalidation.
+ /// Default is true.
+ ///
+ public bool IncludeVersionInCacheKey { get; set; } = true;
+
+ ///
+ /// Creates a copy of this configuration
+ ///
+ public ArchLoaderCacheConfig Clone()
+ {
+ return new ArchLoaderCacheConfig
+ {
+ CachingEnabled = CachingEnabled,
+ UseFileBasedInvalidation = UseFileBasedInvalidation,
+ UserCacheKey = UserCacheKey,
+ IncludeVersionInCacheKey = IncludeVersionInCacheKey
+ };
+ }
+ }
+}
diff --git a/ArchUnitNET/Domain/ArchitectureCacheManager.cs b/ArchUnitNET/Domain/ArchitectureCacheManager.cs
new file mode 100644
index 00000000..5f0c2d6c
--- /dev/null
+++ b/ArchUnitNET/Domain/ArchitectureCacheManager.cs
@@ -0,0 +1,215 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace ArchUnitNET.Domain
+{
+ ///
+ /// Enhanced caching manager for Architecture instances with automatic invalidation support
+ ///
+ public class ArchitectureCacheManager
+ {
+ private readonly ConcurrentDictionary _cache =
+ new ConcurrentDictionary();
+
+ private static readonly Lazy _instance =
+ new Lazy(() => new ArchitectureCacheManager());
+
+ private static readonly string ArchUnitNetVersion =
+ typeof(Architecture).Assembly.GetName().Version?.ToString() ?? "unknown";
+
+ protected ArchitectureCacheManager() { }
+
+ public static ArchitectureCacheManager Instance => _instance.Value;
+
+ ///
+ /// Try to get a cached architecture. Returns null if not found or if cache is invalid.
+ ///
+ public Architecture TryGetArchitecture(
+ ArchitectureCacheKey baseCacheKey,
+ IEnumerable assemblyMetadata,
+ ArchLoaderCacheConfig config)
+ {
+ if (config == null || !config.CachingEnabled)
+ {
+ return null;
+ }
+
+ var assemblyMetadatas = assemblyMetadata as AssemblyMetadata[] ?? assemblyMetadata.ToArray();
+ var enhancedKey = new EnhancedCacheKey(
+ baseCacheKey,
+ config.UseFileBasedInvalidation ? assemblyMetadatas : null,
+ config.UserCacheKey,
+ config.IncludeVersionInCacheKey ? ArchUnitNetVersion : null
+ );
+
+ if (_cache.TryGetValue(enhancedKey, out var cached))
+ {
+ if (config.UseFileBasedInvalidation && cached.AssemblyMetadata != null)
+ {
+ var currentMetadata = assemblyMetadatas?.ToList();
+ if (!AreAssembliesUnchanged(cached.AssemblyMetadata, currentMetadata))
+ {
+ _cache.TryRemove(enhancedKey, out _);
+ return null;
+ }
+ }
+
+ return cached.Architecture;
+ }
+
+ return null;
+ }
+
+ ///
+ /// Add an architecture to the cache
+ ///
+ public bool Add(
+ ArchitectureCacheKey baseCacheKey,
+ Architecture architecture,
+ IEnumerable assemblyMetadata,
+ ArchLoaderCacheConfig config)
+ {
+ if (config == null || !config.CachingEnabled)
+ {
+ return false;
+ }
+
+ var assemblyMetadatas = assemblyMetadata as AssemblyMetadata[] ?? assemblyMetadata.ToArray();
+ var enhancedKey = new EnhancedCacheKey(
+ baseCacheKey,
+ config.UseFileBasedInvalidation ? assemblyMetadatas : null,
+ config.UserCacheKey,
+ config.IncludeVersionInCacheKey ? ArchUnitNetVersion : null
+ );
+
+ var cached = new CachedArchitecture
+ {
+ Architecture = architecture,
+ AssemblyMetadata = config.UseFileBasedInvalidation
+ ? assemblyMetadatas?.ToList()
+ : null,
+ CachedAt = DateTime.UtcNow
+ };
+
+ return _cache.TryAdd(enhancedKey, cached);
+ }
+
+ ///
+ /// Clear all cached architectures
+ ///
+ public void Clear() => _cache.Clear();
+
+ ///
+ /// Get the number of cached architectures
+ ///
+ public int Count => _cache.Count;
+
+ private static bool AreAssembliesUnchanged(
+ List cached,
+ List current)
+ {
+ if (cached == null || current == null)
+ return cached == current;
+
+ if (cached.Count != current.Count)
+ return false;
+
+ var cachedDict = cached.ToDictionary(m => m.FilePath, StringComparer.OrdinalIgnoreCase);
+
+ foreach (var currentMeta in current)
+ {
+ if (!cachedDict.TryGetValue(currentMeta.FilePath, out var cachedMeta))
+ return false;
+
+ if (!cachedMeta.Equals(currentMeta))
+ return false;
+ }
+
+ return true;
+ }
+
+ private class CachedArchitecture
+ {
+ public Architecture Architecture { get; set; }
+ public List AssemblyMetadata { get; set; }
+ public DateTime CachedAt { get; set; }
+ }
+
+ private class EnhancedCacheKey : IEquatable
+ {
+ private readonly ArchitectureCacheKey _baseCacheKey;
+ private readonly List _assemblyMetadata;
+ private readonly string _userCacheKey;
+ private readonly string _version;
+ private readonly int _hashCode;
+
+ public EnhancedCacheKey(
+ ArchitectureCacheKey baseCacheKey,
+ IEnumerable assemblyMetadata,
+ string userCacheKey,
+ string version)
+ {
+ _baseCacheKey = baseCacheKey ?? throw new ArgumentNullException(nameof(baseCacheKey));
+ _assemblyMetadata = assemblyMetadata?.OrderBy(m => m.FilePath, StringComparer.OrdinalIgnoreCase).ToList();
+ _userCacheKey = userCacheKey;
+ _version = version;
+ _hashCode = ComputeHashCode();
+ }
+
+ private int ComputeHashCode()
+ {
+ unchecked
+ {
+ var hash = _baseCacheKey.GetHashCode();
+ hash = (hash * 397) ^ (_userCacheKey?.GetHashCode() ?? 0);
+ hash = (hash * 397) ^ (_version?.GetHashCode() ?? 0);
+
+ if (_assemblyMetadata != null)
+ {
+ foreach (var metadata in _assemblyMetadata)
+ {
+ hash = (hash * 397) ^ metadata.GetHashCode();
+ }
+ }
+
+ return hash;
+ }
+ }
+
+ public bool Equals(EnhancedCacheKey other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+
+ if (!_baseCacheKey.Equals(other._baseCacheKey))
+ return false;
+
+ if (_userCacheKey != other._userCacheKey)
+ return false;
+
+ if (_version != other._version)
+ return false;
+
+ if (_assemblyMetadata == null && other._assemblyMetadata == null)
+ return true;
+
+ if (_assemblyMetadata == null || other._assemblyMetadata == null)
+ return false;
+
+ if (_assemblyMetadata.Count != other._assemblyMetadata.Count)
+ return false;
+
+ return _assemblyMetadata.SequenceEqual(other._assemblyMetadata);
+ }
+
+ public override bool Equals(object obj)
+ {
+ return obj is EnhancedCacheKey other && Equals(other);
+ }
+
+ public override int GetHashCode() => _hashCode;
+ }
+ }
+}
diff --git a/ArchUnitNET/Domain/AssemblyMetadata.cs b/ArchUnitNET/Domain/AssemblyMetadata.cs
new file mode 100644
index 00000000..95f69401
--- /dev/null
+++ b/ArchUnitNET/Domain/AssemblyMetadata.cs
@@ -0,0 +1,82 @@
+using System;
+using System.IO;
+using System.Security.Cryptography;
+
+namespace ArchUnitNET.Domain
+{
+ ///
+ /// Tracks metadata for an assembly file to detect changes
+ ///
+ public sealed class AssemblyMetadata : IEquatable
+ {
+ public string FilePath { get; }
+ public string FileHash { get; }
+ public DateTime LastWriteTimeUtc { get; }
+ public long FileSize { get; }
+
+ public AssemblyMetadata(string filePath)
+ {
+ if (string.IsNullOrEmpty(filePath))
+ {
+ throw new ArgumentException("File path cannot be null or empty", nameof(filePath));
+ }
+
+ if (!File.Exists(filePath))
+ {
+ throw new FileNotFoundException($"Assembly file not found: {filePath}", filePath);
+ }
+
+ FilePath = Path.GetFullPath(filePath);
+ var fileInfo = new FileInfo(FilePath);
+ LastWriteTimeUtc = fileInfo.LastWriteTimeUtc;
+ FileSize = fileInfo.Length;
+ FileHash = ComputeFileHash(FilePath);
+ }
+
+ private static string ComputeFileHash(string filePath)
+ {
+ using (var sha256 = SHA256.Create())
+ using (var stream = File.OpenRead(filePath))
+ {
+ var hash = sha256.ComputeHash(stream);
+ return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
+ }
+ }
+
+ public bool Equals(AssemblyMetadata other)
+ {
+ if (ReferenceEquals(null, other)) return false;
+ if (ReferenceEquals(this, other)) return true;
+
+ return string.Equals(FilePath, other.FilePath, StringComparison.OrdinalIgnoreCase)
+ && FileHash == other.FileHash
+ && LastWriteTimeUtc.Equals(other.LastWriteTimeUtc)
+ && FileSize == other.FileSize;
+ }
+
+ public override bool Equals(object obj)
+ {
+ if (ReferenceEquals(null, obj)) return false;
+ if (ReferenceEquals(this, obj)) return true;
+ if (obj.GetType() != GetType()) return false;
+ return Equals((AssemblyMetadata)obj);
+ }
+
+ public override int GetHashCode()
+ {
+ unchecked
+ {
+ var hashCode = StringComparer.OrdinalIgnoreCase.GetHashCode(FilePath ?? string.Empty);
+ hashCode = (hashCode * 397) ^ (FileHash?.GetHashCode() ?? 0);
+ hashCode = (hashCode * 397) ^ LastWriteTimeUtc.GetHashCode();
+ hashCode = (hashCode * 397) ^ FileSize.GetHashCode();
+ return hashCode;
+ }
+ }
+
+ public override string ToString()
+ {
+ return $"AssemblyMetadata(Path={FilePath}, Hash={FileHash?.Substring(0, 8)}..., Size={FileSize}, Modified={LastWriteTimeUtc})";
+ }
+ }
+}
diff --git a/ArchUnitNET/Loader/ArchBuilder.cs b/ArchUnitNET/Loader/ArchBuilder.cs
index db9af6a0..6d52f9c6 100644
--- a/ArchUnitNET/Loader/ArchBuilder.cs
+++ b/ArchUnitNET/Loader/ArchBuilder.cs
@@ -1,10 +1,10 @@
-using System.Collections.Generic;
-using System.Linq;
using ArchUnitNET.Domain;
using ArchUnitNET.Domain.Extensions;
using ArchUnitNET.Loader.LoadTasks;
using JetBrains.Annotations;
using Mono.Cecil;
+using System.Collections.Generic;
+using System.Linq;
using GenericParameter = ArchUnitNET.Domain.GenericParameter;
namespace ArchUnitNET.Loader
@@ -161,5 +161,66 @@ public Architecture Build()
_architectureCache.Add(_architectureCacheKey, newArchitecture);
return newArchitecture;
}
+
+ public Architecture Build(List assemblyPaths, ArchLoaderCacheConfig cacheConfig)
+ {
+ var enhancedCacheManager = ArchitectureCacheManager.Instance;
+
+ List assemblyMetadata = null;
+ if (cacheConfig.UseFileBasedInvalidation && assemblyPaths != null && assemblyPaths.Count > 0)
+ {
+ assemblyMetadata = new List();
+ foreach (var path in assemblyPaths)
+ {
+ try
+ {
+ if (System.IO.File.Exists(path))
+ {
+ assemblyMetadata.Add(new AssemblyMetadata(path));
+ }
+ }
+ catch
+ {
+ assemblyMetadata = null;
+ break;
+ }
+ }
+ }
+
+ var architecture = enhancedCacheManager.TryGetArchitecture(
+ _architectureCacheKey,
+ assemblyMetadata,
+ cacheConfig
+ );
+
+ if (architecture != null)
+ {
+ return architecture;
+ }
+
+ UpdateTypeDefinitions();
+ var allTypes = _typeFactory.GetAllNonCompilerGeneratedTypes().ToList();
+ var genericParameters = allTypes.OfType().ToList();
+ var referencedTypes = allTypes.Except(Types).Except(genericParameters);
+ var namespaces = Namespaces.Where(ns => ns.Types.Any());
+ var newArchitecture = new Architecture(
+ Assemblies,
+ namespaces,
+ Types,
+ genericParameters,
+ referencedTypes
+ );
+
+ enhancedCacheManager.Add(
+ _architectureCacheKey,
+ newArchitecture,
+ assemblyMetadata,
+ cacheConfig
+ );
+
+ _architectureCache.Add(_architectureCacheKey, newArchitecture);
+
+ return newArchitecture;
+ }
}
}
diff --git a/ArchUnitNET/Loader/ArchLoader.cs b/ArchUnitNET/Loader/ArchLoader.cs
index 5ac7ab92..d834dd51 100644
--- a/ArchUnitNET/Loader/ArchLoader.cs
+++ b/ArchUnitNET/Loader/ArchLoader.cs
@@ -1,10 +1,10 @@
-using System;
+using ArchUnitNET.Domain;
+using ArchUnitNET.Domain.Extensions;
+using Mono.Cecil;
+using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
-using ArchUnitNET.Domain;
-using ArchUnitNET.Domain.Extensions;
-using Mono.Cecil;
using static System.IO.SearchOption;
using Assembly = System.Reflection.Assembly;
@@ -14,16 +14,53 @@ public class ArchLoader
{
private readonly ArchBuilder _archBuilder = new ArchBuilder();
private DotNetCoreAssemblyResolver _assemblyResolver = new DotNetCoreAssemblyResolver();
-
+ private ArchLoaderCacheConfig _cacheConfig = new ArchLoaderCacheConfig();
+ private readonly List _loadedAssemblyPaths = new List();
public Architecture Build()
{
- var architecture = _archBuilder.Build();
+ var architecture = _archBuilder.Build(_loadedAssemblyPaths, _cacheConfig);
_assemblyResolver.Dispose();
_assemblyResolver = new DotNetCoreAssemblyResolver();
return architecture;
}
+ ///
+ /// Configure caching behavior for this ArchLoader instance
+ ///
+ /// Cache configuration
+ /// This ArchLoader instance for fluent API
+ public ArchLoader WithCacheConfig(ArchLoaderCacheConfig config)
+ {
+ if (config == null)
+ {
+ throw new ArgumentNullException(nameof(config));
+ }
+ _cacheConfig = config.Clone();
+ return this;
+ }
+
+ ///
+ /// Disable caching for this ArchLoader instance
+ ///
+ /// This ArchLoader instance for fluent API
+ public ArchLoader WithoutCaching()
+ {
+ _cacheConfig = new ArchLoaderCacheConfig { CachingEnabled = false };
+ return this;
+ }
+
+ ///
+ /// Set a user-defined cache key for fine-grained cache control
+ ///
+ /// Custom cache key
+ /// This ArchLoader instance for fluent API
+ public ArchLoader WithUserCacheKey(string userCacheKey)
+ {
+ _cacheConfig.UserCacheKey = userCacheKey;
+ return this;
+ }
+
public ArchLoader LoadAssemblies(params Assembly[] assemblies)
{
var assemblySet = new HashSet(assemblies);
@@ -121,6 +158,15 @@ private void LoadModule(
{
try
{
+ if (!string.IsNullOrEmpty(fileName) && File.Exists(fileName))
+ {
+ var fullPath = Path.GetFullPath(fileName);
+ if (!_loadedAssemblyPaths.Contains(fullPath))
+ {
+ _loadedAssemblyPaths.Add(fullPath);
+ }
+ }
+
var module = ModuleDefinition.ReadModule(
fileName,
new ReaderParameters { AssemblyResolver = _assemblyResolver }
diff --git a/ArchUnitNETTests/Loader/ArchLoaderCacheTests.cs b/ArchUnitNETTests/Loader/ArchLoaderCacheTests.cs
new file mode 100644
index 00000000..aeeda886
--- /dev/null
+++ b/ArchUnitNETTests/Loader/ArchLoaderCacheTests.cs
@@ -0,0 +1,177 @@
+using ArchUnitNET.Domain;
+using ArchUnitNET.Loader;
+using System.Diagnostics;
+using Xunit;
+
+namespace ArchUnitNETTests.Loader
+{
+ public class ArchLoaderCacheTests
+ {
+ [Fact]
+ public void LoadingSameAssemblyTwiceReturnsCachedArchitecture()
+ {
+ // Arrange
+ var assembly = typeof(Architecture).Assembly;
+
+ // Act
+ var architecture1 = new ArchLoader().LoadAssemblies(assembly).Build();
+ var architecture2 = new ArchLoader().LoadAssemblies(assembly).Build();
+
+ // Assert - Should return the same cached instance
+ Assert.Same(architecture1, architecture2);
+ }
+
+ [Fact]
+ public void LoadingSameAssemblyTwiceIsFasterOnSecondLoad()
+ {
+ // Arrange
+ var assembly = typeof(Architecture).Assembly;
+ ArchitectureCache.Instance.Clear(); // Clear cache to ensure fair timing
+ ArchitectureCacheManager.Instance.Clear();
+
+ // Act
+ var sw1 = Stopwatch.StartNew();
+ var architecture1 = new ArchLoader().LoadAssemblies(assembly).Build();
+ sw1.Stop();
+
+ var sw2 = Stopwatch.StartNew();
+ var architecture2 = new ArchLoader().LoadAssemblies(assembly).Build();
+ sw2.Stop();
+
+ // Assert - Second load should be significantly faster (at least 50% faster)
+ Assert.True(
+ sw2.ElapsedMilliseconds < sw1.ElapsedMilliseconds * 0.5,
+ $"Expected second load ({sw2.ElapsedMilliseconds}ms) to be at least 50% faster than first load ({sw1.ElapsedMilliseconds}ms)"
+ );
+ Assert.Same(architecture1, architecture2);
+ }
+
+ [Fact]
+ public void LoadingMultipleAssembliesReturnsCachedArchitecture()
+ {
+ // Arrange - Use two truly different assemblies
+ var assembly1 = typeof(Architecture).Assembly; // ArchUnitNET
+ var assembly2 = typeof(ArchLoaderCacheTests).Assembly; // ArchUnitNETTests
+
+ // Act
+ var architecture1 = new ArchLoader()
+ .LoadAssemblies(assembly1, assembly2)
+ .Build();
+ var architecture2 = new ArchLoader()
+ .LoadAssemblies(assembly1, assembly2)
+ .Build();
+
+ // Assert
+ Assert.Same(architecture1, architecture2);
+ }
+
+ [Fact]
+ public void LoadingAssembliesInDifferentOrderReturnsSameCachedArchitecture()
+ {
+ // Arrange - Use two truly different assemblies
+ var assembly1 = typeof(Architecture).Assembly; // ArchUnitNET
+ var assembly2 = typeof(ArchLoaderCacheTests).Assembly; // ArchUnitNETTests
+
+ // Act - Load in different order
+ var architecture1 = new ArchLoader()
+ .LoadAssemblies(assembly1, assembly2)
+ .Build();
+ var architecture2 = new ArchLoader()
+ .LoadAssemblies(assembly2, assembly1)
+ .Build();
+
+ // Assert - Should return same cached instance regardless of order
+ Assert.Same(architecture1, architecture2);
+ }
+
+ [Fact]
+ public void LoadingDifferentAssembliesReturnsDifferentArchitectures()
+ {
+ // Arrange - Use two truly different assemblies
+ var assembly1 = typeof(Architecture).Assembly; // ArchUnitNET
+ var assembly2 = typeof(ArchLoaderCacheTests).Assembly; // ArchUnitNETTests
+
+ // Act
+ var architecture1 = new ArchLoader().LoadAssemblies(assembly1).Build();
+ var architecture2 = new ArchLoader().LoadAssemblies(assembly2).Build();
+
+ // Assert - Different assemblies should return different architectures
+ Assert.NotSame(architecture1, architecture2);
+ }
+
+ [Fact]
+ public void LoadingWithNamespaceFilterReturnsCachedArchitecture()
+ {
+ // Arrange
+ var assembly = typeof(Architecture).Assembly;
+ var namespaceName = "ArchUnitNET.Domain";
+
+ // Act
+ var architecture1 = new ArchLoader()
+ .LoadNamespacesWithinAssembly(assembly, namespaceName)
+ .Build();
+ var architecture2 = new ArchLoader()
+ .LoadNamespacesWithinAssembly(assembly, namespaceName)
+ .Build();
+
+ // Assert
+ Assert.Same(architecture1, architecture2);
+ }
+
+ [Fact]
+ public void LoadingDifferentNamespacesReturnsDifferentArchitectures()
+ {
+ // Arrange
+ var assembly = typeof(Architecture).Assembly;
+
+ // Act
+ var architecture1 = new ArchLoader()
+ .LoadNamespacesWithinAssembly(assembly, "ArchUnitNET.Domain")
+ .Build();
+ var architecture2 = new ArchLoader()
+ .LoadNamespacesWithinAssembly(assembly, "ArchUnitNET.Loader")
+ .Build();
+
+ // Assert - Different namespace filters should return different architectures
+ Assert.NotSame(architecture1, architecture2);
+ }
+
+ [Fact]
+ public void ClearingCacheForcesReloadOnNextBuild()
+ {
+ // Arrange
+ var assembly = typeof(Architecture).Assembly;
+ var architecture1 = new ArchLoader().LoadAssemblies(assembly).Build();
+
+ // Act
+ ArchitectureCache.Instance.Clear();
+ ArchitectureCacheManager.Instance.Clear();
+ var architecture2 = new ArchLoader().LoadAssemblies(assembly).Build();
+
+ // Assert - After clearing cache, should get a new instance
+ Assert.NotSame(architecture1, architecture2);
+
+ // But loading again should return cached version
+ var architecture3 = new ArchLoader().LoadAssemblies(assembly).Build();
+ Assert.Same(architecture2, architecture3);
+ }
+
+ [Fact]
+ public void LoadingAssembliesIncludingDependenciesUsesCaching()
+ {
+ // Arrange
+ var assembly = typeof(Architecture).Assembly;
+
+ // Act
+ var architecture1 = new ArchLoader()
+ .LoadAssembliesIncludingDependencies(assembly)
+ .Build();
+ var architecture2 = new ArchLoader()
+ .LoadAssembliesIncludingDependencies(assembly)
+ .Build();
+
+ // Assert
+ Assert.Same(architecture1, architecture2);
+ }
+ }
+}
diff --git a/ArchUnitNETTests/Loader/ArchLoaderTests.cs b/ArchUnitNETTests/Loader/ArchLoaderTests.cs
index 31aaacdc..95b92cd5 100644
--- a/ArchUnitNETTests/Loader/ArchLoaderTests.cs
+++ b/ArchUnitNETTests/Loader/ArchLoaderTests.cs
@@ -1,18 +1,16 @@
extern alias LoaderTestAssemblyAlias;
extern alias OtherLoaderTestAssemblyAlias;
-
-using System;
-using System.IO;
-using System.Linq;
using ArchUnitNET.Domain;
using ArchUnitNET.Domain.Extensions;
using ArchUnitNET.Loader;
using ArchUnitNET.xUnit;
using ArchUnitNETTests.Domain.Dependencies.Members;
+using System;
+using System.IO;
+using System.Linq;
using Xunit;
using static ArchUnitNET.Fluent.ArchRuleDefinition;
using static ArchUnitNETTests.StaticTestArchitectures;
-
using DuplicateClass = LoaderTestAssemblyAlias::DuplicateClassAcrossAssemblies.DuplicateClass;
using OtherDuplicateClass = OtherLoaderTestAssemblyAlias::DuplicateClassAcrossAssemblies.DuplicateClass;
@@ -154,5 +152,109 @@ public void UnavailableTypeTest()
Assert.NotNull(loggerType);
Assert.True(loggerType is UnavailableType);
}
+
+ [Fact]
+ public void WithoutCachingDisablesCaching()
+ {
+ // Arrange & Act
+ var architecture1 = new ArchLoader()
+ .WithoutCaching()
+ .LoadAssemblies(typeof(Architecture).Assembly)
+ .Build();
+
+ var architecture2 = new ArchLoader()
+ .WithoutCaching()
+ .LoadAssemblies(typeof(Architecture).Assembly)
+ .Build();
+
+ // Assert - Without caching, should get different instances
+ Assert.NotSame(architecture1, architecture2);
+ }
+
+ [Fact]
+ public void WithUserCacheKeySetsCustomCacheKey()
+ {
+ // Arrange
+ var assembly = typeof(Architecture).Assembly;
+
+ // Act
+ var architecture1 = new ArchLoader()
+ .WithUserCacheKey("key1")
+ .LoadAssemblies(assembly)
+ .Build();
+
+ var architecture2 = new ArchLoader()
+ .WithUserCacheKey("key1")
+ .LoadAssemblies(assembly)
+ .Build();
+
+ var architecture3 = new ArchLoader()
+ .WithUserCacheKey("key2")
+ .LoadAssemblies(assembly)
+ .Build();
+
+ // Assert - Same cache key should return same instance
+ Assert.Same(architecture1, architecture2);
+ // Different cache key should return different instance
+ Assert.NotSame(architecture1, architecture3);
+ }
+
+ [Fact]
+ public void FluentAPIChainingWorks()
+ {
+ // Arrange & Act
+ var architecture = new ArchLoader()
+ .WithUserCacheKey("test-chain")
+ .LoadAssemblies(typeof(Architecture).Assembly)
+ .Build();
+
+ // Assert
+ Assert.NotNull(architecture);
+ Assert.NotEmpty(architecture.Types);
+ }
+
+ [Fact]
+ public void WithCacheConfigThrowsOnNullConfig()
+ {
+ // Arrange
+ var loader = new ArchLoader();
+
+ // Act & Assert
+ Assert.Throws(() => loader.WithCacheConfig(null));
+ }
+
+ [Fact]
+ public void WithCacheConfigClonesConfigToPreventExternalMutation()
+ {
+ // Arrange
+ var config = new ArchLoaderCacheConfig
+ {
+ CachingEnabled = true,
+ UseFileBasedInvalidation = true,
+ UserCacheKey = "original-key"
+ };
+
+ var assembly = typeof(Architecture).Assembly;
+
+ // Act
+ var architecture1 = new ArchLoader()
+ .WithCacheConfig(config)
+ .LoadAssemblies(assembly)
+ .Build();
+
+ // Mutate the original config
+ config.UserCacheKey = "modified-key";
+ config.CachingEnabled = false;
+
+ // Build again with a new loader using the SAME config object
+ var architecture2 = new ArchLoader()
+ .WithCacheConfig(config)
+ .LoadAssemblies(assembly)
+ .Build();
+
+ // Assert - The first architecture should still be cached with original key
+ // and the second should use the modified key, so they should be different
+ Assert.NotSame(architecture1, architecture2);
+ }
}
}