feat: support concurrency in json file storage provider

This commit is contained in:
Stevan Freeborn
2026-03-29 21:25:39 -05:00
parent 8453381fe0
commit c8dbde8dc8
2 changed files with 61 additions and 9 deletions
@@ -4,6 +4,7 @@ namespace StevanFreeborn.Extensions.Configuration.Secure.Storage;
public sealed class JsonFileStorageProvider(JsonStorageOptions options)
{
private static readonly SemaphoreSlim FileLock = new(1, 1);
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
@@ -13,7 +14,7 @@ public sealed class JsonFileStorageProvider(JsonStorageOptions options)
public async Task<string> ReadAsync(string key, CancellationToken ct = default)
{
var data = await LoadAsync(ct);
var data = await LoadWithLockAsync(ct);
if (data is not null && data.TryGetValue(key, out var v))
{
@@ -25,23 +26,55 @@ public sealed class JsonFileStorageProvider(JsonStorageOptions options)
public Task<Dictionary<string, string>> ReadAllAsync(CancellationToken ct = default)
{
return LoadAsync(ct);
return LoadWithLockAsync(ct);
}
public async Task WriteAsync(string key, string encryptedData, CancellationToken ct = default)
{
await FileLock.WaitAsync(ct);
try
{
var data = await LoadAsync(ct);
data[key] = encryptedData;
await SaveAsync(data, ct);
}
finally
{
FileLock.Release();
}
}
public async Task<bool> DeleteAsync(string key, CancellationToken ct = default)
{
await FileLock.WaitAsync(ct);
try
{
var data = await LoadAsync(ct);
var result = data.Remove(key);
await SaveAsync(data, ct);
return result;
}
finally
{
FileLock.Release();
}
}
private async Task<Dictionary<string, string>> LoadWithLockAsync(CancellationToken ct)
{
await FileLock.WaitAsync(ct);
try
{
return await LoadAsync(ct);
}
finally
{
FileLock.Release();
}
}
private async Task<Dictionary<string, string>> LoadAsync(CancellationToken ct)
{
@@ -79,4 +79,23 @@ public class JsonFileStorageProviderTests : IDisposable
GC.SuppressFinalize(this);
}
[Fact]
public async Task WriteAsync_WhenCalledConcurrently_ItShouldNotThrowFileInUseException()
{
const int numberOfWrites = 50;
var tasks = new List<Task>();
foreach (var index in Enumerable.Range(0, numberOfWrites))
{
tasks.Add(Task.Run(() => _sut.WriteAsync($"Key{index}", $"Val{index}")));
}
var act = async () => await Task.WhenAll(tasks);
await act.Should().NotThrowAsync();
var allData = await _sut.ReadAllAsync();
allData.Should().HaveCount(numberOfWrites);
}
}