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); + } } }