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;
+ }
+}