Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 52 additions & 0 deletions ArchUnitNET/Domain/ArchLoaderCacheConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
namespace ArchUnitNET.Domain
{
/// <summary>
/// Configuration options for the ArchLoader caching mechanism
/// </summary>
public sealed class ArchLoaderCacheConfig
{
/// <summary>
/// Creates a new instance with default settings (caching enabled, file-based invalidation enabled)
/// </summary>
public ArchLoaderCacheConfig()
{
}

/// <summary>
/// Gets or sets whether caching is enabled. Default is true.
/// </summary>
public bool CachingEnabled { get; set; } = true;

/// <summary>
/// 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.
/// </summary>
public bool UseFileBasedInvalidation { get; set; } = true;

/// <summary>
/// Gets or sets an optional user-provided cache key for fine-grained control.
/// When set, this key is included in the cache key computation.
/// </summary>
public string UserCacheKey { get; set; }

/// <summary>
/// Gets or sets whether to include the ArchUnitNET version in cache invalidation.
/// Default is true.
/// </summary>
public bool IncludeVersionInCacheKey { get; set; } = true;

/// <summary>
/// Creates a copy of this configuration
/// </summary>
public ArchLoaderCacheConfig Clone()
{
return new ArchLoaderCacheConfig
{
CachingEnabled = CachingEnabled,
UseFileBasedInvalidation = UseFileBasedInvalidation,
UserCacheKey = UserCacheKey,
IncludeVersionInCacheKey = IncludeVersionInCacheKey
};
}
}
}
215 changes: 215 additions & 0 deletions ArchUnitNET/Domain/ArchitectureCacheManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,215 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;

namespace ArchUnitNET.Domain
{
/// <summary>
/// Enhanced caching manager for Architecture instances with automatic invalidation support
/// </summary>
public class ArchitectureCacheManager
{
private readonly ConcurrentDictionary<EnhancedCacheKey, CachedArchitecture> _cache =
new ConcurrentDictionary<EnhancedCacheKey, CachedArchitecture>();

private static readonly Lazy<ArchitectureCacheManager> _instance =
new Lazy<ArchitectureCacheManager>(() => new ArchitectureCacheManager());

private static readonly string ArchUnitNetVersion =
typeof(Architecture).Assembly.GetName().Version?.ToString() ?? "unknown";

protected ArchitectureCacheManager() { }

public static ArchitectureCacheManager Instance => _instance.Value;

/// <summary>
/// Try to get a cached architecture. Returns null if not found or if cache is invalid.
/// </summary>
public Architecture TryGetArchitecture(
ArchitectureCacheKey baseCacheKey,
IEnumerable<AssemblyMetadata> 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;
}

/// <summary>
/// Add an architecture to the cache
/// </summary>
public bool Add(
ArchitectureCacheKey baseCacheKey,
Architecture architecture,
IEnumerable<AssemblyMetadata> 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);
}

/// <summary>
/// Clear all cached architectures
/// </summary>
public void Clear() => _cache.Clear();

/// <summary>
/// Get the number of cached architectures
/// </summary>
public int Count => _cache.Count;

private static bool AreAssembliesUnchanged(
List<AssemblyMetadata> cached,
List<AssemblyMetadata> 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> AssemblyMetadata { get; set; }
public DateTime CachedAt { get; set; }
}

private class EnhancedCacheKey : IEquatable<EnhancedCacheKey>
{
private readonly ArchitectureCacheKey _baseCacheKey;
private readonly List<AssemblyMetadata> _assemblyMetadata;
private readonly string _userCacheKey;
private readonly string _version;
private readonly int _hashCode;

public EnhancedCacheKey(
ArchitectureCacheKey baseCacheKey,
IEnumerable<AssemblyMetadata> 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;
}
}
}
82 changes: 82 additions & 0 deletions ArchUnitNET/Domain/AssemblyMetadata.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
using System;
using System.IO;
using System.Security.Cryptography;

namespace ArchUnitNET.Domain
{
/// <summary>
/// Tracks metadata for an assembly file to detect changes
/// </summary>
public sealed class AssemblyMetadata : IEquatable<AssemblyMetadata>
{
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})";
}
}
}
Loading