Skip to content

Commit 47b7c10

Browse files
thomhurstclaude
andauthored
perf: Add caching for Hasher hash algorithms and Azure KeyVault clients (#1685)
* perf: add caching for Hasher hash algorithms and Azure KeyVault clients 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> * fix: Address review feedback for caching improvements - 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> * fix: Key Azure clients by vault URI only with documentation 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> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 911632e commit 47b7c10

2 files changed

Lines changed: 47 additions & 18 deletions

File tree

Lines changed: 30 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,24 +1,51 @@
1+
using System.Collections.Concurrent;
12
using Azure.Core;
23
using Azure.Security.KeyVault.Certificates;
34
using Azure.Security.KeyVault.Keys;
45
using Azure.Security.KeyVault.Secrets;
56

67
namespace ModularPipelines.Azure;
78

9+
/// <summary>
10+
/// Provides access to Azure Key Vault clients with caching.
11+
/// </summary>
12+
/// <remarks>
13+
/// <para>
14+
/// Azure SDK clients (SecretClient, CertificateClient, KeyClient) are thread-safe and designed
15+
/// to be long-lived and reused. This class caches clients by vault URI to avoid the overhead
16+
/// of creating new clients on every call.
17+
/// </para>
18+
/// <para>
19+
/// <strong>Important:</strong> Clients are cached by vault URI only. Within a scope, the first
20+
/// TokenCredential used for a vault URI will be used for all subsequent requests to that vault.
21+
/// This is intentional since AzureKeyVault is registered as Scoped and typically the same
22+
/// credential is used throughout a pipeline execution scope.
23+
/// </para>
24+
/// </remarks>
825
internal class AzureKeyVault : IAzureKeyVault
926
{
27+
// Cache by vault URI only. Within a scope, the same credential is typically used.
28+
// TokenCredential lacks value equality, so including it in the key would cause cache misses
29+
// for logically identical credentials (e.g., two DefaultAzureCredential instances).
30+
private readonly ConcurrentDictionary<string, SecretClient> _secretClients = new();
31+
private readonly ConcurrentDictionary<string, CertificateClient> _certificateClients = new();
32+
private readonly ConcurrentDictionary<string, KeyClient> _keyClients = new();
33+
1034
public SecretClient GetSecretClient(Uri vaultUri, TokenCredential tokenCredential)
1135
{
12-
return new SecretClient(vaultUri, tokenCredential);
36+
var key = vaultUri.ToString();
37+
return _secretClients.GetOrAdd(key, _ => new SecretClient(vaultUri, tokenCredential));
1338
}
1439

1540
public CertificateClient GetCertificateClient(Uri vaultUri, TokenCredential tokenCredential)
1641
{
17-
return new CertificateClient(vaultUri, tokenCredential);
42+
var key = vaultUri.ToString();
43+
return _certificateClients.GetOrAdd(key, _ => new CertificateClient(vaultUri, tokenCredential));
1844
}
1945

2046
public KeyClient GetKeyClient(Uri vaultUri, TokenCredential tokenCredential)
2147
{
22-
return new KeyClient(vaultUri, tokenCredential);
48+
var key = vaultUri.ToString();
49+
return _keyClients.GetOrAdd(key, _ => new KeyClient(vaultUri, tokenCredential));
2350
}
2451
}

src/ModularPipelines/Context/Hasher.cs

Lines changed: 17 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,13 @@
33

44
namespace ModularPipelines.Context;
55

6+
/// <summary>
7+
/// Provides hashing operations using various algorithms.
8+
/// </summary>
9+
/// <remarks>
10+
/// Uses the static HashData methods available in .NET 5+ which are thread-safe
11+
/// and don't require disposal, avoiding resource leaks.
12+
/// </remarks>
613
internal class Hasher : IHasher
714
{
815
private readonly IHex _hex;
@@ -16,36 +23,31 @@ public Hasher(IHex hex, IBase64 base64)
1623

1724
public string Sha1(string input, HashType hashType = HashType.Hex)
1825
{
19-
return ComputeHash(SHA1.Create(), input, hashType);
26+
var bytes = System.Security.Cryptography.SHA1.HashData(Encoding.UTF8.GetBytes(input));
27+
return hashType == HashType.Hex ? _hex.ToHex(bytes) : _base64.ToBase64String(bytes);
2028
}
2129

2230
public string Sha256(string input, HashType hashType = HashType.Hex)
2331
{
24-
return ComputeHash(SHA256.Create(), input, hashType);
32+
var bytes = System.Security.Cryptography.SHA256.HashData(Encoding.UTF8.GetBytes(input));
33+
return hashType == HashType.Hex ? _hex.ToHex(bytes) : _base64.ToBase64String(bytes);
2534
}
2635

2736
public string Sha384(string input, HashType hashType = HashType.Hex)
2837
{
29-
return ComputeHash(SHA384.Create(), input, hashType);
38+
var bytes = System.Security.Cryptography.SHA384.HashData(Encoding.UTF8.GetBytes(input));
39+
return hashType == HashType.Hex ? _hex.ToHex(bytes) : _base64.ToBase64String(bytes);
3040
}
3141

3242
public string Sha512(string input, HashType hashType = HashType.Hex)
3343
{
34-
return ComputeHash(SHA512.Create(), input, hashType);
44+
var bytes = System.Security.Cryptography.SHA512.HashData(Encoding.UTF8.GetBytes(input));
45+
return hashType == HashType.Hex ? _hex.ToHex(bytes) : _base64.ToBase64String(bytes);
3546
}
3647

3748
public string Md5(string input, HashType hashType = HashType.Hex)
3849
{
39-
return ComputeHash(MD5.Create(), input, hashType);
40-
}
41-
42-
private string ComputeHash(HashAlgorithm hashAlgorithm, string input, HashType hashType)
43-
{
44-
using (hashAlgorithm)
45-
{
46-
var bytes = hashAlgorithm.ComputeHash(Encoding.UTF8.GetBytes(input));
47-
48-
return hashType == HashType.Hex ? _hex.ToHex(bytes) : _base64.ToBase64String(bytes);
49-
}
50+
var bytes = System.Security.Cryptography.MD5.HashData(Encoding.UTF8.GetBytes(input));
51+
return hashType == HashType.Hex ? _hex.ToHex(bytes) : _base64.ToBase64String(bytes);
5052
}
5153
}

0 commit comments

Comments
 (0)