Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions CredentialCache.Test/CredentialCacheTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<InvalidOperationException>(
() => 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<ObjectDisposedException>(() => old.TryGet(CredentialCache.CreatePersonaGUID(), out _));
}

[TestMethod]
public void DisposingAnOwnedCacheStillDisposesIt()
{
CredentialCache cache = new(new InMemoryCredentialStore());

cache.Dispose();

Assert.Throws<ObjectDisposedException>(() => cache.TryGet(CredentialCache.CreatePersonaGUID(), out _));
}
}

public class CredentialWithNothingFactory : ICredentialFactory<CredentialWithNothing>
Expand Down
33 changes: 31 additions & 2 deletions CredentialCache/CredentialCache.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
/// <summary>
/// Represents a globally unique identifier for a persona.
/// </summary>
public sealed record class PersonaGUID : SemanticString<PersonaGUID> { }

Check warning on line 12 in CredentialCache/CredentialCache.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Rename record 'PersonaGUID' to match pascal case naming rules, consider using 'PersonaGuid'.

Check warning on line 12 in CredentialCache/CredentialCache.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Rename record 'PersonaGUID' to match pascal case naming rules, consider using 'PersonaGuid'.

Check warning on line 12 in CredentialCache/CredentialCache.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Rename record 'PersonaGUID' to match pascal case naming rules, consider using 'PersonaGuid'.

Check warning on line 12 in CredentialCache/CredentialCache.cs

View workflow job for this annotation

GitHub Actions / ci / .NET / Analyze & Release

Rename record 'PersonaGUID' to match pascal case naming rules, consider using 'PersonaGuid'.

/// <summary>
/// Caches <see cref="Credential"/> instances in memory and persists each one through
Expand Down Expand Up @@ -38,6 +38,12 @@
private readonly Lock[] _personaLocks = CreatePersonaLocks();
private bool _disposed;

/// <summary>
/// Set on the cache <see cref="Instance"/> builds, so a caller's <see cref="Dispose"/> of
/// the shared singleton cannot break it for every other component in the process.
/// </summary>
private bool _isSingleton;

/// <summary>
/// Creates a new credential cache backed by <paramref name="store"/>.
/// </summary>
Expand All @@ -52,9 +58,17 @@
/// Gets the process-wide singleton instance, constructed on first access.
/// </summary>
/// <remarks>
/// <para>
/// To override the backing store call <see cref="ConfigureStore"/> before
/// touching this property. The first access constructs the singleton; later
/// reconfiguration requires <see cref="ResetSingletonForTesting"/>.
/// </para>
/// <para>
/// The singleton is shared by the whole process, so calling <see cref="Dispose"/> on it
/// (for example through <c>using var cache = CredentialCache.Instance;</c>) does nothing:
/// it stays usable and keeps serving what it has cached. Only
/// <see cref="ResetSingletonForTesting"/> disposes it.
/// </para>
/// </remarks>
public static CredentialCache Instance
{
Expand All @@ -65,7 +79,7 @@
if (_instance is null)
{
ICredentialStore store = _configuredStore ?? CredentialStoreFactory.CreateDefault();
_instance = new CredentialCache(store);
_instance = new CredentialCache(store) { _isSingleton = true };
}
return _instance;
}
Expand Down Expand Up @@ -109,7 +123,7 @@
{
lock (_instanceLock)
{
_instance?.Dispose();
_instance?.DisposeCore();
_instance = null;
_configuredStore = null;
}
Expand Down Expand Up @@ -281,7 +295,22 @@
/// store; nothing is persisted or deleted at dispose-time because every
/// mutation is already flushed eagerly in <see cref="AddOrReplace"/>.
/// </summary>
/// <remarks>
/// Does nothing when called on the process-wide <see cref="Instance"/>, which other
/// components may still be using; see <see cref="Instance"/>. A cache created with
/// <see cref="CredentialCache(ICredentialStore)"/> is disposed as usual.
/// </remarks>
public void Dispose()
{
if (_isSingleton)
{
return;
}

DisposeCore();
}

private void DisposeCore()
{
if (_disposed)
{
Expand Down
Loading