diff --git a/CLAUDE.md b/CLAUDE.md index 38377ed..672be02 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -47,6 +47,7 @@ This is a .NET library (`ktsu.Essentials`) providing high-performance interfaces - `Shared/DeflateBound.cs` - zlib's `deflateBound` plus container overhead, linked into the Deflate, Gzip and ZLib providers as their `GetMaxCompressedLength` - `Shared/SpecialFunctions.cs` - Incomplete gamma, incomplete beta, error function and normal quantile, linked into the Normal, LogNormal, Binomial and Poisson providers rather than placed in the interfaces-only package - `Shared/DistributionArguments.cs` - Parameter validation shared by every distribution provider, linked in the same way +- `Shared/PersistenceDefaults.cs` - The `IPersistenceProvider` default `RetrieveOrCreateAsync` body, linked into the FileSystem and Temp providers, which declare that member themselves and so hide the default - `Essentials/ISerializationProvider.cs` - Object serialization/deserialization interface - `Essentials/ISerializationOptions.cs` - Configurable serialization options (naming, inclusion, boxing policies) - `Essentials/ICacheProvider.cs` - Generic cache interface with expiration and get-or-add diff --git a/Essentials.PersistenceProviders.FileSystem/Essentials.PersistenceProviders.FileSystem.csproj b/Essentials.PersistenceProviders.FileSystem/Essentials.PersistenceProviders.FileSystem.csproj index f7ce966..2f1340e 100644 --- a/Essentials.PersistenceProviders.FileSystem/Essentials.PersistenceProviders.FileSystem.csproj +++ b/Essentials.PersistenceProviders.FileSystem/Essentials.PersistenceProviders.FileSystem.csproj @@ -14,6 +14,10 @@ + + + + diff --git a/Essentials.PersistenceProviders.FileSystem/FileSystemPersistenceProvider.cs b/Essentials.PersistenceProviders.FileSystem/FileSystemPersistenceProvider.cs index 102a155..62d9de2 100644 --- a/Essentials.PersistenceProviders.FileSystem/FileSystemPersistenceProvider.cs +++ b/Essentials.PersistenceProviders.FileSystem/FileSystemPersistenceProvider.cs @@ -118,11 +118,8 @@ public async Task StoreAsync(TKey key, T obj, CancellationToken cancellationT } /// - public async Task RetrieveOrCreateAsync(TKey key, CancellationToken cancellationToken = default) where T : new() - { - T? obj = await RetrieveAsync(key, cancellationToken).ConfigureAwait(false); - return obj ?? new T(); - } + public Task RetrieveOrCreateAsync(TKey key, CancellationToken cancellationToken = default) where T : new() + => PersistenceDefaults.RetrieveOrCreateAsync(this, key, cancellationToken); /// public Task ExistsAsync(TKey key, CancellationToken cancellationToken = default) diff --git a/Essentials.PersistenceProviders.Temp/Essentials.PersistenceProviders.Temp.csproj b/Essentials.PersistenceProviders.Temp/Essentials.PersistenceProviders.Temp.csproj index f7ce966..2f1340e 100644 --- a/Essentials.PersistenceProviders.Temp/Essentials.PersistenceProviders.Temp.csproj +++ b/Essentials.PersistenceProviders.Temp/Essentials.PersistenceProviders.Temp.csproj @@ -14,6 +14,10 @@ + + + + diff --git a/Essentials.PersistenceProviders.Temp/TempPersistenceProvider.cs b/Essentials.PersistenceProviders.Temp/TempPersistenceProvider.cs index fa79d96..d0ad67f 100644 --- a/Essentials.PersistenceProviders.Temp/TempPersistenceProvider.cs +++ b/Essentials.PersistenceProviders.Temp/TempPersistenceProvider.cs @@ -115,11 +115,8 @@ public async Task StoreAsync(TKey key, T obj, CancellationToken cancellationT } /// - public async Task RetrieveOrCreateAsync(TKey key, CancellationToken cancellationToken = default) where T : new() - { - T? obj = await RetrieveAsync(key, cancellationToken).ConfigureAwait(false); - return obj ?? new T(); - } + public Task RetrieveOrCreateAsync(TKey key, CancellationToken cancellationToken = default) where T : new() + => PersistenceDefaults.RetrieveOrCreateAsync(this, key, cancellationToken); /// public Task ExistsAsync(TKey key, CancellationToken cancellationToken = default) diff --git a/Essentials.Tests/PersistenceProviderTests.cs b/Essentials.Tests/PersistenceProviderTests.cs index 556ba98..8d2cc10 100644 --- a/Essentials.Tests/PersistenceProviderTests.cs +++ b/Essentials.Tests/PersistenceProviderTests.cs @@ -516,6 +516,42 @@ public void Temp_Properties() } } + // --- Cross-provider contract --- + + [TestMethod] + public async Task RetrieveOrCreate_Stores_The_New_Instance_On_Every_Provider() + { + // Called through the interface, as consumers do: FileSystem and Temp declared their own public + // RetrieveOrCreateAsync that returned a default without storing it, so the same call persisted on + // InMemory, ConfigHome and DataHome but not on these two. + string fileSystemDir = Path.Join(Path.GetTempPath(), "PersistenceTests_FS_" + Guid.NewGuid().ToString("N")[..8]); + NativeFileSystemProvider fs = new(); + JsonSerializationProvider serializer = new(); + using TempPersistenceProvider temp = new(fs, serializer, "PersistenceTests_Temp_" + Guid.NewGuid().ToString("N")[..8]); + + try + { + IPersistenceProvider[] providers = + [ + CreatePersistence(), + new FileSystemPersistenceProvider(fs, serializer, fileSystemDir), + temp, + ]; + + foreach (IPersistenceProvider persistence in providers) + { + TestData created = await persistence.RetrieveOrCreateAsync("bootstrap", TestContext.CancellationToken).ConfigureAwait(false); + Assert.IsNotNull(created); + Assert.IsTrue(await persistence.ExistsAsync("bootstrap", TestContext.CancellationToken).ConfigureAwait(false), $"{persistence.ProviderName} did not store the new instance"); + } + } + finally + { + CleanupDirectory(fileSystemDir); + temp.CleanupDirectory(); + } + } + public sealed class TestData { public string Name { get; set; } = string.Empty; diff --git a/Shared/PersistenceDefaults.cs b/Shared/PersistenceDefaults.cs new file mode 100644 index 0000000..5545b9b --- /dev/null +++ b/Shared/PersistenceDefaults.cs @@ -0,0 +1,44 @@ +// Copyright (c) 2023-2026 ktsu-dev contributors + +namespace ktsu.Essentials; + +using System.Threading; +using System.Threading.Tasks; + +/// +/// The interface default bodies a persistence provider has to repeat when it declares the member itself. +/// +/// +/// Linked into the FileSystem and Temp providers rather than placed in the interfaces package, following +/// DistributionArguments. Both declare a public RetrieveOrCreateAsync of their own, which +/// hides the default body, so the documented behaviour has to be +/// restated; restating it once here keeps the two from drifting apart again. +/// +internal static class PersistenceDefaults +{ + /// + /// Retrieves the object stored under , or creates, stores and returns a new one. + /// + /// The type used to identify stored objects. + /// The type of object to retrieve or create. + /// The provider to read from and store into. + /// The key of the object. + /// A token to cancel the operation. + /// The stored object, or the new instance now stored under . + internal static async Task RetrieveOrCreateAsync(IPersistenceProvider provider, TKey key, CancellationToken cancellationToken) + where TKey : notnull + where T : new() + { + T? existing = await provider.RetrieveAsync(key, cancellationToken).ConfigureAwait(false); + if (existing is not null) + { + return existing; + } + + // Stored before it is returned, as the interface documents: callers bootstrap a record with this, + // and one that was never written comes back as a fresh default on every call. + T newInstance = new(); + await provider.StoreAsync(key, newInstance, cancellationToken).ConfigureAwait(false); + return newInstance; + } +}