Skip to content

Commit f7080a4

Browse files
committed
Add enhanced caching mechanism for ArchLoader builds
Introduced a new caching system via `ArchitectureCacheManager` to improve performance and efficiency of `ArchLoader` builds. The caching mechanism supports file-based invalidation, user-defined cache keys, and version-based cache invalidation. Key changes: - Added `ArchitectureCacheManager` for centralized caching. - Introduced `ArchLoaderCacheConfig` for configurable caching options. - Added `AssemblyMetadata` to track assembly file changes. - Enhanced `ArchBuilder` with a new `Build` method supporting caching. - Improved `ArchLoader` with fluent API methods for caching configuration. - Added extensive unit tests to validate caching behavior and performance. Refactored code to integrate caching seamlessly while maintaining backward compatibility. Improved performance for repeated builds of the same assemblies. Signed-off-by: mokarchi <[email protected]>
1 parent 51d6e8c commit f7080a4

File tree

7 files changed

+748
-13
lines changed

7 files changed

+748
-13
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
namespace ArchUnitNET.Domain
2+
{
3+
/// <summary>
4+
/// Configuration options for the ArchLoader caching mechanism
5+
/// </summary>
6+
public sealed class ArchLoaderCacheConfig
7+
{
8+
/// <summary>
9+
/// Creates a new instance with default settings (caching enabled, file-based invalidation enabled)
10+
/// </summary>
11+
public ArchLoaderCacheConfig()
12+
{
13+
}
14+
15+
/// <summary>
16+
/// Gets or sets whether caching is enabled. Default is true.
17+
/// </summary>
18+
public bool CachingEnabled { get; set; } = true;
19+
20+
/// <summary>
21+
/// Gets or sets whether to use file-based invalidation (hash + timestamp + size checking).
22+
/// Default is true. When false, only module names are used for cache keys.
23+
/// </summary>
24+
public bool UseFileBasedInvalidation { get; set; } = true;
25+
26+
/// <summary>
27+
/// Gets or sets an optional user-provided cache key for fine-grained control.
28+
/// When set, this key is included in the cache key computation.
29+
/// </summary>
30+
public string UserCacheKey { get; set; }
31+
32+
/// <summary>
33+
/// Gets or sets whether to include the ArchUnitNET version in cache invalidation.
34+
/// Default is true.
35+
/// </summary>
36+
public bool IncludeVersionInCacheKey { get; set; } = true;
37+
38+
/// <summary>
39+
/// Creates a copy of this configuration
40+
/// </summary>
41+
public ArchLoaderCacheConfig Clone()
42+
{
43+
return new ArchLoaderCacheConfig
44+
{
45+
CachingEnabled = CachingEnabled,
46+
UseFileBasedInvalidation = UseFileBasedInvalidation,
47+
UserCacheKey = UserCacheKey,
48+
IncludeVersionInCacheKey = IncludeVersionInCacheKey
49+
};
50+
}
51+
}
52+
}
Lines changed: 215 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,215 @@
1+
using System;
2+
using System.Collections.Concurrent;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
6+
namespace ArchUnitNET.Domain
7+
{
8+
/// <summary>
9+
/// Enhanced caching manager for Architecture instances with automatic invalidation support
10+
/// </summary>
11+
public class ArchitectureCacheManager
12+
{
13+
private readonly ConcurrentDictionary<EnhancedCacheKey, CachedArchitecture> _cache =
14+
new ConcurrentDictionary<EnhancedCacheKey, CachedArchitecture>();
15+
16+
private static readonly Lazy<ArchitectureCacheManager> _instance =
17+
new Lazy<ArchitectureCacheManager>(() => new ArchitectureCacheManager());
18+
19+
private static readonly string ArchUnitNetVersion =
20+
typeof(Architecture).Assembly.GetName().Version?.ToString() ?? "unknown";
21+
22+
protected ArchitectureCacheManager() { }
23+
24+
public static ArchitectureCacheManager Instance => _instance.Value;
25+
26+
/// <summary>
27+
/// Try to get a cached architecture. Returns null if not found or if cache is invalid.
28+
/// </summary>
29+
public Architecture TryGetArchitecture(
30+
ArchitectureCacheKey baseCacheKey,
31+
IEnumerable<AssemblyMetadata> assemblyMetadata,
32+
ArchLoaderCacheConfig config)
33+
{
34+
if (config == null || !config.CachingEnabled)
35+
{
36+
return null;
37+
}
38+
39+
var assemblyMetadatas = assemblyMetadata as AssemblyMetadata[] ?? assemblyMetadata.ToArray();
40+
var enhancedKey = new EnhancedCacheKey(
41+
baseCacheKey,
42+
config.UseFileBasedInvalidation ? assemblyMetadatas : null,
43+
config.UserCacheKey,
44+
config.IncludeVersionInCacheKey ? ArchUnitNetVersion : null
45+
);
46+
47+
if (_cache.TryGetValue(enhancedKey, out var cached))
48+
{
49+
if (config.UseFileBasedInvalidation && cached.AssemblyMetadata != null)
50+
{
51+
var currentMetadata = assemblyMetadatas?.ToList();
52+
if (!AreAssembliesUnchanged(cached.AssemblyMetadata, currentMetadata))
53+
{
54+
_cache.TryRemove(enhancedKey, out _);
55+
return null;
56+
}
57+
}
58+
59+
return cached.Architecture;
60+
}
61+
62+
return null;
63+
}
64+
65+
/// <summary>
66+
/// Add an architecture to the cache
67+
/// </summary>
68+
public bool Add(
69+
ArchitectureCacheKey baseCacheKey,
70+
Architecture architecture,
71+
IEnumerable<AssemblyMetadata> assemblyMetadata,
72+
ArchLoaderCacheConfig config)
73+
{
74+
if (config == null || !config.CachingEnabled)
75+
{
76+
return false;
77+
}
78+
79+
var assemblyMetadatas = assemblyMetadata as AssemblyMetadata[] ?? assemblyMetadata.ToArray();
80+
var enhancedKey = new EnhancedCacheKey(
81+
baseCacheKey,
82+
config.UseFileBasedInvalidation ? assemblyMetadatas : null,
83+
config.UserCacheKey,
84+
config.IncludeVersionInCacheKey ? ArchUnitNetVersion : null
85+
);
86+
87+
var cached = new CachedArchitecture
88+
{
89+
Architecture = architecture,
90+
AssemblyMetadata = config.UseFileBasedInvalidation
91+
? assemblyMetadatas?.ToList()
92+
: null,
93+
CachedAt = DateTime.UtcNow
94+
};
95+
96+
return _cache.TryAdd(enhancedKey, cached);
97+
}
98+
99+
/// <summary>
100+
/// Clear all cached architectures
101+
/// </summary>
102+
public void Clear() => _cache.Clear();
103+
104+
/// <summary>
105+
/// Get the number of cached architectures
106+
/// </summary>
107+
public int Count => _cache.Count;
108+
109+
private static bool AreAssembliesUnchanged(
110+
List<AssemblyMetadata> cached,
111+
List<AssemblyMetadata> current)
112+
{
113+
if (cached == null || current == null)
114+
return cached == current;
115+
116+
if (cached.Count != current.Count)
117+
return false;
118+
119+
var cachedDict = cached.ToDictionary(m => m.FilePath, StringComparer.OrdinalIgnoreCase);
120+
121+
foreach (var currentMeta in current)
122+
{
123+
if (!cachedDict.TryGetValue(currentMeta.FilePath, out var cachedMeta))
124+
return false;
125+
126+
if (!cachedMeta.Equals(currentMeta))
127+
return false;
128+
}
129+
130+
return true;
131+
}
132+
133+
private class CachedArchitecture
134+
{
135+
public Architecture Architecture { get; set; }
136+
public List<AssemblyMetadata> AssemblyMetadata { get; set; }
137+
public DateTime CachedAt { get; set; }
138+
}
139+
140+
private class EnhancedCacheKey : IEquatable<EnhancedCacheKey>
141+
{
142+
private readonly ArchitectureCacheKey _baseCacheKey;
143+
private readonly List<AssemblyMetadata> _assemblyMetadata;
144+
private readonly string _userCacheKey;
145+
private readonly string _version;
146+
private readonly int _hashCode;
147+
148+
public EnhancedCacheKey(
149+
ArchitectureCacheKey baseCacheKey,
150+
IEnumerable<AssemblyMetadata> assemblyMetadata,
151+
string userCacheKey,
152+
string version)
153+
{
154+
_baseCacheKey = baseCacheKey ?? throw new ArgumentNullException(nameof(baseCacheKey));
155+
_assemblyMetadata = assemblyMetadata?.OrderBy(m => m.FilePath, StringComparer.OrdinalIgnoreCase).ToList();
156+
_userCacheKey = userCacheKey;
157+
_version = version;
158+
_hashCode = ComputeHashCode();
159+
}
160+
161+
private int ComputeHashCode()
162+
{
163+
unchecked
164+
{
165+
var hash = _baseCacheKey.GetHashCode();
166+
hash = (hash * 397) ^ (_userCacheKey?.GetHashCode() ?? 0);
167+
hash = (hash * 397) ^ (_version?.GetHashCode() ?? 0);
168+
169+
if (_assemblyMetadata != null)
170+
{
171+
foreach (var metadata in _assemblyMetadata)
172+
{
173+
hash = (hash * 397) ^ metadata.GetHashCode();
174+
}
175+
}
176+
177+
return hash;
178+
}
179+
}
180+
181+
public bool Equals(EnhancedCacheKey other)
182+
{
183+
if (ReferenceEquals(null, other)) return false;
184+
if (ReferenceEquals(this, other)) return true;
185+
186+
if (!_baseCacheKey.Equals(other._baseCacheKey))
187+
return false;
188+
189+
if (_userCacheKey != other._userCacheKey)
190+
return false;
191+
192+
if (_version != other._version)
193+
return false;
194+
195+
if (_assemblyMetadata == null && other._assemblyMetadata == null)
196+
return true;
197+
198+
if (_assemblyMetadata == null || other._assemblyMetadata == null)
199+
return false;
200+
201+
if (_assemblyMetadata.Count != other._assemblyMetadata.Count)
202+
return false;
203+
204+
return _assemblyMetadata.SequenceEqual(other._assemblyMetadata);
205+
}
206+
207+
public override bool Equals(object obj)
208+
{
209+
return obj is EnhancedCacheKey other && Equals(other);
210+
}
211+
212+
public override int GetHashCode() => _hashCode;
213+
}
214+
}
215+
}
Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
using System;
2+
using System.IO;
3+
using System.Security.Cryptography;
4+
5+
namespace ArchUnitNET.Domain
6+
{
7+
/// <summary>
8+
/// Tracks metadata for an assembly file to detect changes
9+
/// </summary>
10+
public sealed class AssemblyMetadata : IEquatable<AssemblyMetadata>
11+
{
12+
public string FilePath { get; }
13+
public string FileHash { get; }
14+
public DateTime LastWriteTimeUtc { get; }
15+
public long FileSize { get; }
16+
17+
public AssemblyMetadata(string filePath)
18+
{
19+
if (string.IsNullOrEmpty(filePath))
20+
{
21+
throw new ArgumentException("File path cannot be null or empty", nameof(filePath));
22+
}
23+
24+
if (!File.Exists(filePath))
25+
{
26+
throw new FileNotFoundException($"Assembly file not found: {filePath}", filePath);
27+
}
28+
29+
FilePath = Path.GetFullPath(filePath);
30+
var fileInfo = new FileInfo(FilePath);
31+
LastWriteTimeUtc = fileInfo.LastWriteTimeUtc;
32+
FileSize = fileInfo.Length;
33+
FileHash = ComputeFileHash(FilePath);
34+
}
35+
36+
private static string ComputeFileHash(string filePath)
37+
{
38+
using (var sha256 = SHA256.Create())
39+
using (var stream = File.OpenRead(filePath))
40+
{
41+
var hash = sha256.ComputeHash(stream);
42+
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
43+
}
44+
}
45+
46+
public bool Equals(AssemblyMetadata other)
47+
{
48+
if (ReferenceEquals(null, other)) return false;
49+
if (ReferenceEquals(this, other)) return true;
50+
51+
return string.Equals(FilePath, other.FilePath, StringComparison.OrdinalIgnoreCase)
52+
&& FileHash == other.FileHash
53+
&& LastWriteTimeUtc.Equals(other.LastWriteTimeUtc)
54+
&& FileSize == other.FileSize;
55+
}
56+
57+
public override bool Equals(object obj)
58+
{
59+
if (ReferenceEquals(null, obj)) return false;
60+
if (ReferenceEquals(this, obj)) return true;
61+
if (obj.GetType() != GetType()) return false;
62+
return Equals((AssemblyMetadata)obj);
63+
}
64+
65+
public override int GetHashCode()
66+
{
67+
unchecked
68+
{
69+
var hashCode = StringComparer.OrdinalIgnoreCase.GetHashCode(FilePath ?? string.Empty);
70+
hashCode = (hashCode * 397) ^ (FileHash?.GetHashCode() ?? 0);
71+
hashCode = (hashCode * 397) ^ LastWriteTimeUtc.GetHashCode();
72+
hashCode = (hashCode * 397) ^ FileSize.GetHashCode();
73+
return hashCode;
74+
}
75+
}
76+
77+
public override string ToString()
78+
{
79+
return $"AssemblyMetadata(Path={FilePath}, Hash={FileHash?.Substring(0, 8)}..., Size={FileSize}, Modified={LastWriteTimeUtc})";
80+
}
81+
}
82+
}

0 commit comments

Comments
 (0)