fix: address analyzer warnings

- call configureawait(false) where applicable
- add missing xml comments
This commit is contained in:
Stevan Freeborn
2026-03-29 21:41:22 -05:00
parent c39efb698b
commit dbb56a21dd
2 changed files with 73 additions and 13 deletions
@@ -2,6 +2,10 @@ using System.Text.Json;
namespace StevanFreeborn.Extensions.Configuration.Secure.Storage; namespace StevanFreeborn.Extensions.Configuration.Secure.Storage;
/// <summary>
/// Provides a mechanism to store and retrieve secure configuration data in a JSON file.
/// </summary>
/// <param name="options">The <see cref="JsonStorageOptions"/> configuring the storage provider, including file paths.</param>
public sealed class JsonFileStorageProvider(JsonStorageOptions options) public sealed class JsonFileStorageProvider(JsonStorageOptions options)
{ {
private static readonly SemaphoreSlim FileLock = new(1, 1); private static readonly SemaphoreSlim FileLock = new(1, 1);
@@ -12,9 +16,15 @@ public sealed class JsonFileStorageProvider(JsonStorageOptions options)
private readonly JsonStorageOptions _options = options private readonly JsonStorageOptions _options = options
?? throw new ArgumentNullException(nameof(options)); ?? throw new ArgumentNullException(nameof(options));
/// <summary>
/// Reads the value associated with the specified key from the JSON file asynchronously.
/// </summary>
/// <param name="key">The key of the configuration value to read.</param>
/// <param name="ct">A cancellation token that can be used to cancel the read operation.</param>
/// <returns>A task that represents the asynchronous read operation. The task result contains the value associated with the specified key, or an empty string if the key is not found.</returns>
public async Task<string> ReadAsync(string key, CancellationToken ct = default) public async Task<string> ReadAsync(string key, CancellationToken ct = default)
{ {
var data = await LoadWithLockAsync(ct); var data = await AcquireLockAndLoadAsync(ct).ConfigureAwait(false);
if (data is not null && data.TryGetValue(key, out var v)) if (data is not null && data.TryGetValue(key, out var v))
{ {
@@ -24,20 +34,32 @@ public sealed class JsonFileStorageProvider(JsonStorageOptions options)
return string.Empty; return string.Empty;
} }
/// <summary>
/// Reads all configuration values from the JSON file asynchronously.
/// </summary>
/// <param name="ct">A cancellation token that can be used to cancel the read operation.</param>
/// <returns>A task that represents the asynchronous read operation. The task result contains a dictionary of all configuration keys and their values.</returns>
public Task<Dictionary<string, string>> ReadAllAsync(CancellationToken ct = default) public Task<Dictionary<string, string>> ReadAllAsync(CancellationToken ct = default)
{ {
return LoadWithLockAsync(ct); return AcquireLockAndLoadAsync(ct);
} }
/// <summary>
/// Writes the specified key and encrypted data to the JSON file asynchronously.
/// </summary>
/// <param name="key">The key of the configuration value to write.</param>
/// <param name="encryptedData">The encrypted configuration data to write.</param>
/// <param name="ct">A cancellation token that can be used to cancel the write operation.</param>
/// <returns>A task that represents the asynchronous write operation.</returns>
public async Task WriteAsync(string key, string encryptedData, CancellationToken ct = default) public async Task WriteAsync(string key, string encryptedData, CancellationToken ct = default)
{ {
await FileLock.WaitAsync(ct); await FileLock.WaitAsync(ct).ConfigureAwait(false);
try try
{ {
var data = await LoadAsync(ct); var data = await LoadAsync(ct).ConfigureAwait(false);
data[key] = encryptedData; data[key] = encryptedData;
await SaveAsync(data, ct); await SaveAsync(data, ct).ConfigureAwait(false);
} }
finally finally
{ {
@@ -45,15 +67,21 @@ public sealed class JsonFileStorageProvider(JsonStorageOptions options)
} }
} }
/// <summary>
/// Deletes the configuration value associated with the specified key from the JSON file asynchronously.
/// </summary>
/// <param name="key">The key of the configuration value to delete.</param>
/// <param name="ct">A cancellation token that can be used to cancel the delete operation.</param>
/// <returns>A task that represents the asynchronous delete operation. The task result contains <c>true</c> if the value was successfully deleted; otherwise, <c>false</c>.</returns>
public async Task<bool> DeleteAsync(string key, CancellationToken ct = default) public async Task<bool> DeleteAsync(string key, CancellationToken ct = default)
{ {
await FileLock.WaitAsync(ct); await FileLock.WaitAsync(ct).ConfigureAwait(false);
try try
{ {
var data = await LoadAsync(ct); var data = await LoadAsync(ct).ConfigureAwait(false);
var result = data.Remove(key); var result = data.Remove(key);
await SaveAsync(data, ct); await SaveAsync(data, ct).ConfigureAwait(false);
return result; return result;
} }
finally finally
@@ -62,13 +90,18 @@ public sealed class JsonFileStorageProvider(JsonStorageOptions options)
} }
} }
private async Task<Dictionary<string, string>> LoadWithLockAsync(CancellationToken ct) /// <summary>
/// Acquires an exclusive lock and loads the configuration data from the JSON file.
/// </summary>
/// <param name="ct">A cancellation token to observe while waiting for the lock or during the load operation.</param>
/// <returns>A dictionary containing the loaded configuration data.</returns>
private async Task<Dictionary<string, string>> AcquireLockAndLoadAsync(CancellationToken ct)
{ {
await FileLock.WaitAsync(ct); await FileLock.WaitAsync(ct).ConfigureAwait(false);
try try
{ {
return await LoadAsync(ct); return await LoadAsync(ct).ConfigureAwait(false);
} }
finally finally
{ {
@@ -76,6 +109,11 @@ public sealed class JsonFileStorageProvider(JsonStorageOptions options)
} }
} }
/// <summary>
/// Loads the configuration data from the JSON file.
/// </summary>
/// <param name="ct">A cancellation token to observe while loading the data.</param>
/// <returns>A dictionary containing the loaded configuration data, or an empty dictionary if the file does not exist or is empty.</returns>
private async Task<Dictionary<string, string>> LoadAsync(CancellationToken ct) private async Task<Dictionary<string, string>> LoadAsync(CancellationToken ct)
{ {
if (File.Exists(_options.FullPath) is false) if (File.Exists(_options.FullPath) is false)
@@ -90,11 +128,18 @@ public sealed class JsonFileStorageProvider(JsonStorageOptions options)
return []; return [];
} }
var data = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, cancellationToken: ct); var data = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, cancellationToken: ct)
.ConfigureAwait(false);
return data ?? []; return data ?? [];
} }
/// <summary>
/// Saves the configuration data to the JSON file.
/// </summary>
/// <param name="data">The configuration data to save.</param>
/// <param name="ct">A cancellation token to observe while saving the data.</param>
/// <returns>A task that represents the asynchronous save operation.</returns>
private async Task SaveAsync(Dictionary<string, string> data, CancellationToken ct) private async Task SaveAsync(Dictionary<string, string> data, CancellationToken ct)
{ {
Directory.CreateDirectory(_options.DirectoryPath); Directory.CreateDirectory(_options.DirectoryPath);
@@ -106,6 +151,7 @@ public sealed class JsonFileStorageProvider(JsonStorageOptions options)
FileShare.None FileShare.None
); );
await JsonSerializer.SerializeAsync(stream, data, JsonOptions, ct); await JsonSerializer.SerializeAsync(stream, data, JsonOptions, ct)
.ConfigureAwait(false);
} }
} }
@@ -1,8 +1,22 @@
namespace StevanFreeborn.Extensions.Configuration.Secure.Storage; namespace StevanFreeborn.Extensions.Configuration.Secure.Storage;
/// <summary>
/// Options for configuring the <see cref="JsonFileStorageProvider"/>.
/// </summary>
public sealed class JsonStorageOptions public sealed class JsonStorageOptions
{ {
/// <summary>
/// Gets or sets the name of the JSON file used for storage.
/// </summary>
public string FileName { get; set; } = string.Empty; public string FileName { get; set; } = string.Empty;
/// <summary>
/// Gets or sets the directory path where the JSON file is located. Defaults to the base directory of the application.
/// </summary>
public string DirectoryPath { get; set; } = AppContext.BaseDirectory; public string DirectoryPath { get; set; } = AppContext.BaseDirectory;
/// <summary>
/// Gets the full, combined path to the JSON file, including the directory and file name.
/// </summary>
public string FullPath => Path.Combine(DirectoryPath, FileName); public string FullPath => Path.Combine(DirectoryPath, FileName);
} }