Skip to content

Commit a37fd38

Browse files
Refactor CredentialCache to use a dedicated data model and enhance persistence management
1 parent 267a788 commit a37fd38

1 file changed

Lines changed: 136 additions & 12 deletions

File tree

CredentialCache/CredentialCache.cs

Lines changed: 136 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -7,40 +7,100 @@ namespace ktsu.CredentialCache;
77
using System.Collections.Concurrent;
88
using System.Text.Json.Serialization;
99

10-
using ktsu.AppDataStorage;
1110
using ktsu.Extensions;
11+
using ktsu.FileSystemProvider;
12+
using ktsu.PersistenceProvider;
1213
using ktsu.StrongStrings;
14+
using ktsu.UniversalSerializer.Json;
15+
using ktsu.UniversalSerializer.Services;
1316

1417
/// <summary>
1518
/// Represents a globally unique identifier for a persona.
1619
/// </summary>
1720
public sealed record class PersonaGUID : StrongStringAbstract<PersonaGUID> { }
1821

22+
/// <summary>
23+
/// Data model for credential cache persistence.
24+
/// </summary>
25+
internal sealed class CredentialCacheData
26+
{
27+
/// <summary>
28+
/// Gets or sets the dictionary of credentials.
29+
/// </summary>
30+
[JsonInclude]
31+
public ConcurrentDictionary<PersonaGUID, Credential> Credentials { get; set; } = new();
32+
}
33+
1934
/// <summary>
2035
/// Manages the caching of credentials and their associated factories.
2136
/// </summary>
22-
public class CredentialCache : AppData<CredentialCache>
37+
public sealed class CredentialCache : IDisposable
2338
{
39+
private const string CacheKey = "CredentialCache";
40+
private static readonly object _lock = new();
41+
private static CredentialCache? _instance;
42+
private static IPersistenceProvider<string>? _persistenceProvider;
43+
2444
/// <summary>
2545
/// Gets the dictionary of credential factories.
2646
/// </summary>
27-
private ConcurrentDictionary<Type, ICredentialFactory> CredentialFactories { get; init; } = new();
47+
private ConcurrentDictionary<Type, ICredentialFactory> CredentialFactories { get; } = new();
2848

2949
/// <summary>
30-
/// Gets the dictionary of credentials.
50+
/// Gets the underlying data model.
3151
/// </summary>
32-
[JsonInclude]
33-
private ConcurrentDictionary<PersonaGUID, Credential> Credentials { get; init; } = new();
52+
private CredentialCacheData Data { get; set; }
53+
54+
/// <summary>
55+
/// Gets the persistence provider used for storage operations.
56+
/// </summary>
57+
private IPersistenceProvider<string> PersistenceProvider { get; }
58+
59+
/// <summary>
60+
/// Initializes a new instance of the <see cref="CredentialCache"/> class.
61+
/// </summary>
62+
/// <param name="persistenceProvider">The persistence provider for storage operations.</param>
63+
private CredentialCache(IPersistenceProvider<string> persistenceProvider)
64+
{
65+
PersistenceProvider = persistenceProvider ?? throw new ArgumentNullException(nameof(persistenceProvider));
66+
Data = LoadOrCreateData().GetAwaiter().GetResult();
67+
}
3468

3569
/// <summary>
3670
/// Gets the singleton instance of the <see cref="CredentialCache"/> class.
3771
/// </summary>
38-
private static Lazy<CredentialCache> LazyInstance { get; } = new(LoadOrCreate);
72+
public static CredentialCache Instance
73+
{
74+
get
75+
{
76+
lock (_lock)
77+
{
78+
if (_instance is null)
79+
{
80+
_persistenceProvider ??= CreateDefaultPersistenceProvider();
81+
_instance = new CredentialCache(_persistenceProvider);
82+
}
83+
return _instance;
84+
}
85+
}
86+
}
3987

4088
/// <summary>
41-
/// Retrieves the singleton instance of the <see cref="CredentialCache"/> class.
89+
/// Configures the persistence provider for the credential cache.
90+
/// This must be called before accessing the Instance property if you want to use a custom provider.
4291
/// </summary>
43-
public static CredentialCache Instance => LazyInstance.Value;
92+
/// <param name="persistenceProvider">The persistence provider to use.</param>
93+
public static void ConfigurePersistenceProvider(IPersistenceProvider<string> persistenceProvider)
94+
{
95+
lock (_lock)
96+
{
97+
if (_instance is not null)
98+
{
99+
throw new InvalidOperationException("Cannot configure persistence provider after instance has been created. Call this method before accessing Instance.");
100+
}
101+
_persistenceProvider = persistenceProvider ?? throw new ArgumentNullException(nameof(persistenceProvider));
102+
}
103+
}
44104

45105
/// <summary>
46106
/// Tries to get a credential associated with the specified persona GUID.
@@ -49,18 +109,30 @@ public class CredentialCache : AppData<CredentialCache>
49109
/// <param name="gitCredential">When this method returns, contains the credential associated with the specified GUID, if the GUID is found; otherwise, null.</param>
50110
/// <returns><c>true</c> if the credential was found; otherwise, <c>false</c>.</returns>
51111
public bool TryGet(PersonaGUID providerGuid, out Credential? gitCredential) =>
52-
Credentials.TryGetValue(providerGuid, out gitCredential);
112+
Data.Credentials.TryGetValue(providerGuid, out gitCredential);
53113

54114
/// <summary>
55115
/// Adds or replaces a credential associated with the specified persona GUID.
56116
/// </summary>
57117
/// <param name="providerGuid">The GUID of the persona.</param>
58118
/// <param name="gitCredential">The credential to add or replace.</param>
59119
/// <exception cref="ArgumentNullException">Thrown when <paramref name="gitCredential"/> is null.</exception>
60-
public void AddOrReplace(PersonaGUID providerGuid, Credential gitCredential)
120+
public async Task AddOrReplaceAsync(PersonaGUID providerGuid, Credential gitCredential, CancellationToken cancellationToken = default)
61121
{
62122
ArgumentNullException.ThrowIfNull(gitCredential);
63-
Credentials[providerGuid] = gitCredential;
123+
Data.Credentials[providerGuid] = gitCredential;
124+
await SaveAsync(cancellationToken).ConfigureAwait(false);
125+
}
126+
127+
/// <summary>
128+
/// Adds or replaces a credential associated with the specified persona GUID synchronously.
129+
/// </summary>
130+
/// <param name="providerGuid">The GUID of the persona.</param>
131+
/// <param name="gitCredential">The credential to add or replace.</param>
132+
/// <exception cref="ArgumentNullException">Thrown when <paramref name="gitCredential"/> is null.</exception>
133+
public void AddOrReplace(PersonaGUID providerGuid, Credential gitCredential)
134+
{
135+
AddOrReplaceAsync(providerGuid, gitCredential).GetAwaiter().GetResult();
64136
}
65137

66138
/// <summary>
@@ -108,5 +180,57 @@ public void RegisterCredentialFactory<T>(ICredentialFactory<T> factory) where T
108180
/// <typeparam name="T">The type of the credential.</typeparam>
109181
public void UnregisterCredentialFactory<T>() where T : Credential =>
110182
CredentialFactories.TryRemove(typeof(T), out _);
183+
184+
/// <summary>
185+
/// Saves the current credential cache data to persistent storage.
186+
/// </summary>
187+
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
188+
/// <returns>A task that represents the asynchronous save operation.</returns>
189+
public async Task SaveAsync(CancellationToken cancellationToken = default)
190+
{
191+
await PersistenceProvider.StoreAsync(CacheKey, Data, cancellationToken).ConfigureAwait(false);
192+
}
193+
194+
/// <summary>
195+
/// Saves the current credential cache data to persistent storage synchronously.
196+
/// </summary>
197+
public void Save()
198+
{
199+
SaveAsync().GetAwaiter().GetResult();
200+
}
201+
202+
/// <summary>
203+
/// Loads or creates the credential cache data from persistent storage.
204+
/// </summary>
205+
/// <param name="cancellationToken">A cancellation token to cancel the operation.</param>
206+
/// <returns>The loaded or newly created credential cache data.</returns>
207+
private async Task<CredentialCacheData> LoadOrCreateData(CancellationToken cancellationToken = default)
208+
{
209+
return await PersistenceProvider.RetrieveOrCreateAsync<CredentialCacheData>(CacheKey, cancellationToken).ConfigureAwait(false);
210+
}
211+
212+
/// <summary>
213+
/// Creates the default persistence provider for the credential cache.
214+
/// </summary>
215+
/// <returns>A new instance of the default persistence provider.</returns>
216+
private static IPersistenceProvider<string> CreateDefaultPersistenceProvider()
217+
{
218+
var fileSystemProvider = new FileSystemProvider();
219+
var jsonSerializer = new JsonSerializer();
220+
var serializationProvider = new UniversalSerializationProvider(jsonSerializer, providerName: "CredentialCache.Json");
221+
return new AppDataPersistenceProvider<string>(
222+
fileSystemProvider,
223+
serializationProvider,
224+
applicationName: "ktsu",
225+
subdirectory: "CredentialCache");
226+
}
227+
228+
/// <summary>
229+
/// Disposes the credential cache instance and saves any pending changes.
230+
/// </summary>
231+
public void Dispose()
232+
{
233+
Save();
234+
}
111235
}
112236

0 commit comments

Comments
 (0)