Skip to content
Merged
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
1 change: 1 addition & 0 deletions CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@

<PackageReference Include="Testably.Abstractions.FileSystem.Interface" /></ItemGroup>

<ItemGroup>
<Compile Include="..\Shared\PersistenceDefaults.cs" Link="PersistenceDefaults.cs" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="ktsu.Essentials.Tests" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -118,11 +118,8 @@ public async Task StoreAsync<T>(TKey key, T obj, CancellationToken cancellationT
}

/// <inheritdoc/>
public async Task<T> RetrieveOrCreateAsync<T>(TKey key, CancellationToken cancellationToken = default) where T : new()
{
T? obj = await RetrieveAsync<T>(key, cancellationToken).ConfigureAwait(false);
return obj ?? new T();
}
public Task<T> RetrieveOrCreateAsync<T>(TKey key, CancellationToken cancellationToken = default) where T : new()
=> PersistenceDefaults.RetrieveOrCreateAsync<TKey, T>(this, key, cancellationToken);

/// <inheritdoc/>
public Task<bool> ExistsAsync(TKey key, CancellationToken cancellationToken = default)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,10 @@

<PackageReference Include="Testably.Abstractions.FileSystem.Interface" /></ItemGroup>

<ItemGroup>
<Compile Include="..\Shared\PersistenceDefaults.cs" Link="PersistenceDefaults.cs" />
</ItemGroup>

<ItemGroup>
<InternalsVisibleTo Include="ktsu.Essentials.Tests" />
</ItemGroup>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -115,11 +115,8 @@ public async Task StoreAsync<T>(TKey key, T obj, CancellationToken cancellationT
}

/// <inheritdoc/>
public async Task<T> RetrieveOrCreateAsync<T>(TKey key, CancellationToken cancellationToken = default) where T : new()
{
T? obj = await RetrieveAsync<T>(key, cancellationToken).ConfigureAwait(false);
return obj ?? new T();
}
public Task<T> RetrieveOrCreateAsync<T>(TKey key, CancellationToken cancellationToken = default) where T : new()
=> PersistenceDefaults.RetrieveOrCreateAsync<TKey, T>(this, key, cancellationToken);

/// <inheritdoc/>
public Task<bool> ExistsAsync(TKey key, CancellationToken cancellationToken = default)
Expand Down
36 changes: 36 additions & 0 deletions Essentials.Tests/PersistenceProviderTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<string> temp = new(fs, serializer, "PersistenceTests_Temp_" + Guid.NewGuid().ToString("N")[..8]);

try
{
IPersistenceProvider<string>[] providers =
[
CreatePersistence(),
new FileSystemPersistenceProvider<string>(fs, serializer, fileSystemDir),
temp,
];

foreach (IPersistenceProvider<string> persistence in providers)
{
TestData created = await persistence.RetrieveOrCreateAsync<TestData>("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;
Expand Down
44 changes: 44 additions & 0 deletions Shared/PersistenceDefaults.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) 2023-2026 ktsu-dev contributors

namespace ktsu.Essentials;

using System.Threading;
using System.Threading.Tasks;

/// <summary>
/// The interface default bodies a persistence provider has to repeat when it declares the member itself.
/// </summary>
/// <remarks>
/// Linked into the FileSystem and Temp providers rather than placed in the interfaces package, following
/// <c>DistributionArguments</c>. Both declare a public <c>RetrieveOrCreateAsync</c> of their own, which
/// hides the <see cref="IPersistenceProvider{TKey}"/> default body, so the documented behaviour has to be
/// restated; restating it once here keeps the two from drifting apart again.
/// </remarks>
internal static class PersistenceDefaults
{
/// <summary>
/// Retrieves the object stored under <paramref name="key"/>, or creates, stores and returns a new one.
/// </summary>
/// <typeparam name="TKey">The type used to identify stored objects.</typeparam>
/// <typeparam name="T">The type of object to retrieve or create.</typeparam>
/// <param name="provider">The provider to read from and store into.</param>
/// <param name="key">The key of the object.</param>
/// <param name="cancellationToken">A token to cancel the operation.</param>
/// <returns>The stored object, or the new instance now stored under <paramref name="key"/>.</returns>
internal static async Task<T> RetrieveOrCreateAsync<TKey, T>(IPersistenceProvider<TKey> provider, TKey key, CancellationToken cancellationToken)
where TKey : notnull
where T : new()
{
T? existing = await provider.RetrieveAsync<T>(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;
}
}
Loading