feat: implement read and write methods for json file storage provider

This commit is contained in:
Stevan Freeborn
2026-03-29 20:46:45 -05:00
parent b41669cbc6
commit 38daead9b2
12 changed files with 266 additions and 6 deletions
+9 -6
View File
@@ -25,8 +25,8 @@ indent_size = 2
#### Core EditorConfig Options ####
# Indentation and spacing
indent_size = 4
tab_width = 4
indent_size = 2
tab_width = 2
# New line preferences
insert_final_newline = false
@@ -70,7 +70,7 @@ dotnet_style_prefer_auto_properties = true:suggestion
dotnet_style_prefer_collection_expression = when_types_loosely_match:suggestion
dotnet_style_prefer_compound_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_assignment = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = true:suggestion
dotnet_style_prefer_conditional_expression_over_return = false:silent;
dotnet_style_prefer_foreach_explicit_cast_in_source = when_strongly_typed:suggestion
dotnet_style_prefer_inferred_anonymous_type_member_names = true:suggestion
dotnet_style_prefer_inferred_tuple_names = true:suggestion
@@ -90,10 +90,13 @@ dotnet_remove_unnecessary_suppression_exclusions = none
#### C# Coding Conventions ####
[*.cs]
dotnet_diagnostic.IDE0058.severity = none
dotnet_diagnostic.IDE0100.severity = none
# var preferences
csharp_style_var_elsewhere = false:silent
csharp_style_var_for_built_in_types = false:silent
csharp_style_var_when_type_is_apparent = false:silent
csharp_style_var_elsewhere = true:suggestion
csharp_style_var_for_built_in_types = true:suggestion
csharp_style_var_when_type_is_apparent = true:suggestion
# Expression-bodied members
csharp_style_expression_bodied_accessors = true:silent
+6
View File
@@ -1,2 +1,8 @@
<Solution>
<Folder Name="/src/">
<Project Path="src/StevanFreeborn.Extensions.Configuration.Secure/StevanFreeborn.Extensions.Configuration.Secure.csproj" />
</Folder>
<Folder Name="/tests/">
<Project Path="tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/StevanFreeborn.Extensions.Configuration.Secure.Tests.csproj" />
</Folder>
</Solution>
@@ -0,0 +1,6 @@
namespace StevanFreeborn.Extensions.Configuration.Secure.Sample;
public sealed record ApiOptions
{
public string ApiKey { get; init; } = string.Empty;
}
@@ -0,0 +1,38 @@
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using StevanFreeborn.Extensions.Configuration.Secure.Sample;
using StevanFreeborn.Extensions.Configuration.Secure.Storage;
var builder = Host.CreateApplicationBuilder();
const string configFileName = "appsettings.json";
var opts = new JsonStorageOptions()
{
FileName = configFileName,
};
var storageProvider = new JsonFileStorageProvider(opts);
builder.Configuration.AddSecureConfig(storageProvider);
builder.Services.Configure<ApiOptions>(
builder.Configuration.GetSection(nameof(ApiOptions))
);
builder.Services.AddSecureConfig()
.UseJsonFileStorage(opt => opt.FileName = configFileName);
var app = builder.Build();
var options = app.Services.GetRequiredService<IOptions<ApiOptions>>();
var secureConfig = app.Services.GetRequiredService<ISecureConfig>();
Console.WriteLine(options.Value);
await secureConfig.SetAsync(
nameof(ApiOptions),
new ApiOptions { ApiKey = "apiKey" }
);
@@ -0,0 +1,18 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net11.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.Hosting" Version="10.0.5" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\StevanFreeborn.Extensions.Configuration.Secure\StevanFreeborn.Extensions.Configuration.Secure.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,15 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.1</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<LangVersion>latest</LangVersion>
<IsAotCompatible Condition="$([MSBuild]::IsTargetFrameworkCompatible('$(TargetFramework)', 'net8.0'))">true</IsAotCompatible>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="System.Text.Json" Version="10.0.5" />
</ItemGroup>
</Project>
@@ -0,0 +1,56 @@
using System.Text.Json;
namespace StevanFreeborn.Extensions.Configuration.Secure.Storage;
public sealed class JsonFileStorageProvider(JsonStorageOptions options)
{
private static readonly JsonSerializerOptions JsonOptions = new()
{
WriteIndented = true,
};
private readonly JsonStorageOptions _options = options
?? throw new ArgumentNullException(nameof(options));
public async Task<string> ReadAsync(string key, CancellationToken ct = default)
{
if (File.Exists(_options.FullPath) is false)
{
return string.Empty;
}
using var stream = new FileStream(_options.FullPath, FileMode.Open, FileAccess.Read);
if (stream.Length is 0)
{
return string.Empty;
}
var data = await JsonSerializer.DeserializeAsync<Dictionary<string, string>>(stream, cancellationToken: ct);
if (data is not null && data.TryGetValue(key, out var v))
{
return v;
}
return string.Empty;
}
public async Task WriteAsync(string key, string encryptedData, CancellationToken ct = default)
{
Directory.CreateDirectory(_options.DirectoryPath);
var data = new Dictionary<string, string>
{
[key] = encryptedData
};
using var stream = new FileStream(
_options.FullPath,
FileMode.Create,
FileAccess.Write,
FileShare.None
);
await JsonSerializer.SerializeAsync(stream, data, JsonOptions, ct);
}
}
@@ -0,0 +1,8 @@
namespace StevanFreeborn.Extensions.Configuration.Secure.Storage;
public sealed class JsonStorageOptions
{
public string FileName { get; set; } = string.Empty;
public string DirectoryPath { get; set; } = AppContext.BaseDirectory;
public string FullPath => Path.Combine(DirectoryPath, FileName);
}
+3
View File
@@ -0,0 +1,3 @@
[*.cs]
dotnet_diagnostic.CA1707.severity = none
@@ -0,0 +1,27 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net11.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="AwesomeAssertions" Version="9.4.0" />
<PackageReference Include="coverlet.collector" Version="6.0.4" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.14.1" />
<PackageReference Include="xunit" Version="2.9.3" />
<PackageReference Include="xunit.runner.visualstudio" Version="3.1.4" />
</ItemGroup>
<ItemGroup>
<Using Include="Xunit" />
<Using Include="AwesomeAssertions" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\StevanFreeborn.Extensions.Configuration.Secure\StevanFreeborn.Extensions.Configuration.Secure.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,44 @@
using StevanFreeborn.Extensions.Configuration.Secure.Storage;
namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Unit.Storage;
public class JsonFileStorageProviderTests
{
private readonly string _tmpDirectory;
private readonly JsonStorageOptions _options;
private readonly JsonFileStorageProvider _sut;
public JsonFileStorageProviderTests()
{
_tmpDirectory = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString());
Directory.CreateDirectory(_tmpDirectory);
_options = new()
{
FileName = "testsettings.json"
};
_sut = new(_options);
}
[Fact]
public void Constructor_WhenCalledWithNullOptions_ItShouldThrowArgumentNullException()
{
var act = static () => new JsonFileStorageProvider(null!);
act.Should().Throw<ArgumentNullException>();
}
[Fact]
public async Task WriteAsync_And_ReadAsync_WhenCalled_ItShouldBeAbleToPersistAndRetrieveValues()
{
var configKey = "Key";
var configValue = "Value";
await _sut.WriteAsync(configKey, configValue);
var result = await _sut.ReadAsync(configKey);
result.Should().Be(configValue);
File.Exists(_options.FullPath).Should().BeTrue();
}
}
@@ -0,0 +1,36 @@
using StevanFreeborn.Extensions.Configuration.Secure.Storage;
namespace StevanFreeborn.Extensions.Configuration.Secure.Tests.Unit.Storage;
public class JsonStorageOptionsTests
{
[Fact]
public void Path_WhenCalledWithFileNameSet_ItShouldReturnExpectedPath()
{
var fileName = "appsettings.json";
var expectedPath = Path.Combine(AppContext.BaseDirectory, fileName);
var opts = new JsonStorageOptions()
{
FileName = fileName,
};
opts.FullPath.Should().Be(expectedPath);
}
[Fact]
public void Path_WhenCalledWithFileNameAndDirectoryPathSet_ItShouldReturnExpectedPath()
{
var fileName = "appsettings.json";
var directoryPath = @"C:\Path\To\Some\Directory";
var expectedPath = Path.Combine(directoryPath, fileName);
var opts = new JsonStorageOptions()
{
FileName = fileName,
DirectoryPath = directoryPath,
};
opts.FullPath.Should().Be(expectedPath);
}
}