From cec633008a813807337bb29798a497bac516c4a7 Mon Sep 17 00:00:00 2001 From: Claude Date: Thu, 5 Nov 2026 16:29:15 +0000 Subject: [PATCH] Ignore Dispose on the process-wide Instance [patch] Disposing what CredentialCache.Instance returned (easy to do with `using var cache = CredentialCache.Instance;`, since the same type is disposable when you new it) left the singleton disposed for the rest of the process: Instance kept returning it, every method threw ObjectDisposedException, and ConfigureStore refused to replace it. The cache Instance builds now ignores Dispose, so it stays usable for every other component. ResetSingletonForTesting still disposes it, and a cache created with the constructor disposes as before. The Instance and Dispose docs say so. Fixes #201 Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_017vfJW5wqHMdBMn6Wino1eH --- CredentialCache.Test/CredentialCacheTests.cs | 60 ++++++++++++++++++++ CredentialCache/CredentialCache.cs | 33 ++++++++++- 2 files changed, 91 insertions(+), 2 deletions(-) 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) {