diff --git a/CredentialCache.Test/CredentialCacheTests.cs b/CredentialCache.Test/CredentialCacheTests.cs index f18bb79..272584f 100644 --- a/CredentialCache.Test/CredentialCacheTests.cs +++ b/CredentialCache.Test/CredentialCacheTests.cs @@ -309,6 +309,66 @@ public void SingletonUsesConfiguredStore() CredentialCache.ResetSingletonForTesting(); } } + + [TestMethod] + public void DisposingTheSingletonLeavesItUsable() + { + CredentialCache.ResetSingletonForTesting(); + InMemoryCredentialStore store = new(); + CredentialCache.ConfigureStore(store); + try + { + PersonaGUID cached = CredentialCache.CreatePersonaGUID(); + PersonaGUID storedOnly = CredentialCache.CreatePersonaGUID(); + CredentialWithNothing cachedCredential = new(); + CredentialWithNothing storedCredential = new(); + store.Save(storedOnly, storedCredential); + + using (CredentialCache cache = CredentialCache.Instance) + { + cache.AddOrReplace(cached, cachedCredential); + } + + CredentialCache instance = CredentialCache.Instance; + Assert.IsTrue(instance.TryGet(cached, out Credential? first)); + Assert.AreSame(cachedCredential, first); + Assert.IsTrue(instance.TryGet(storedOnly, out Credential? second)); + Assert.IsNotNull(second); + + PersonaGUID added = CredentialCache.CreatePersonaGUID(); + instance.AddOrReplace(added, new CredentialWithNothing()); + Assert.IsTrue(instance.TryGet(added, out _)); + + Assert.Throws( + () => CredentialCache.ConfigureStore(new InMemoryCredentialStore())); + } + finally + { + CredentialCache.ResetSingletonForTesting(); + } + } + + [TestMethod] + public void ResetSingletonForTestingStillDisposesTheOldSingleton() + { + CredentialCache.ResetSingletonForTesting(); + CredentialCache.ConfigureStore(new InMemoryCredentialStore()); + CredentialCache old = CredentialCache.Instance; + + CredentialCache.ResetSingletonForTesting(); + + Assert.Throws(() => old.TryGet(CredentialCache.CreatePersonaGUID(), out _)); + } + + [TestMethod] + public void DisposingAnOwnedCacheStillDisposesIt() + { + CredentialCache cache = new(new InMemoryCredentialStore()); + + cache.Dispose(); + + Assert.Throws(() => cache.TryGet(CredentialCache.CreatePersonaGUID(), out _)); + } } public class CredentialWithNothingFactory : ICredentialFactory diff --git a/CredentialCache/CredentialCache.cs b/CredentialCache/CredentialCache.cs index b5ffca9..6b74949 100644 --- a/CredentialCache/CredentialCache.cs +++ b/CredentialCache/CredentialCache.cs @@ -38,6 +38,12 @@ public sealed class CredentialCache : IDisposable private readonly Lock[] _personaLocks = CreatePersonaLocks(); private bool _disposed; + /// + /// Set on the cache builds, so a caller's of + /// the shared singleton cannot break it for every other component in the process. + /// + private bool _isSingleton; + /// /// Creates a new credential cache backed by . /// @@ -52,9 +58,17 @@ public CredentialCache(ICredentialStore store) /// Gets the process-wide singleton instance, constructed on first access. /// /// + /// /// To override the backing store call before /// touching this property. The first access constructs the singleton; later /// reconfiguration requires . + /// + /// + /// The singleton is shared by the whole process, so calling on it + /// (for example through using var cache = CredentialCache.Instance;) does nothing: + /// it stays usable and keeps serving what it has cached. Only + /// disposes it. + /// /// public static CredentialCache Instance { @@ -65,7 +79,7 @@ public static CredentialCache Instance if (_instance is null) { ICredentialStore store = _configuredStore ?? CredentialStoreFactory.CreateDefault(); - _instance = new CredentialCache(store); + _instance = new CredentialCache(store) { _isSingleton = true }; } return _instance; } @@ -109,7 +123,7 @@ public static void ResetSingletonForTesting() { lock (_instanceLock) { - _instance?.Dispose(); + _instance?.DisposeCore(); _instance = null; _configuredStore = null; } @@ -281,7 +295,22 @@ public bool TryCreate(out Credential? credential) where T : Credential /// store; nothing is persisted or deleted at dispose-time because every /// mutation is already flushed eagerly in . /// + /// + /// Does nothing when called on the process-wide , which other + /// components may still be using; see . A cache created with + /// is disposed as usual. + /// public void Dispose() + { + if (_isSingleton) + { + return; + } + + DisposeCore(); + } + + private void DisposeCore() { if (_disposed) {