From 38daead9b28cf7d84311d18ebd6f617ca52900a5 Mon Sep 17 00:00:00 2001
From: Stevan Freeborn <65925598+StevanFreeborn@users.noreply.github.com>
Date: Sun, 29 Mar 2026 20:46:45 -0500
Subject: [PATCH] feat: implement read and write methods for json file storage
provider
---
.editorconfig | 15 +++--
StevanFreeborn.SecureConfig.slnx | 6 ++
.../ApiOptions.cs | 6 ++
.../Program.cs | 38 +++++++++++++
...ensions.Configuration.Secure.Sample.csproj | 18 ++++++
...orn.Extensions.Configuration.Secure.csproj | 15 +++++
.../Storage/JsonFileStorageProvider.cs | 56 +++++++++++++++++++
.../Storage/JsonStorageOptions.cs | 8 +++
tests/.editorconfig | 3 +
...tensions.Configuration.Secure.Tests.csproj | 27 +++++++++
.../Storage/JsonFileStorageProviderTests.cs | 44 +++++++++++++++
.../Unit/Storage/JsonStorageOptionsTests.cs | 36 ++++++++++++
12 files changed, 266 insertions(+), 6 deletions(-)
create mode 100644 src/StevanFreeborn.Extensions.Configuration.Secure.Sample/ApiOptions.cs
create mode 100644 src/StevanFreeborn.Extensions.Configuration.Secure.Sample/Program.cs
create mode 100644 src/StevanFreeborn.Extensions.Configuration.Secure.Sample/StevanFreeborn.Extensions.Configuration.Secure.Sample.csproj
create mode 100644 src/StevanFreeborn.Extensions.Configuration.Secure/StevanFreeborn.Extensions.Configuration.Secure.csproj
create mode 100644 src/StevanFreeborn.Extensions.Configuration.Secure/Storage/JsonFileStorageProvider.cs
create mode 100644 src/StevanFreeborn.Extensions.Configuration.Secure/Storage/JsonStorageOptions.cs
create mode 100644 tests/.editorconfig
create mode 100644 tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/StevanFreeborn.Extensions.Configuration.Secure.Tests.csproj
create mode 100644 tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Storage/JsonFileStorageProviderTests.cs
create mode 100644 tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Storage/JsonStorageOptionsTests.cs
diff --git a/.editorconfig b/.editorconfig
index af3d700..8296f0a 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -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
diff --git a/StevanFreeborn.SecureConfig.slnx b/StevanFreeborn.SecureConfig.slnx
index ba788ff..f129078 100644
--- a/StevanFreeborn.SecureConfig.slnx
+++ b/StevanFreeborn.SecureConfig.slnx
@@ -1,2 +1,8 @@
+
+
+
+
+
+
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/ApiOptions.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/ApiOptions.cs
new file mode 100644
index 0000000..2b7fd41
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/ApiOptions.cs
@@ -0,0 +1,6 @@
+namespace StevanFreeborn.Extensions.Configuration.Secure.Sample;
+
+public sealed record ApiOptions
+{
+ public string ApiKey { get; init; } = string.Empty;
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/Program.cs b/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/Program.cs
new file mode 100644
index 0000000..ab33fba
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/Program.cs
@@ -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(
+ builder.Configuration.GetSection(nameof(ApiOptions))
+);
+
+builder.Services.AddSecureConfig()
+ .UseJsonFileStorage(opt => opt.FileName = configFileName);
+
+var app = builder.Build();
+
+var options = app.Services.GetRequiredService>();
+var secureConfig = app.Services.GetRequiredService();
+
+Console.WriteLine(options.Value);
+
+await secureConfig.SetAsync(
+ nameof(ApiOptions),
+ new ApiOptions { ApiKey = "apiKey" }
+);
+
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/StevanFreeborn.Extensions.Configuration.Secure.Sample.csproj b/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/StevanFreeborn.Extensions.Configuration.Secure.Sample.csproj
new file mode 100644
index 0000000..94ae0dc
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure.Sample/StevanFreeborn.Extensions.Configuration.Secure.Sample.csproj
@@ -0,0 +1,18 @@
+
+
+
+ Exe
+ net11.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure/StevanFreeborn.Extensions.Configuration.Secure.csproj b/src/StevanFreeborn.Extensions.Configuration.Secure/StevanFreeborn.Extensions.Configuration.Secure.csproj
new file mode 100644
index 0000000..c2b6789
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure/StevanFreeborn.Extensions.Configuration.Secure.csproj
@@ -0,0 +1,15 @@
+
+
+
+ netstandard2.1
+ enable
+ enable
+ latest
+ true
+
+
+
+
+
+
+
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure/Storage/JsonFileStorageProvider.cs b/src/StevanFreeborn.Extensions.Configuration.Secure/Storage/JsonFileStorageProvider.cs
new file mode 100644
index 0000000..9e24588
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure/Storage/JsonFileStorageProvider.cs
@@ -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 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>(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
+ {
+ [key] = encryptedData
+ };
+
+ using var stream = new FileStream(
+ _options.FullPath,
+ FileMode.Create,
+ FileAccess.Write,
+ FileShare.None
+ );
+
+ await JsonSerializer.SerializeAsync(stream, data, JsonOptions, ct);
+ }
+}
\ No newline at end of file
diff --git a/src/StevanFreeborn.Extensions.Configuration.Secure/Storage/JsonStorageOptions.cs b/src/StevanFreeborn.Extensions.Configuration.Secure/Storage/JsonStorageOptions.cs
new file mode 100644
index 0000000..c40a93a
--- /dev/null
+++ b/src/StevanFreeborn.Extensions.Configuration.Secure/Storage/JsonStorageOptions.cs
@@ -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);
+}
\ No newline at end of file
diff --git a/tests/.editorconfig b/tests/.editorconfig
new file mode 100644
index 0000000..ccb26d4
--- /dev/null
+++ b/tests/.editorconfig
@@ -0,0 +1,3 @@
+[*.cs]
+
+dotnet_diagnostic.CA1707.severity = none
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/StevanFreeborn.Extensions.Configuration.Secure.Tests.csproj b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/StevanFreeborn.Extensions.Configuration.Secure.Tests.csproj
new file mode 100644
index 0000000..33fa446
--- /dev/null
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/StevanFreeborn.Extensions.Configuration.Secure.Tests.csproj
@@ -0,0 +1,27 @@
+
+
+
+ net11.0
+ enable
+ enable
+ false
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Storage/JsonFileStorageProviderTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Storage/JsonFileStorageProviderTests.cs
new file mode 100644
index 0000000..71fcfd7
--- /dev/null
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Storage/JsonFileStorageProviderTests.cs
@@ -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();
+ }
+
+ [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();
+ }
+}
diff --git a/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Storage/JsonStorageOptionsTests.cs b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Storage/JsonStorageOptionsTests.cs
new file mode 100644
index 0000000..948e90c
--- /dev/null
+++ b/tests/StevanFreeborn.Extensions.Configuration.Secure.Tests/Unit/Storage/JsonStorageOptionsTests.cs
@@ -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);
+ }
+}
\ No newline at end of file