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