diff --git a/Core/Configurations/ConfigurationManager/AppConfiguration.cs b/Core/Configurations/ConfigurationManager/AppConfiguration.cs deleted file mode 100644 index 25adc81..0000000 --- a/Core/Configurations/ConfigurationManager/AppConfiguration.cs +++ /dev/null @@ -1,14 +0,0 @@ -namespace Configuration.Core; -public class AppConfiguration -{ - public long Id { get; set; } - public string Key { get; set; } - public string Value { get; set; } - public string Label { get; set; } - public string ContentType { get; set; } - public DateTime? ValidFrom { get; set; } - public DateTime? ExpiresAt { get; set; } - public DateTime? CreatedAt { get; set; } - public DateTime? ModifiedAt { get; set; } - public Guid? Etag { get; set; } -} \ No newline at end of file diff --git a/Core/Configurations/ConfigurationManager/ConfigurationExtensions.cs b/Core/Configurations/ConfigurationManager/ConfigurationExtensions.cs deleted file mode 100644 index 62ad83b..0000000 --- a/Core/Configurations/ConfigurationManager/ConfigurationExtensions.cs +++ /dev/null @@ -1,28 +0,0 @@ -using Configuration.Core; -using Microsoft.Extensions.Configuration; - -namespace Core.Configurations.ConfigurationManager -{ - public static class ConfigurationExtensions - { - public static T Get(this IConfigurationSection section) where T : class - { - if (section is JsonConfigurationSection jsonSection) - { - var token = jsonSection.GetToken(); - return token?.ToObject(); - } - throw new InvalidOperationException("Section is not a JsonConfigurationSection"); - } - - public static T GetValue(this IConfigurationSection section, string key) - { - if (section is JsonConfigurationSection jsonSection) - { - var token = jsonSection.GetToken().SelectToken(key.Replace(":", ".")); - return token.ToObject(); - } - throw new InvalidOperationException("Section is not a JsonConfigurationSection"); - } - } -} diff --git a/Core/Configurations/ConfigurationManager/ConfigurationRepository.cs b/Core/Configurations/ConfigurationManager/ConfigurationRepository.cs deleted file mode 100644 index 60a44a1..0000000 --- a/Core/Configurations/ConfigurationManager/ConfigurationRepository.cs +++ /dev/null @@ -1,24 +0,0 @@ -using System.Data; -using Insight.Database; - -namespace Configuration.Core; -public class ConfigurationRepository : IConfigurationRepository -{ - private readonly IDbConnection _connection; - - public ConfigurationRepository(IDbConnection connection) - { - _connection = connection; - } - - public async Task> GetActiveConfigurations() - { - const string sql = @" - SELECT id, key, value, label, content_type, valid_from, expires_at, created_at, modified_at, etag - FROM prod.app_configuration - WHERE (expires_at IS NULL OR expires_at <= CURRENT_TIMESTAMP) - AND (valid_from IS NULL OR valid_from >= CURRENT_TIMESTAMP)"; - - return await _connection.QueryAsync(sql); - } -} \ No newline at end of file diff --git a/Core/Configurations/ConfigurationManager/IConfigurationRepository.cs b/Core/Configurations/ConfigurationManager/IConfigurationRepository.cs deleted file mode 100644 index 5e1fc74..0000000 --- a/Core/Configurations/ConfigurationManager/IConfigurationRepository.cs +++ /dev/null @@ -1,5 +0,0 @@ -namespace Configuration.Core; - public interface IConfigurationRepository -{ - Task> GetActiveConfigurations(); -} \ No newline at end of file diff --git a/Core/Configurations/ConfigurationManager/JsonConfiguration.cs b/Core/Configurations/ConfigurationManager/JsonConfiguration.cs deleted file mode 100644 index ce0122f..0000000 --- a/Core/Configurations/ConfigurationManager/JsonConfiguration.cs +++ /dev/null @@ -1,31 +0,0 @@ -using Newtonsoft.Json.Linq; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Primitives; -using System.Data; - -namespace Configuration.Core; - public class JsonConfiguration : IConfiguration -{ - private readonly JObject _data; - private readonly IChangeToken _changeToken; - - public JsonConfiguration(JObject data, IChangeToken changeToken) - { - _data = data; - _changeToken = changeToken; - } - - public string this[string key] - { - get => _data.SelectToken(key.Replace(":", "."))?.ToString(); - set => throw new NotImplementedException(); - } - - public IConfigurationSection GetSection(string key) => - new JsonConfigurationSection(_data, key); - - public IEnumerable GetChildren() => - _data.Properties().Select(p => new JsonConfigurationSection(_data, p.Name)); - - public IChangeToken GetReloadToken() => _changeToken; -} \ No newline at end of file diff --git a/Core/Configurations/ConfigurationManager/JsonConfigurationSection.cs b/Core/Configurations/ConfigurationManager/JsonConfigurationSection.cs deleted file mode 100644 index 7ffd90b..0000000 --- a/Core/Configurations/ConfigurationManager/JsonConfigurationSection.cs +++ /dev/null @@ -1,80 +0,0 @@ -using Newtonsoft.Json.Linq; -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Primitives; - -namespace Configuration.Core -{ - public class JsonConfigurationSection : IConfigurationSection - { - private readonly JObject _data; - private readonly string _path; - private readonly string _normalizedPath; - - public JsonConfigurationSection(JObject data, string path) - { - _data = data ?? throw new ArgumentNullException(nameof(data)); - _path = path ?? throw new ArgumentNullException(nameof(path)); - _normalizedPath = NormalizePath(_path); - } - - public string this[string key] - { - get - { - if (string.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key)); - - var token = _data.SelectToken($"{_normalizedPath}.{NormalizePath(key)}"); - return token?.ToString(); - } - set => throw new NotImplementedException("Setting values is not supported."); - } - - public string Key => _path.Split(':').Last(); - public string Path => _path; - - public string Value - { - get - { - var token = _data.SelectToken(_normalizedPath); - return token?.ToString(); - } - set => throw new NotImplementedException("Setting values is not supported."); - } - - public IConfigurationSection GetSection(string key) - { - if (string.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key)); - - return new JsonConfigurationSection(_data, string.IsNullOrEmpty(_path) ? key : $"{_path}:{key}"); - } - - public JToken GetToken() => _data.SelectToken(_normalizedPath); - - public IEnumerable GetChildren() - { - var token = _data.SelectToken(_normalizedPath); - if (token is JObject obj) - { - return obj.Properties() - .Select(p => new JsonConfigurationSection(_data, string.IsNullOrEmpty(_path) ? p.Name : $"{_path}:{p.Name}")); - } - return Enumerable.Empty(); - } - - public T Get() where T : class - { - var token = _data.SelectToken(_normalizedPath); - return token?.ToObject(); - } - - public IChangeToken GetReloadToken() => new ConfigurationReloadToken(); - - private static string NormalizePath(string path) - { - return path?.Replace(":", ".", StringComparison.Ordinal) ?? string.Empty; - } - } -} \ No newline at end of file diff --git a/Core/Configurations/ConfigurationManager/KeyValueConfigurationBuilder.cs b/Core/Configurations/ConfigurationManager/KeyValueConfigurationBuilder.cs deleted file mode 100644 index 9b636b0..0000000 --- a/Core/Configurations/ConfigurationManager/KeyValueConfigurationBuilder.cs +++ /dev/null @@ -1,103 +0,0 @@ -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; -using Microsoft.Extensions.Configuration; - -namespace Configuration.Core -{ - public class KeyValueConfigurationBuilder - { - private readonly IConfigurationRepository _repository; - private readonly JObject _rootObject = new(); - private ConfigurationReloadToken _reloadToken = new(); - private IConfiguration _configuration; - private readonly object _configurationLock = new(); - - public KeyValueConfigurationBuilder(IConfigurationRepository repository) - { - _repository = repository ?? throw new ArgumentNullException(nameof(repository)); - } - - /// - /// Loads configurations from the repository and builds the configuration tree. - /// - public async Task LoadConfiguration() - { - try - { - var configurations = await _repository.GetActiveConfigurations(); - foreach (var config in configurations) - { - AddKeyValue(config.Key, config.Value); - } - OnReload(); - } - catch (Exception ex) - { - // Log the exception or handle it as needed - throw new InvalidOperationException("Failed to load configurations.", ex); - } - } - - /// - /// Adds a key-value pair to the configuration tree. - /// - /// The key to add. - /// The JSON value to add. - public void AddKeyValue(string key, string jsonValue) - { - if (string.IsNullOrEmpty(key)) - throw new ArgumentNullException(nameof(key)); - if (string.IsNullOrEmpty(jsonValue)) - throw new ArgumentNullException(nameof(jsonValue)); - - try - { - var valueObject = JsonConvert.DeserializeObject(jsonValue); - var parts = key.Split(':'); - - JObject current = _rootObject; - for (int i = 0; i < parts.Length - 1; i++) - { - var part = parts[i]; - if (!current.ContainsKey(part)) - { - current[part] = new JObject(); - } - current = (JObject)current[part]; - } - - current[parts[^1]] = valueObject; - } - catch (JsonException ex) - { - throw new ArgumentException("Invalid JSON value.", nameof(jsonValue), ex); - } - } - - private void OnReload() - { - var previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken()); - previousToken.OnReload(); - _configuration = null; // Reset the configuration to force a rebuild - } - - /// - /// Builds the configuration instance. - /// - /// The built instance. - public IConfiguration Build() - { - if (_configuration == null) - { - lock (_configurationLock) - { - if (_configuration == null) - { - _configuration = new JsonConfiguration(_rootObject, _reloadToken); - } - } - } - return _configuration; - } - } -} \ No newline at end of file diff --git a/Core/Configurations/SmartConfiguration/AppConfiguration.cs b/Core/Configurations/SmartConfiguration/AppConfiguration.cs new file mode 100644 index 0000000..0dc9b98 --- /dev/null +++ b/Core/Configurations/SmartConfiguration/AppConfiguration.cs @@ -0,0 +1,14 @@ +namespace Core.Configurations.SmartConfiguration; +public class AppConfiguration +{ + public long Id { get; set; } + public string Key { get; set; } + public string Value { get; set; } + public string Label { get; set; } + public string ContentType { get; set; } + public DateTime? ValidFrom { get; set; } + public DateTime? ExpiresAt { get; set; } + public DateTime? CreatedAt { get; set; } + public DateTime? ModifiedAt { get; set; } + public Guid? Etag { get; set; } +} \ No newline at end of file diff --git a/Core/Configurations/SmartConfiguration/ConfigurationExtensions.cs b/Core/Configurations/SmartConfiguration/ConfigurationExtensions.cs new file mode 100644 index 0000000..8b4732a --- /dev/null +++ b/Core/Configurations/SmartConfiguration/ConfigurationExtensions.cs @@ -0,0 +1,27 @@ +using Microsoft.Extensions.Configuration; + +namespace Core.Configurations.SmartConfiguration +{ + public static class ConfigurationExtensions + { + public static T Get(this IConfigurationSection section) where T : class + { + if (section is JsonConfigurationSection jsonSection) + { + var token = jsonSection.GetToken(); + return token?.ToObject(); + } + throw new InvalidOperationException("Section is not a JsonConfigurationSection"); + } + + public static T GetValue(this IConfigurationSection section, string key) + { + if (section is JsonConfigurationSection jsonSection) + { + var token = jsonSection.GetToken().SelectToken(key.Replace(":", ".")); + return token.ToObject(); + } + throw new InvalidOperationException("Section is not a JsonConfigurationSection"); + } + } +} diff --git a/Core/Configurations/SmartConfiguration/ConfigurationRepository.cs b/Core/Configurations/SmartConfiguration/ConfigurationRepository.cs new file mode 100644 index 0000000..8bd78a9 --- /dev/null +++ b/Core/Configurations/SmartConfiguration/ConfigurationRepository.cs @@ -0,0 +1,24 @@ +using System.Data; +using Insight.Database; + +namespace Core.Configurations.SmartConfiguration; +public class ConfigurationRepository : IConfigurationRepository +{ + private readonly IDbConnection _connection; + + public ConfigurationRepository(IDbConnection connection) + { + _connection = connection; + } + + public async Task> GetActiveConfigurations() + { + const string sql = @" + SELECT id, key, value, label, content_type, valid_from, expires_at, created_at, modified_at, etag + FROM prod.app_configuration + WHERE (expires_at IS NULL OR expires_at > CURRENT_TIMESTAMP) + AND (valid_from IS NULL OR valid_from < CURRENT_TIMESTAMP)"; + + return await _connection.QueryAsync(sql); + } +} \ No newline at end of file diff --git a/Core/Configurations/SmartConfiguration/IConfigurationRepository.cs b/Core/Configurations/SmartConfiguration/IConfigurationRepository.cs new file mode 100644 index 0000000..0bd3d14 --- /dev/null +++ b/Core/Configurations/SmartConfiguration/IConfigurationRepository.cs @@ -0,0 +1,5 @@ +namespace Core.Configurations.SmartConfiguration; +public interface IConfigurationRepository +{ + Task> GetActiveConfigurations(); +} \ No newline at end of file diff --git a/Core/Configurations/SmartConfiguration/JsonConfiguration.cs b/Core/Configurations/SmartConfiguration/JsonConfiguration.cs new file mode 100644 index 0000000..670b0ab --- /dev/null +++ b/Core/Configurations/SmartConfiguration/JsonConfiguration.cs @@ -0,0 +1,29 @@ +using Newtonsoft.Json.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Primitives; +using System.Data; + +namespace Core.Configurations.SmartConfiguration; +public class JsonConfiguration : IConfiguration +{ + private readonly JObject _data; + + public JsonConfiguration(JObject data) + { + _data = data; + } + + public string this[string key] + { + get => _data.SelectToken(key.Replace(":", "."))?.ToString(); + set => throw new NotImplementedException(); + } + + public IConfigurationSection GetSection(string key) => + new JsonConfigurationSection(_data, key); + + public IEnumerable GetChildren() => + _data.Properties().Select(p => new JsonConfigurationSection(_data, p.Name)); + + public IChangeToken GetReloadToken() => throw new NotImplementedException(); +} \ No newline at end of file diff --git a/Core/Configurations/SmartConfiguration/JsonConfigurationSection.cs b/Core/Configurations/SmartConfiguration/JsonConfigurationSection.cs new file mode 100644 index 0000000..8a652a2 --- /dev/null +++ b/Core/Configurations/SmartConfiguration/JsonConfigurationSection.cs @@ -0,0 +1,80 @@ +using Newtonsoft.Json.Linq; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Primitives; + +namespace Core.Configurations.SmartConfiguration +{ + public class JsonConfigurationSection : IConfigurationSection + { + private readonly JObject _data; + private readonly string _path; + private readonly string _normalizedPath; + + public JsonConfigurationSection(JObject data, string path) + { + _data = data ?? throw new ArgumentNullException(nameof(data)); + _path = path ?? throw new ArgumentNullException(nameof(path)); + _normalizedPath = NormalizePath(_path); + } + + public string this[string key] + { + get + { + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key)); + + var token = _data.SelectToken($"{_normalizedPath}.{NormalizePath(key)}"); + return token?.ToString(); + } + set => throw new NotImplementedException("Setting values is not supported."); + } + + public string Key => _path.Split(':').Last(); + public string Path => _path; + + public string Value + { + get + { + var token = _data.SelectToken(_normalizedPath); + return token?.ToString(); + } + set => throw new NotImplementedException("Setting values is not supported."); + } + + public IConfigurationSection GetSection(string key) + { + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key)); + + return new JsonConfigurationSection(_data, string.IsNullOrEmpty(_path) ? key : $"{_path}:{key}"); + } + + public JToken GetToken() => _data.SelectToken(_normalizedPath); + + public IEnumerable GetChildren() + { + var token = _data.SelectToken(_normalizedPath); + if (token is JObject obj) + { + return obj.Properties() + .Select(p => new JsonConfigurationSection(_data, string.IsNullOrEmpty(_path) ? p.Name : $"{_path}:{p.Name}")); + } + return Enumerable.Empty(); + } + + public T Get() where T : class + { + var token = _data.SelectToken(_normalizedPath); + return token?.ToObject(); + } + + public IChangeToken GetReloadToken() => new ConfigurationReloadToken(); + + private static string NormalizePath(string path) + { + return path?.Replace(":", ".", StringComparison.Ordinal) ?? string.Empty; + } + } +} \ No newline at end of file diff --git a/Core/Configurations/SmartConfiguration/KeyValueConfigurationBuilder.cs b/Core/Configurations/SmartConfiguration/KeyValueConfigurationBuilder.cs new file mode 100644 index 0000000..1ce921c --- /dev/null +++ b/Core/Configurations/SmartConfiguration/KeyValueConfigurationBuilder.cs @@ -0,0 +1,83 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; +using Microsoft.Extensions.Configuration; + +namespace Core.Configurations.SmartConfiguration +{ + public class KeyValueConfigurationBuilder + { + private readonly IConfigurationRepository _repository; + private readonly JObject _rootObject = new(); + private IConfiguration _configuration; + private readonly object _configurationLock = new(); + + public KeyValueConfigurationBuilder(IConfigurationRepository repository) + { + _repository = repository ?? throw new ArgumentNullException(nameof(repository)); + } + + /// + /// Loads configurations from the repository and builds the configuration tree. + /// + public async Task LoadConfiguration() + { + try + { + var configurations = await _repository.GetActiveConfigurations(); + foreach (var config in configurations) + AddKeyValue(config.Key, config.Value); + + } + catch (Exception ex) + { + // Log the exception or handle it as needed + throw new InvalidOperationException("Failed to load configurations.", ex); + } + } + + /// + /// Adds a key-value pair to the configuration tree. + /// + /// The key to add. + /// The JSON value to add. + public void AddKeyValue(string key, string jsonValue) + { + if (string.IsNullOrEmpty(key)) + throw new ArgumentNullException(nameof(key)); + if (string.IsNullOrEmpty(jsonValue)) + throw new ArgumentNullException(nameof(jsonValue)); + + try + { + var valueObject = JsonConvert.DeserializeObject(jsonValue); + var parts = key.Split(':'); + + JObject current = _rootObject; + for (int i = 0; i < parts.Length - 1; i++) + { + var part = parts[i]; + if (!current.ContainsKey(part)) + { + current[part] = new JObject(); + } + current = (JObject)current[part]; + } + + current[parts[^1]] = valueObject; + } + catch (JsonException ex) + { + throw new ArgumentException("Invalid JSON value.", nameof(jsonValue), ex); + } + } + /// + /// Builds the configuration instance. + /// + /// The built instance. + public IConfiguration Build() + { + _configuration = new JsonConfiguration(_rootObject); + return _configuration; + } + } +} \ No newline at end of file diff --git a/Core/Configurations/SmartConfiguration/SmartConfigManager.cs b/Core/Configurations/SmartConfiguration/SmartConfigManager.cs new file mode 100644 index 0000000..72f2838 --- /dev/null +++ b/Core/Configurations/SmartConfiguration/SmartConfigManager.cs @@ -0,0 +1,48 @@ + +using Microsoft.Extensions.Configuration; + +namespace Core.Configurations +{ + public class SmartConfigManager + { + private static IConfigurationBuilder _configurationBuilder; + + /// + /// This AppConfigBuilder assumes that AppConfigEndpoint and AppConfigLabelFilter are configured as Settings on Azure for the Application that needs them. + /// AppConfigEndpoint would look like this: Endpoint=https://config-dec-test.azconfig.io;Id=0-l9-s0:foo;Secret=somesecret/bar + /// + /// + /// Path relative to the base path stored in Microsoft.Extensions.Configuration.IConfigurationBuilder.Properties of builder. + /// + public static IConfigurationBuilder AddSmartConfigBuilder(string localSettingsFile) + { + if (_configurationBuilder == null) + { + var envConfiguration = new ConfigurationBuilder() + .AddEnvironmentVariables() + .Build(); + + var appConfigEndpoint = envConfiguration["AppConfigEndpoint"]; + var appConfigLabel = envConfiguration["AppConfigLabelFilter"]; + + _configurationBuilder = new ConfigurationBuilder(); + if (!string.IsNullOrEmpty(appConfigEndpoint)) + { + _configurationBuilder + .AddAzureAppConfiguration(options => + { + options.Connect(appConfigEndpoint); + options.Select(keyFilter: "*", labelFilter: appConfigLabel); + }) + .AddEnvironmentVariables(); + } + else + { + _configurationBuilder.SetBasePath(Directory.GetCurrentDirectory()); + _configurationBuilder.AddJsonFile(localSettingsFile, optional: false); + } + } + return _configurationBuilder; + } + } +} diff --git a/Tests/TestConfigurationManagement.cs b/Tests/TestConfigurationManagement.cs index 0d482a9..9e73fdb 100644 --- a/Tests/TestConfigurationManagement.cs +++ b/Tests/TestConfigurationManagement.cs @@ -5,7 +5,8 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using Tests; -using Core.Configurations.ConfigurationManager; +using Microsoft.Extensions.Configuration; +using Core.Configurations.SmartConfiguration; namespace Configuration.Core.Tests; @@ -141,7 +142,7 @@ public class ConfigurationTests : TestFixture } }; - var configuration = new JsonConfiguration(configData, new Microsoft.Extensions.Configuration.ConfigurationReloadToken()); + IConfiguration configuration = new JsonConfiguration(configData, new Microsoft.Extensions.Configuration.ConfigurationReloadToken()); // Act var welcomeConfig = configuration.GetSection("Email:Templates:Welcome").Get(); diff --git a/Tests/TestFixture.cs b/Tests/TestFixture.cs index c1d48a5..36a2cef 100644 --- a/Tests/TestFixture.cs +++ b/Tests/TestFixture.cs @@ -33,7 +33,7 @@ namespace Tests public virtual IConfigurationRoot Configuration() { - IConfigurationBuilder configBuilder = Core.Configurations.AzureConfigurationManager.AppConfigBuilder("appsettings.dev.json"); + IConfigurationBuilder configBuilder = Core.Configurations.SmartConfigManager.AppConfigBuilder("appsettings.dev.json"); IConfigurationRoot configuration = configBuilder.Build(); return configuration;