perf: Add caching for Hasher hash algorithms and Azure KeyVault clients#1685
Conversation
Fixes #1594 and #1596 - Use ThreadLocal<T> for hash algorithm instances in Hasher class since HashAlgorithm is not thread-safe but creating new instances per call is wasteful - Cache Azure KeyVault clients (SecretClient, CertificateClient, KeyClient) in ConcurrentDictionary by vault URI since Azure SDK clients are thread-safe and designed for reuse 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
SummaryAdds ThreadLocal caching for hash algorithm instances and ConcurrentDictionary caching for Azure KeyVault clients to improve performance. Critical Issues1. Resource Leak in Hasher.csThe ThreadLocal instances and their underlying HashAlgorithm objects are never disposed, causing a resource leak. Problem:
Better Fix: Use SHA256.HashData() static methods available in .NET 5+ which don't require disposal 2. Incorrect Cache Key in AzureKeyVault.csThe cache key only includes the vault URI but ignores the TokenCredential parameter. This causes incorrect behavior when the same vault is accessed with different credentials. Problem: If GetSecretClient(uri, credential1) is called, then GetSecretClient(uri, credential2) is called, the second call will return the client created with credential1, not credential2. Fix: Include the credential in the cache key using a composite key: (vaultUri.ToString(), tokenCredential) Verdict |
- Hasher: Use static HashData methods (.NET 5+) instead of ThreadLocal to avoid resource leaks from undisposed HashAlgorithm instances - AzureKeyVault: Use composite key (vaultUri, tokenCredential) to ensure different credentials for the same vault return different clients 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
SummaryThis PR improves performance by eliminating repeated object creation in two areas: modernizing hash computation to use static HashData methods and caching Azure KeyVault SDK clients. Critical Issues1. TokenCredential reference equality breaks cache effectiveness (src/ModularPipelines.Azure/AzureKeyVault.cs:18-20)The composite key Example scenario: var cred1 = new DefaultAzureCredential();
var cred2 = new DefaultAzureCredential();
var client1 = keyVault.GetSecretClient(uri, cred1); // Creates new client
var client2 = keyVault.GetSecretClient(uri, cred2); // Creates ANOTHER new client (cache miss)Impact: The caching won't work as intended when different Suggested fix: Either:
Suggestions1. PR description mentions ThreadLocal but code uses HashDataThe PR description states "Adds Suggested fix: Update PR description to reflect the actual implementation. 2. Consider documenting the scoped lifetime limitation (src/ModularPipelines.Azure/AzureKeyVault.cs:9-18)Since This is actually fine for resource management (clients are cleaned up when scope ends), but the limited cache lifetime reduces the performance benefit. Suggested approach: Either accept this trade-off or add a code comment explaining the scoped caching behavior. Verdict |
TokenCredential lacks value equality, so including it in the cache key causes cache misses for logically identical credentials. Since this service is Scoped, the same credential is typically used throughout a pipeline execution. Added documentation explaining this behavior. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
SummaryAdds client caching to AzureKeyVault and modernizes Hasher implementation to use static HashData methods. Critical IssuesNone found ✅ SuggestionsAzureKeyVault.cs - Credential Change BehaviorThe caching implementation is correct for typical pipeline scenarios, but the documentation could be more explicit about one edge case: Current behavior: Once a vault URI is cached, subsequent calls with different TokenCredentials for the same vault will reuse the first credential. Example scenario: // First call - credential1 is cached
var client1 = azureKeyVault.GetSecretClient(vaultUri, credential1);
// Second call - still uses credential1, not credential2!
var client2 = azureKeyVault.GetSecretClient(vaultUri, credential2);This is intentional and safe because:
Optional improvement: Consider adding a log warning if a different credential instance is passed for an already-cached vault URI. This would make the behavior more discoverable during debugging without adding runtime overhead. Example: return _secretClients.GetOrAdd(key, _ =>
{
_logger?.LogDebug("Creating new SecretClient for vault {VaultUri}", vaultUri);
return new SecretClient(vaultUri, tokenCredential);
});This is purely optional - the current implementation is correct and well-documented. Hasher.cs - Looks PerfectThe migration to static
Verdict✅ APPROVE - No critical issues Both changes are solid improvements. The caching pattern in AzureKeyVault is appropriate for the scoped lifetime, and the Hasher modernization follows .NET best practices perfectly. |
Summary
HashData()methods (.NET 5+) inHasher.csfor thread-safe hashing without disposal concernsConcurrentDictionarycaching for Azure KeyVaultSecretClient,CertificateClient, andKeyClientinstancesNote: Azure clients are cached by vault URI only. Since
AzureKeyVaultis registered as Scoped, the same credential is typically used throughout a pipeline execution scope. This avoids cache misses fromTokenCredential's lack of value equality.Fixes #1596, #1594
Test plan
🤖 Generated with Claude Code