feat: support concurrency in json file storage provider
This commit is contained in:
+42
-9
@@ -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,22 +26,54 @@ 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)
|
||||
{
|
||||
var data = await LoadAsync(ct);
|
||||
data[key] = encryptedData;
|
||||
await SaveAsync(data, ct);
|
||||
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)
|
||||
{
|
||||
var data = await LoadAsync(ct);
|
||||
var result = data.Remove(key);
|
||||
await SaveAsync(data, ct);
|
||||
return result;
|
||||
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)
|
||||
|
||||
+19
@@ -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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user