From b39d0f7bc554b313be18048f2fd9a6d84294af0b Mon Sep 17 00:00:00 2001 From: Mubarak Imam Date: Sun, 30 May 2021 23:28:56 +0100 Subject: [PATCH 1/3] FEAT-1: created TinyConfig.Core abstraction - added TinyConfig.Abstractions library - added a TinyConfig.Core generic implementation - added a TinyConfig.SimpleDB, a light database backed implementation [WIP #1] --- README.md | 5 +- TinyConfig.sln | 18 +++ .../ITinyConfigOptions.cs | 26 ++++ .../ITinyConfigStore.cs | 30 +++++ src/TinyConfig.Abstractions/ITinySetting.cs | 24 ++++ .../TinyConfig.Abstractions.csproj | 7 ++ .../ConfigurationBuilderExtensions.cs | 38 ++++++ .../Extensions/ServiceCollectionExtensions.cs | 27 +++++ src/TinyConfig.Core/TinyConfig.Core.csproj | 16 +++ .../TinyConfigChangeWatcher.cs | 81 +++++++++++++ src/TinyConfig.Core/TinyConfigProvider.cs | 89 ++++++++++++++ src/TinyConfig.Core/TinyConfigSource.cs | 46 +++++++ .../Core/SimpleDbConfigOptions.cs | 35 ++++++ .../Core/SimpleDbConfigStore.cs | 48 ++++++++ src/TinyConfig.SimpleDB/Core/SimpleSetting.cs | 24 ++++ .../Cryptography/AesCrypto.cs | 114 ++++++++++++++++++ .../TinyConfig.SimpleDB.csproj | 17 +++ 17 files changed, 643 insertions(+), 2 deletions(-) create mode 100644 TinyConfig.sln create mode 100644 src/TinyConfig.Abstractions/ITinyConfigOptions.cs create mode 100644 src/TinyConfig.Abstractions/ITinyConfigStore.cs create mode 100644 src/TinyConfig.Abstractions/ITinySetting.cs create mode 100644 src/TinyConfig.Abstractions/TinyConfig.Abstractions.csproj create mode 100644 src/TinyConfig.Core/Extensions/ConfigurationBuilderExtensions.cs create mode 100644 src/TinyConfig.Core/Extensions/ServiceCollectionExtensions.cs create mode 100644 src/TinyConfig.Core/TinyConfig.Core.csproj create mode 100644 src/TinyConfig.Core/TinyConfigChangeWatcher.cs create mode 100644 src/TinyConfig.Core/TinyConfigProvider.cs create mode 100644 src/TinyConfig.Core/TinyConfigSource.cs create mode 100644 src/TinyConfig.SimpleDB/Core/SimpleDbConfigOptions.cs create mode 100644 src/TinyConfig.SimpleDB/Core/SimpleDbConfigStore.cs create mode 100644 src/TinyConfig.SimpleDB/Core/SimpleSetting.cs create mode 100644 src/TinyConfig.SimpleDB/Cryptography/AesCrypto.cs create mode 100644 src/TinyConfig.SimpleDB/TinyConfig.SimpleDB.csproj diff --git a/README.md b/README.md index b8facc9..40c58c0 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,3 @@ -# tinyconfig -TinyConfig is a simple library to build custom configuration provider in .NET with reload support. +# TinyConfig + +TinyConfig is a simple library to democratize building custom configuration provider in .NET. diff --git a/TinyConfig.sln b/TinyConfig.sln new file mode 100644 index 0000000..6f16465 --- /dev/null +++ b/TinyConfig.sln @@ -0,0 +1,18 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.6.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection +EndGlobal diff --git a/src/TinyConfig.Abstractions/ITinyConfigOptions.cs b/src/TinyConfig.Abstractions/ITinyConfigOptions.cs new file mode 100644 index 0000000..bfb5197 --- /dev/null +++ b/src/TinyConfig.Abstractions/ITinyConfigOptions.cs @@ -0,0 +1,26 @@ + +namespace TinyConfig.Abstractions +{ + /// + /// tiny config options + /// + public interface ITinyConfigOptions + { + /// + /// indicate whether tiny config is enabled, false by default + /// + bool Enabled { get => false; } + + /// + /// indicate whether configuration should be reloaded when a change occurs + /// Has default value "false" + /// + bool ReloadOnChange { get => false; } + + /// + /// number of seconds to wait before reloading after a change + /// Has default value "60" seconds + /// + int ReloadDelay { get => 60; } + } +} diff --git a/src/TinyConfig.Abstractions/ITinyConfigStore.cs b/src/TinyConfig.Abstractions/ITinyConfigStore.cs new file mode 100644 index 0000000..a6cb576 --- /dev/null +++ b/src/TinyConfig.Abstractions/ITinyConfigStore.cs @@ -0,0 +1,30 @@ +using System.Collections.Generic; +using System.Threading.Tasks; + +namespace TinyConfig.Abstractions +{ + /// + /// wraps a configuration store, providing access to the actual configuration values + /// + public interface ITinyConfigStore + { + /// + /// check if a change has occurred on the configuration store + /// + /// + /// true is settings has changed + Task HasChanged(object versionToken); + + /// + /// check if config store has any entry + /// + /// true if has at least one entry + Task HasAny(); + + /// + /// get all settings from configuration store + /// + /// a list containing all configuration key value pairs + Task> GetAll(); + } +} diff --git a/src/TinyConfig.Abstractions/ITinySetting.cs b/src/TinyConfig.Abstractions/ITinySetting.cs new file mode 100644 index 0000000..1ff8e61 --- /dev/null +++ b/src/TinyConfig.Abstractions/ITinySetting.cs @@ -0,0 +1,24 @@ + +namespace TinyConfig.Abstractions +{ + /// + /// settings model + /// + public interface ITinySetting + { + /// + /// settings id (key) + /// + string Id { get; } + + /// + /// settings value + /// + string Value { get; } + + /// + /// settings version + /// + byte[] Version { get; } + } +} diff --git a/src/TinyConfig.Abstractions/TinyConfig.Abstractions.csproj b/src/TinyConfig.Abstractions/TinyConfig.Abstractions.csproj new file mode 100644 index 0000000..f51c9c8 --- /dev/null +++ b/src/TinyConfig.Abstractions/TinyConfig.Abstractions.csproj @@ -0,0 +1,7 @@ + + + + netstandard2.1 + + + diff --git a/src/TinyConfig.Core/Extensions/ConfigurationBuilderExtensions.cs b/src/TinyConfig.Core/Extensions/ConfigurationBuilderExtensions.cs new file mode 100644 index 0000000..d6517d3 --- /dev/null +++ b/src/TinyConfig.Core/Extensions/ConfigurationBuilderExtensions.cs @@ -0,0 +1,38 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using TinyConfig.Abstractions; + +namespace TinyConfig.Core.Extensions +{ + /// + /// configuration builder extensions + /// + public static class ConfigurationBuilderExtensions + { + private const string ENABLED_FLAG = "TinyConfig:Enabled"; + private const string CONFIG_SECTION = "TinyConfig"; + + /// + /// add db configuration to list of config + /// + /// + /// existing configuration value + /// logger instance + /// + public static IConfigurationBuilder AddTinyConfig( + this IConfigurationBuilder builder, IConfiguration configuration, + ILogger logger = null) + where TOptions : class, ITinyConfigOptions, new() + where TStore : class, ITinyConfigStore + { + if (configuration.GetValue(ENABLED_FLAG)) + { + var config = new TOptions(); + configuration.Bind(CONFIG_SECTION, config); + return builder.Add(new TinyConfigSource(config, logger)); + } + + return builder; + } + } +} diff --git a/src/TinyConfig.Core/Extensions/ServiceCollectionExtensions.cs b/src/TinyConfig.Core/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..c42fabd --- /dev/null +++ b/src/TinyConfig.Core/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using TinyConfig.Abstractions; + +namespace TinyConfig.Core.Extensions +{ + /// + /// service collection extensions + /// + public static class ServiceCollectionExtensions + { + /// + /// add tiny configuration reloader + /// + /// + /// + /// concrete implementation for config + /// concrete implementation for store + /// + public static IServiceCollection AddTinyConfigReloader(this IServiceCollection services, + IConfiguration configuration) + where TOptions : class, ITinyConfigOptions, new() + where TStore : class, ITinyConfigStore => + services.AddSingleton((IConfigurationRoot) configuration) + .AddHostedService>(); + } +} diff --git a/src/TinyConfig.Core/TinyConfig.Core.csproj b/src/TinyConfig.Core/TinyConfig.Core.csproj new file mode 100644 index 0000000..fcfc1eb --- /dev/null +++ b/src/TinyConfig.Core/TinyConfig.Core.csproj @@ -0,0 +1,16 @@ + + + + netstandard2.1 + + + + + + + + + + + + diff --git a/src/TinyConfig.Core/TinyConfigChangeWatcher.cs b/src/TinyConfig.Core/TinyConfigChangeWatcher.cs new file mode 100644 index 0000000..115c1df --- /dev/null +++ b/src/TinyConfig.Core/TinyConfigChangeWatcher.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Hosting; +using Microsoft.Extensions.Logging; +using TinyConfig.Abstractions; + +namespace TinyConfig.Core +{ + /// + /// Background service to notify about configuration data changes. + /// + public class TinyConfigChangeWatcher : BackgroundService + where TOptions : class, ITinyConfigOptions + where TStore : class, ITinyConfigStore + { + private readonly ILogger> _logger; + private readonly IEnumerable> _configProviders; + + /// + /// Initializes a new instance of the class. + /// test. + /// + /// + /// + public TinyConfigChangeWatcher(IConfigurationRoot configurationRoot, ILogger> logger) + { + if (configurationRoot == null) + { + throw new ArgumentNullException(nameof(configurationRoot)); + } + + _logger = logger; + _configProviders = configurationRoot.Providers.OfType>().Where(p => p.ConfigurationSource.Options.ReloadOnChange).ToList() !; + } + + /// + protected override async Task ExecuteAsync(CancellationToken stoppingToken) + { + var timers = new Dictionary(); // key - index of config provider, value - timer + var minTime = 6000; + var i = 0; + foreach (var provider in _configProviders) + { + var waitForSec = provider.ConfigurationSource.Options.ReloadDelay; + minTime = Math.Min(minTime, waitForSec); + timers[i] = waitForSec; + i++; + } + + _logger.LogInformation($"DbConfigChangeWatcher will use {minTime} seconds interval"); + + while (!stoppingToken.IsCancellationRequested) + { + await Task.Delay(TimeSpan.FromSeconds(minTime), stoppingToken).ConfigureAwait(false); + if (stoppingToken.IsCancellationRequested) + { + break; + } + + for (var j = 0; j < _configProviders.Count(); j++) + { + var timer = timers[j]; + timer -= minTime; + if (timer <= 0) + { + _configProviders.ElementAt(j).Load(); + timers[j] = _configProviders.ElementAt(j).ConfigurationSource.Options.ReloadDelay; + } + else + { + timers[j] = timer; + } + } + } + } + } +} diff --git a/src/TinyConfig.Core/TinyConfigProvider.cs b/src/TinyConfig.Core/TinyConfigProvider.cs new file mode 100644 index 0000000..0951c8e --- /dev/null +++ b/src/TinyConfig.Core/TinyConfigProvider.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using TinyConfig.Abstractions; + +namespace TinyConfig.Core +{ + /// + /// tiny configuration provider + /// + /// concrete implementation of configuration option + /// concrete implementation of configuration store + public class TinyConfigProvider : ConfigurationProvider + where TOptions : class, ITinyConfigOptions + where TStore : class, ITinyConfigStore + { + /// + /// configuration source + /// + public readonly TinyConfigSource ConfigurationSource; + private readonly ITinyConfigStore _store; + private readonly ILogger _logger; + private readonly Dictionary _versionsCache = new Dictionary(); + + /// + /// ctor + /// + /// + /// + /// + public TinyConfigProvider(ITinyConfigStore store, + TinyConfigSource source, + ILogger logger = null) + { + ConfigurationSource = source; + _store = store; + _logger = logger; + } + + /// + /// load configuration from database + /// + public override void Load() + { + var anyCheck = _store.HasAny(); + anyCheck.Wait(); + if (anyCheck.Result) + { + _logger?.LogDebug("loading configuration from store: {0}", typeof(TStore)); + var reload = LoadDatabaseConfigs(); + _logger?.LogDebug("loaded configuration from store: {0}", typeof(TStore)); + if (reload) OnReload(); + } + } + + /// + /// reload config data + /// + public bool LoadDatabaseConfigs() + { + var reload = false; + var allConfigs = _store.GetAll(); + allConfigs.Wait(); + + foreach (var config in allConfigs.Result) + { + if (!_versionsCache.ContainsKey(config.Id)) + { + Set(config.Id, config.Value); + _versionsCache[config.Id] = config.Version; + reload = true; + } else + { + if (_versionsCache.TryGetValue(config.Id, out byte[] lastVersion) + && !lastVersion.SequenceEqual(config.Version)) + { + Set(config.Id, config.Value); + _versionsCache[config.Id] = config.Version; + reload = true; + } + } + } + + return reload; + } + } +} diff --git a/src/TinyConfig.Core/TinyConfigSource.cs b/src/TinyConfig.Core/TinyConfigSource.cs new file mode 100644 index 0000000..fc3d14c --- /dev/null +++ b/src/TinyConfig.Core/TinyConfigSource.cs @@ -0,0 +1,46 @@ +using System; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using TinyConfig.Abstractions; + +namespace TinyConfig.Core +{ + /// + /// configuration source builder + /// + /// concrete implementation of configuration options + /// concrete implementation of configuration store + public class TinyConfigSource : IConfigurationSource + where TOptions : class, ITinyConfigOptions + where TStore : class, ITinyConfigStore + { + /// + /// configuration options + /// + public TOptions Options; + private readonly ILogger _logger; + + /// + /// ctor + /// + /// + /// + public TinyConfigSource(TOptions options, ILogger logger = null) + { + Options = options; + _logger = logger; + } + + /// + /// build configuration source + /// + /// + /// + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + var constructor = typeof(TStore).GetConstructor(new Type[] { typeof(TOptions) }); + ITinyConfigStore store = constructor.Invoke(new object[] { Options }) as ITinyConfigStore; + return new TinyConfigProvider(store, this, _logger); + } + } +} diff --git a/src/TinyConfig.SimpleDB/Core/SimpleDbConfigOptions.cs b/src/TinyConfig.SimpleDB/Core/SimpleDbConfigOptions.cs new file mode 100644 index 0000000..29653fd --- /dev/null +++ b/src/TinyConfig.SimpleDB/Core/SimpleDbConfigOptions.cs @@ -0,0 +1,35 @@ +using TinyConfig.Abstractions; + +namespace TinyConfig.SimpleDB.Core +{ + /// + /// simple db config options + /// + public class SimpleDbConfigOptions : ITinyConfigOptions + { + /// + /// connection string + /// + public string ConnectionString { get; } + + /// + /// table name for storing settings + /// + public string TableName { get; } + + /// + /// encryption key for sensitive credentials + /// + public string EncryptionKey { get; set; } + + /// + /// indicate whether to reload on change + /// + public bool ReloadOnChange { get; set; } + + /// + /// wait time before retrying reloading for changes + /// + public int ReloadDelay { get; set; } + } +} diff --git a/src/TinyConfig.SimpleDB/Core/SimpleDbConfigStore.cs b/src/TinyConfig.SimpleDB/Core/SimpleDbConfigStore.cs new file mode 100644 index 0000000..6ed3dd1 --- /dev/null +++ b/src/TinyConfig.SimpleDB/Core/SimpleDbConfigStore.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Data.SqlClient; +using Dapper; +using TinyConfig.Abstractions; + +namespace TinyConfig.SimpleDB.Core +{ + /// + /// simple db config store + /// + public class SimpleDbConfigStore : ITinyConfigStore + { + private readonly SimpleDbConfigOptions _options; + + public SimpleDbConfigStore(SimpleDbConfigOptions options) + { + _options = options; + } + + /// + public async Task> GetAll() + { + using var conn = CreateConnection(); + return await conn.QueryAsync($"SELECT * FROM {_options.TableName}"); + } + + /// + public async Task HasAny() + { + using var conn = CreateConnection(); + var count = await conn.ExecuteScalarAsync($"SELECT COUNT(*) FROM {_options.TableName}"); + return count > 0; + } + + /// + public Task HasChanged(object versionToken) + { + throw new System.NotImplementedException(); + } + + /// + /// create new + /// + /// + private SqlConnection CreateConnection() => new SqlConnection(_options.ConnectionString); + } +} diff --git a/src/TinyConfig.SimpleDB/Core/SimpleSetting.cs b/src/TinyConfig.SimpleDB/Core/SimpleSetting.cs new file mode 100644 index 0000000..1b00840 --- /dev/null +++ b/src/TinyConfig.SimpleDB/Core/SimpleSetting.cs @@ -0,0 +1,24 @@ +using TinyConfig.Abstractions; + +namespace TinyConfig.SimpleDB.Core +{ + /// + /// simple setting model + /// + public class SimpleSetting : ITinySetting + { + /// + public string Id { get; set; } + + /// + public string Value { get; set; } + + /// + public byte[] Version { get; set; } + + /// + /// indicate if field is a secret + /// + public bool IsSecret { get; set; } + } +} diff --git a/src/TinyConfig.SimpleDB/Cryptography/AesCrypto.cs b/src/TinyConfig.SimpleDB/Cryptography/AesCrypto.cs new file mode 100644 index 0000000..513b665 --- /dev/null +++ b/src/TinyConfig.SimpleDB/Cryptography/AesCrypto.cs @@ -0,0 +1,114 @@ +using System; +using System.IO; +using System.Security.Cryptography; +using System.Text; + +namespace TinyConfig.SimpleDB.Cryptography +{ + /// + /// AES helper + /// + public class AesCrypto + { + private readonly byte[] _passwordBytes; + private const int SALT_SIZE = 16; + + /// + /// creates a new + /// + /// + public AesCrypto(string password) + { + _passwordBytes = Encoding.UTF8.GetBytes(password); + } + + #region helper methods + private static byte[] GenerateSalt() + { + // initialize a byte array to hold salt + var data = new byte[SALT_SIZE]; + // create an instance of random number generator + using var rng = new RNGCryptoServiceProvider(); + // generate random numbers and fill salt with the generated value + for (int i = 0; i < 10; i++) rng.GetBytes(data); + // return filled byte array + return data; + } + + private RijndaelManaged CreateAes(byte[] salt) + { + const int keySize = 256; + const int blockSize = 128; + const int ITERATIONS = 200; + // create key from password and salt + var key = new Rfc2898DeriveBytes(_passwordBytes, salt, ITERATIONS); + + // create an managed aes instance + return new RijndaelManaged() + { + KeySize = keySize, + BlockSize = blockSize, + Mode = CipherMode.CBC, + Padding = PaddingMode.PKCS7, + Key = key.GetBytes(keySize / 8), + IV = key.GetBytes(blockSize / 8) + }; + } + #endregion + + #region Encrypt + /// + /// encrypt and return encrypted string + /// + /// + public string Encrypt(string data) + { + // generate salt, unique per call + var salt = GenerateSalt(); + + // create an managed aes instance + var aes = CreateAes(salt); + + // write salt to output file + using var resultStream = new MemoryStream(); + resultStream.Write(salt, 0, salt.Length); + + // create a crypto stream in write mode with outputfile and configured aes instance + using(var cryptoStream = new CryptoStream(resultStream, aes.CreateEncryptor(), CryptoStreamMode.Write, true)) + { + using var swEncrypt = new StreamWriter(cryptoStream, Encoding.UTF8); + //Write all data to the stream. + swEncrypt.Write(data); + } + + resultStream.Position = 0; + return Convert.ToBase64String(resultStream.ToArray()); + } + #endregion + + #region Decrypt + /// + /// decrypt and return decrypted string + /// + public string Decrypt(string data) + { + var plainText = string.Empty; + var salt = new byte[SALT_SIZE]; + using var inputStream = new MemoryStream(Convert.FromBase64String(data)); + inputStream.Read(salt, 0, salt.Length); + // create an managed aes instance + var aes = CreateAes(salt); + + // create a crypto stream in read mode with input file and configured aes instance + using(var cryptoStream = new CryptoStream(inputStream, aes.CreateDecryptor(), CryptoStreamMode.Read, true)) + { + using var srDecrypt = new StreamReader(cryptoStream); + // Read the decrypted bytes from the decrypting stream + // and place them in a string. + plainText = srDecrypt.ReadToEnd(); + } + return plainText; + } + #endregion + } +} diff --git a/src/TinyConfig.SimpleDB/TinyConfig.SimpleDB.csproj b/src/TinyConfig.SimpleDB/TinyConfig.SimpleDB.csproj new file mode 100644 index 0000000..c9bdd63 --- /dev/null +++ b/src/TinyConfig.SimpleDB/TinyConfig.SimpleDB.csproj @@ -0,0 +1,17 @@ + + + + netstandard2.1 + + + + + + + + + + + + + From 1ba94d8bf0b900bdfadd97db1ce8cf7fd809c72b Mon Sep 17 00:00:00 2001 From: Mubarak Imam Date: Sun, 30 May 2021 23:48:30 +0100 Subject: [PATCH 2/3] FEAT-1: added crypto to simple db config - added extensions methods. --- .../Core/SimpleDbConfigStore.cs | 12 ++++++++- .../ConfigurationBuilderExtensions.cs | 25 +++++++++++++++++++ .../Extensions/ServiceCollectionExtensions.cs | 23 +++++++++++++++++ 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 src/TinyConfig.SimpleDB/Extensions/ConfigurationBuilderExtensions.cs create mode 100644 src/TinyConfig.SimpleDB/Extensions/ServiceCollectionExtensions.cs diff --git a/src/TinyConfig.SimpleDB/Core/SimpleDbConfigStore.cs b/src/TinyConfig.SimpleDB/Core/SimpleDbConfigStore.cs index 6ed3dd1..d23b7c0 100644 --- a/src/TinyConfig.SimpleDB/Core/SimpleDbConfigStore.cs +++ b/src/TinyConfig.SimpleDB/Core/SimpleDbConfigStore.cs @@ -1,8 +1,11 @@ using System.Collections.Generic; +using System.Linq; using System.Threading.Tasks; using Microsoft.Data.SqlClient; using Dapper; using TinyConfig.Abstractions; +using TinyConfig.SimpleDB.Cryptography; + namespace TinyConfig.SimpleDB.Core { @@ -12,17 +15,24 @@ namespace TinyConfig.SimpleDB.Core public class SimpleDbConfigStore : ITinyConfigStore { private readonly SimpleDbConfigOptions _options; + private readonly AesCrypto _aes; public SimpleDbConfigStore(SimpleDbConfigOptions options) { _options = options; + _aes = new AesCrypto(options.EncryptionKey); } /// public async Task> GetAll() { using var conn = CreateConnection(); - return await conn.QueryAsync($"SELECT * FROM {_options.TableName}"); + var settings = await conn.QueryAsync($"SELECT * FROM {_options.TableName}"); + + return settings.Select(entry => { + if (entry.IsSecret) entry.Value = _aes.Decrypt(entry.Value); + return entry; + }); } /// diff --git a/src/TinyConfig.SimpleDB/Extensions/ConfigurationBuilderExtensions.cs b/src/TinyConfig.SimpleDB/Extensions/ConfigurationBuilderExtensions.cs new file mode 100644 index 0000000..fc13b78 --- /dev/null +++ b/src/TinyConfig.SimpleDB/Extensions/ConfigurationBuilderExtensions.cs @@ -0,0 +1,25 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; +using TinyConfig.Core.Extensions; +using TinyConfig.SimpleDB.Core; + +namespace TinyConfig.SimpleDB.Extensions +{ + /// + /// configuration builder extensions + /// + public static class ConfigurationBuilderExtensions + { + /// + /// add db configuration to list of config + /// + /// + /// existing configuration value + /// logger instance + /// + public static IConfigurationBuilder AddSimpleDbConfig( + this IConfigurationBuilder builder, IConfiguration configuration, + ILogger logger = null) => + builder.AddTinyConfig(configuration, logger); + } +} diff --git a/src/TinyConfig.SimpleDB/Extensions/ServiceCollectionExtensions.cs b/src/TinyConfig.SimpleDB/Extensions/ServiceCollectionExtensions.cs new file mode 100644 index 0000000..d3d91a4 --- /dev/null +++ b/src/TinyConfig.SimpleDB/Extensions/ServiceCollectionExtensions.cs @@ -0,0 +1,23 @@ +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using TinyConfig.Core.Extensions; +using TinyConfig.SimpleDB.Core; + +namespace TinyConfig.SimpleDB.Extensions +{ + /// + /// service collection extensions + /// + public static class ServiceCollectionExtensions + { + /// + /// add database configuration reloader + /// + /// + /// + /// + public static IServiceCollection AddSimpleDbConfigReloader(this IServiceCollection services, + IConfiguration configuration) => + services.AddTinyConfigReloader(configuration); + } +} From d033455c8a865f8e44b68e52890b02472f965fc1 Mon Sep 17 00:00:00 2001 From: Mubarak Imam Date: Wed, 2 Jun 2021 18:09:42 +0100 Subject: [PATCH 3/3] FEAT-1: renamed SimpleDbConfig to SqlServerConfig --- .../ITinyConfigStore.cs | 12 +--- src/TinyConfig.Core/TinyConfigProvider.cs | 15 +++-- .../Core/SimpleDbConfigStore.cs | 58 ------------------ .../Core/SimpleSetting.cs | 8 ++- .../Core/SqlServerConfigOptions.cs} | 10 +++- .../Core/SqlServerConfigStore.cs | 60 +++++++++++++++++++ .../Cryptography/AesCrypto.cs | 2 +- .../ConfigurationBuilderExtensions.cs | 8 +-- .../Extensions/ServiceCollectionExtensions.cs | 8 +-- .../TinyConfig.SqlServer.csproj} | 0 10 files changed, 96 insertions(+), 85 deletions(-) delete mode 100644 src/TinyConfig.SimpleDB/Core/SimpleDbConfigStore.cs rename src/{TinyConfig.SimpleDB => TinyConfig.SqlServer}/Core/SimpleSetting.cs (69%) rename src/{TinyConfig.SimpleDB/Core/SimpleDbConfigOptions.cs => TinyConfig.SqlServer/Core/SqlServerConfigOptions.cs} (73%) create mode 100644 src/TinyConfig.SqlServer/Core/SqlServerConfigStore.cs rename src/{TinyConfig.SimpleDB => TinyConfig.SqlServer}/Cryptography/AesCrypto.cs (98%) rename src/{TinyConfig.SimpleDB => TinyConfig.SqlServer}/Extensions/ConfigurationBuilderExtensions.cs (74%) rename src/{TinyConfig.SimpleDB => TinyConfig.SqlServer}/Extensions/ServiceCollectionExtensions.cs (66%) rename src/{TinyConfig.SimpleDB/TinyConfig.SimpleDB.csproj => TinyConfig.SqlServer/TinyConfig.SqlServer.csproj} (100%) diff --git a/src/TinyConfig.Abstractions/ITinyConfigStore.cs b/src/TinyConfig.Abstractions/ITinyConfigStore.cs index a6cb576..65aeb93 100644 --- a/src/TinyConfig.Abstractions/ITinyConfigStore.cs +++ b/src/TinyConfig.Abstractions/ITinyConfigStore.cs @@ -13,18 +13,12 @@ public interface ITinyConfigStore /// /// /// true is settings has changed - Task HasChanged(object versionToken); - - /// - /// check if config store has any entry - /// - /// true if has at least one entry - Task HasAny(); + Task HasChanged(object versionToken = null); /// /// get all settings from configuration store /// - /// a list containing all configuration key value pairs - Task> GetAll(); + /// a list containing all configuration key value pairs, with table version token + Task<(IEnumerable, object)> GetAllWithVersionToken(); } } diff --git a/src/TinyConfig.Core/TinyConfigProvider.cs b/src/TinyConfig.Core/TinyConfigProvider.cs index 0951c8e..a5caf27 100644 --- a/src/TinyConfig.Core/TinyConfigProvider.cs +++ b/src/TinyConfig.Core/TinyConfigProvider.cs @@ -23,6 +23,7 @@ public class TinyConfigProvider : ConfigurationProvider private readonly ITinyConfigStore _store; private readonly ILogger _logger; private readonly Dictionary _versionsCache = new Dictionary(); + private object VersionToken; /// /// ctor @@ -44,7 +45,7 @@ public TinyConfigProvider(ITinyConfigStore store, /// public override void Load() { - var anyCheck = _store.HasAny(); + var anyCheck = _store.HasChanged(VersionToken); anyCheck.Wait(); if (anyCheck.Result) { @@ -61,10 +62,12 @@ public override void Load() public bool LoadDatabaseConfigs() { var reload = false; - var allConfigs = _store.GetAll(); + var allConfigs = _store.GetAllWithVersionToken(); allConfigs.Wait(); - foreach (var config in allConfigs.Result) + var (settings, lastVersion) = allConfigs.Result; + + foreach (var config in settings) { if (!_versionsCache.ContainsKey(config.Id)) { @@ -73,8 +76,8 @@ public bool LoadDatabaseConfigs() reload = true; } else { - if (_versionsCache.TryGetValue(config.Id, out byte[] lastVersion) - && !lastVersion.SequenceEqual(config.Version)) + if (_versionsCache.TryGetValue(config.Id, out byte[] version) + && !version.SequenceEqual(config.Version)) { Set(config.Id, config.Value); _versionsCache[config.Id] = config.Version; @@ -83,6 +86,8 @@ public bool LoadDatabaseConfigs() } } + VersionToken = lastVersion; + return reload; } } diff --git a/src/TinyConfig.SimpleDB/Core/SimpleDbConfigStore.cs b/src/TinyConfig.SimpleDB/Core/SimpleDbConfigStore.cs deleted file mode 100644 index d23b7c0..0000000 --- a/src/TinyConfig.SimpleDB/Core/SimpleDbConfigStore.cs +++ /dev/null @@ -1,58 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using System.Threading.Tasks; -using Microsoft.Data.SqlClient; -using Dapper; -using TinyConfig.Abstractions; -using TinyConfig.SimpleDB.Cryptography; - - -namespace TinyConfig.SimpleDB.Core -{ - /// - /// simple db config store - /// - public class SimpleDbConfigStore : ITinyConfigStore - { - private readonly SimpleDbConfigOptions _options; - private readonly AesCrypto _aes; - - public SimpleDbConfigStore(SimpleDbConfigOptions options) - { - _options = options; - _aes = new AesCrypto(options.EncryptionKey); - } - - /// - public async Task> GetAll() - { - using var conn = CreateConnection(); - var settings = await conn.QueryAsync($"SELECT * FROM {_options.TableName}"); - - return settings.Select(entry => { - if (entry.IsSecret) entry.Value = _aes.Decrypt(entry.Value); - return entry; - }); - } - - /// - public async Task HasAny() - { - using var conn = CreateConnection(); - var count = await conn.ExecuteScalarAsync($"SELECT COUNT(*) FROM {_options.TableName}"); - return count > 0; - } - - /// - public Task HasChanged(object versionToken) - { - throw new System.NotImplementedException(); - } - - /// - /// create new - /// - /// - private SqlConnection CreateConnection() => new SqlConnection(_options.ConnectionString); - } -} diff --git a/src/TinyConfig.SimpleDB/Core/SimpleSetting.cs b/src/TinyConfig.SqlServer/Core/SimpleSetting.cs similarity index 69% rename from src/TinyConfig.SimpleDB/Core/SimpleSetting.cs rename to src/TinyConfig.SqlServer/Core/SimpleSetting.cs index 1b00840..6f474d4 100644 --- a/src/TinyConfig.SimpleDB/Core/SimpleSetting.cs +++ b/src/TinyConfig.SqlServer/Core/SimpleSetting.cs @@ -1,6 +1,7 @@ +using System; using TinyConfig.Abstractions; -namespace TinyConfig.SimpleDB.Core +namespace TinyConfig.SqlServer.Core { /// /// simple setting model @@ -20,5 +21,10 @@ public class SimpleSetting : ITinySetting /// indicate if field is a secret /// public bool IsSecret { get; set; } + + /// + /// date entry was last modified + /// + public DateTime LastModifiedOn { get; set; } } } diff --git a/src/TinyConfig.SimpleDB/Core/SimpleDbConfigOptions.cs b/src/TinyConfig.SqlServer/Core/SqlServerConfigOptions.cs similarity index 73% rename from src/TinyConfig.SimpleDB/Core/SimpleDbConfigOptions.cs rename to src/TinyConfig.SqlServer/Core/SqlServerConfigOptions.cs index 29653fd..83190cc 100644 --- a/src/TinyConfig.SimpleDB/Core/SimpleDbConfigOptions.cs +++ b/src/TinyConfig.SqlServer/Core/SqlServerConfigOptions.cs @@ -1,25 +1,29 @@ +using System.Diagnostics.CodeAnalysis; using TinyConfig.Abstractions; -namespace TinyConfig.SimpleDB.Core +namespace TinyConfig.SqlServer.Core { /// /// simple db config options /// - public class SimpleDbConfigOptions : ITinyConfigOptions + public class SqlServerConfigOptions : ITinyConfigOptions { /// /// connection string /// + [NotNull] public string ConnectionString { get; } /// /// table name for storing settings /// + [NotNull] public string TableName { get; } /// /// encryption key for sensitive credentials /// + [NotNull] public string EncryptionKey { get; set; } /// @@ -30,6 +34,6 @@ public class SimpleDbConfigOptions : ITinyConfigOptions /// /// wait time before retrying reloading for changes /// - public int ReloadDelay { get; set; } + public int ReloadDelay { get; set; } = 60; } } diff --git a/src/TinyConfig.SqlServer/Core/SqlServerConfigStore.cs b/src/TinyConfig.SqlServer/Core/SqlServerConfigStore.cs new file mode 100644 index 0000000..0b63991 --- /dev/null +++ b/src/TinyConfig.SqlServer/Core/SqlServerConfigStore.cs @@ -0,0 +1,60 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Data.SqlClient; +using Dapper; +using TinyConfig.Abstractions; +using TinyConfig.SqlServer.Cryptography; + +namespace TinyConfig.SqlServer.Core +{ + /// + /// simple db config store + /// + public class SqlServerConfigStore : ITinyConfigStore + { + private readonly SqlServerConfigOptions _options; + private readonly AesCrypto _aes; + + public SqlServerConfigStore(SqlServerConfigOptions options) + { + _options = options; + _aes = new AesCrypto(options.EncryptionKey); + } + + /// + public async Task<(IEnumerable, object)> GetAllWithVersionToken() + { + using var conn = CreateConnection(); + var result = await conn.QueryMultipleAsync($"{SelectAllQuery}; {LastVersionQuery}"); + var settings = await result.ReadAsync(); + var lastVersion = await result.ReadAsync(); + + return (settings.Select(entry => { + if (entry.IsSecret) entry.Value = _aes.Decrypt(entry.Value); + return entry; + }), lastVersion); + } + + /// + public async Task HasChanged(object versionToken = null) + { + using var conn = CreateConnection(); + var mostRecentLastModified = await conn.ExecuteScalarAsync(LastVersionQuery); + if (mostRecentLastModified.HasValue && versionToken != null) + return mostRecentLastModified.Value.CompareTo(versionToken) != 0; + return mostRecentLastModified.HasValue; + } + + /// + /// create new + /// + /// + private SqlConnection CreateConnection() => new SqlConnection(_options.ConnectionString); + + private string SelectAllQuery => $"SELECT * FROM {_options.TableName}"; + + private string LastVersionQuery => $"SELECT TOP 1 LastModifiedOn FROM {_options.TableName} ORDER BY LastModifiedOn DESC"; + } +} diff --git a/src/TinyConfig.SimpleDB/Cryptography/AesCrypto.cs b/src/TinyConfig.SqlServer/Cryptography/AesCrypto.cs similarity index 98% rename from src/TinyConfig.SimpleDB/Cryptography/AesCrypto.cs rename to src/TinyConfig.SqlServer/Cryptography/AesCrypto.cs index 513b665..ebf832c 100644 --- a/src/TinyConfig.SimpleDB/Cryptography/AesCrypto.cs +++ b/src/TinyConfig.SqlServer/Cryptography/AesCrypto.cs @@ -3,7 +3,7 @@ using System.Security.Cryptography; using System.Text; -namespace TinyConfig.SimpleDB.Cryptography +namespace TinyConfig.SqlServer.Cryptography { /// /// AES helper diff --git a/src/TinyConfig.SimpleDB/Extensions/ConfigurationBuilderExtensions.cs b/src/TinyConfig.SqlServer/Extensions/ConfigurationBuilderExtensions.cs similarity index 74% rename from src/TinyConfig.SimpleDB/Extensions/ConfigurationBuilderExtensions.cs rename to src/TinyConfig.SqlServer/Extensions/ConfigurationBuilderExtensions.cs index fc13b78..718f67e 100644 --- a/src/TinyConfig.SimpleDB/Extensions/ConfigurationBuilderExtensions.cs +++ b/src/TinyConfig.SqlServer/Extensions/ConfigurationBuilderExtensions.cs @@ -1,9 +1,9 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using TinyConfig.Core.Extensions; -using TinyConfig.SimpleDB.Core; +using TinyConfig.SqlServer.Core; -namespace TinyConfig.SimpleDB.Extensions +namespace TinyConfig.SqlServer.Extensions { /// /// configuration builder extensions @@ -17,9 +17,9 @@ public static class ConfigurationBuilderExtensions /// existing configuration value /// logger instance /// - public static IConfigurationBuilder AddSimpleDbConfig( + public static IConfigurationBuilder AddSqlServerConfig( this IConfigurationBuilder builder, IConfiguration configuration, ILogger logger = null) => - builder.AddTinyConfig(configuration, logger); + builder.AddTinyConfig(configuration, logger); } } diff --git a/src/TinyConfig.SimpleDB/Extensions/ServiceCollectionExtensions.cs b/src/TinyConfig.SqlServer/Extensions/ServiceCollectionExtensions.cs similarity index 66% rename from src/TinyConfig.SimpleDB/Extensions/ServiceCollectionExtensions.cs rename to src/TinyConfig.SqlServer/Extensions/ServiceCollectionExtensions.cs index d3d91a4..1d82e63 100644 --- a/src/TinyConfig.SimpleDB/Extensions/ServiceCollectionExtensions.cs +++ b/src/TinyConfig.SqlServer/Extensions/ServiceCollectionExtensions.cs @@ -1,9 +1,9 @@ using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using TinyConfig.Core.Extensions; -using TinyConfig.SimpleDB.Core; +using TinyConfig.SqlServer.Core; -namespace TinyConfig.SimpleDB.Extensions +namespace TinyConfig.SqlServer.Extensions { /// /// service collection extensions @@ -16,8 +16,8 @@ public static class ServiceCollectionExtensions /// /// /// - public static IServiceCollection AddSimpleDbConfigReloader(this IServiceCollection services, + public static IServiceCollection AddSqlServerConfigReloader(this IServiceCollection services, IConfiguration configuration) => - services.AddTinyConfigReloader(configuration); + services.AddTinyConfigReloader(configuration); } } diff --git a/src/TinyConfig.SimpleDB/TinyConfig.SimpleDB.csproj b/src/TinyConfig.SqlServer/TinyConfig.SqlServer.csproj similarity index 100% rename from src/TinyConfig.SimpleDB/TinyConfig.SimpleDB.csproj rename to src/TinyConfig.SqlServer/TinyConfig.SqlServer.csproj