Auto stash before merge of "main" and "origin/main"

This commit is contained in:
Janus C. H. Knudsen 2025-02-02 23:13:17 +01:00
parent 21d7128e74
commit 521190475d
41 changed files with 991 additions and 1150 deletions

View file

@ -1,20 +1,19 @@
using Autofac;
using Core.Configurations.JsonConfigProvider;
using Core.Configurations;
namespace PlanTempus
{
public class Startup
{
public Startup(IWebHostEnvironment env)
{
var builder = new ConfigurationBuilder()
.SetBasePath(env.ContentRootPath)
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
.AddEnvironmentVariables();
var builder = new Core.Configurations.ConfigurationBuilder()
.AddJsonFile("appconfiguration.json", optional: true, reloadOnChange: true);
ConfigurationRoot = builder.Build();
}
public IConfigurationRoot ConfigurationRoot { get; private set; }
public Core.Configurations.IConfigurationRoot ConfigurationRoot { get; private set; }
public ILifetimeScope AutofacContainer { get; private set; }
@ -44,6 +43,7 @@ namespace PlanTempus
TelemetryConfig = ConfigurationRoot.GetSection(nameof(Core.ModuleRegistry.TelemetryConfig)).Get<Core.ModuleRegistry.TelemetryConfig>()
});
}
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

View file

@ -5,5 +5,10 @@
},
"TelemetryConfig": {
"ConnectionString": "InstrumentationKey=07d2a2b9-5e8e-4924-836e-264f8438f6c5;IngestionEndpoint=https://northeurope-2.in.applicationinsights.azure.com/;LiveEndpoint=https://northeurope.livediagnostics.monitor.azure.com/;ApplicationId=56748c39-2fa3-4880-a1e2-24068e791548"
},
"Feature": {
"Enabled": true,
"RolloutPercentage": 25,
"AllowedUserGroups": [ "beta" ]
}
}

View file

@ -1,50 +0,0 @@

using Microsoft.Extensions.Configuration;
namespace Core.Configurations
{
public class AzureConfigurationManager
{
private static IConfigurationBuilder _configurationBuilder;
/// <summary>
/// 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
/// </summary>
/// <param name="localSettingsFile">
/// Path relative to the base path stored in Microsoft.Extensions.Configuration.IConfigurationBuilder.Properties of builder.
/// </param>
public static IConfigurationBuilder AppConfigBuilder(string localSettingsFile)
{
if (_configurationBuilder == null)
{
var envConfiguration = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
.AddEnvironmentVariables()
.Build();
System.Diagnostics.Trace.Listeners.Add(new System.Diagnostics.TextWriterTraceListener(Console.Out));
var appConfigEndpoint = envConfiguration["AppConfigEndpoint"];
var appConfigLabel = envConfiguration["AppConfigLabelFilter"];
_configurationBuilder = new Microsoft.Extensions.Configuration.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;
}
}
}

View file

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core.Configurations.Common
{
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
public static class KeyValueToJson
{
public static JObject Convert(IEnumerable<KeyValuePair<string, JToken>> pairs)
{
var root = new JObject();
foreach (var pair in pairs)
{
var keys = pair.Key.Split(':');
var current = root;
// Gennemgå hierarkiet og opret underobjekter, hvis de ikke eksisterer
for (int i = 0; i < keys.Length - 1; i++)
{
var key = keys[i];
if (current[key] == null)
{
current[key] = new JObject();
}
current = (JObject)current[key];
}
// Håndter den sidste nøgle og tilføj værdien
var lastKey = keys[keys.Length - 1];
var value = ConvertValue(pair.Value);
// Hvis den sidste nøgle allerede eksisterer, tilføj til en liste
if (current[lastKey] != null)
{
// Hvis den allerede er en liste, tilføj til listen
if (current[lastKey].Type == JTokenType.Array)
{
((JArray)current[lastKey]).Add(value);
}
// Hvis den ikke er en liste, konverter til en liste
else
{
var existingValue = current[lastKey];
current[lastKey] = new JArray { existingValue, value };
}
}
// Ellers tilføj som en enkelt værdi
else
{
current[lastKey] = value;
}
}
return root;
}
private static JToken ConvertValue(object value)
{
// Hvis værdien allerede er en JToken, returner den direkte
if (value is JToken token)
{
return token;
}
// Konverter andre typer
return value switch
{
int i => new JValue(i),
double d => new JValue(d),
bool b => new JValue(b),
string s => new JValue(s),
_ => new JValue(value.ToString())
};
}
}
}

View file

@ -1,61 +1,116 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Newtonsoft.Json.Linq;
namespace Core.Configurations
{
public class ConfigurationBuilder
public interface IConfigurationBuilder
{
private readonly List<IConfigurationProvider> _providers = new();
ConfigurationBuilder AddProvider(IConfigurationProvider provider);
IConfigurationRoot Build();
List<IConfigurationProvider> ConfigurationProviders { get; }
}
public class ConfigurationBuilder : IConfigurationBuilder
{
public List<IConfigurationProvider> ConfigurationProviders { get; private set; } = [];
public ConfigurationBuilder AddProvider(IConfigurationProvider provider)
{
_providers.Add(provider);
((IConfigurationBuilder)this).ConfigurationProviders.Add(provider);
return this;
}
public IConfigurationRoot Build()
{
// Her kan du implementere din egen sammenlægningslogik
return new ConfigurationRoot(_providers);
foreach (var provider in ConfigurationProviders)
{
provider.Build();
}
//TODO: we need to come up with merge strategy
return new ConfigurationRoot(ConfigurationProviders);
}
}
public class ConfigurationRoot : IConfigurationRoot
{
List<IConfigurationProvider> IConfigurationRoot.ConfigurationProviders { get; set; }
public ConfigurationRoot(List<IConfigurationProvider> configurationProviders)
{
}
public T GetSection<T>(string key)
{
throw new NotImplementedException();
}
}
public static class ConfigurationPredicateExtensions
{
public static IConfigurationSection GetSection(this IConfigurationRoot configurationSection, string key)
{
return null;
((IConfigurationRoot)this).ConfigurationProviders = configurationProviders;
}
public static T Get<T>(this IConfigurationSection configuration, string key)
{
return default(T);
}
public static class ConfigurationBinder
{
private static string NormalizePath(string path)
{
return path?.Replace(":", ".", StringComparison.Ordinal) ?? string.Empty;
}
public static string GetConnectionString(this IConfigurationRoot configuration, string name)
{
return configuration.GetSection("ConnectionStrings").Get<string>(name);
}
public static IConfigurationSection GetSection(this IConfigurationRoot configuration, string path)
{
JToken value = null;
foreach (var provider in configuration.ConfigurationProviders)
{
var test = provider.Configuration().SelectToken(NormalizePath(path));
if (test != null)
value = test;
}
return new ConfigurationSection { Path = path, Key = path.Split(':').Last(), Value = value };
}
public static T Get<T>(this IConfigurationRoot configuration, string path)
{
JToken value = null;
foreach (var provider in configuration.ConfigurationProviders)
{
var test = provider.Configuration().SelectToken(NormalizePath(path));
if (test != null)
value = test;
}
return value.ToObject<T>();
}
public static T Get<T>(this IConfigurationSection configuration, string path)
{
var value = configuration.Value.SelectToken(NormalizePath(path)).ToObject<T>();
return value;
}
public static T ToObject<T>(this IConfigurationSection configuration)
{
var value = configuration.Value.ToObject<T>();
return value;
}
[Obsolete("Use ToObject")]
public static T Get<T>(this IConfigurationSection configuration)
{
return configuration.Value.ToObject<T>();
}
}
public interface IConfigurationProvider
{
Dictionary<string, object> Configuration();
void Build();
Newtonsoft.Json.Linq.JObject Configuration();
}
public class ConfigurationSection : IConfigurationSection
{
public required string Path { get; set; }
public required string Key { get; set; }
public required JToken Value { get; set; }
}
public interface IConfigurationSection
{
string Key { get; }
string Path { get; }
string Value { get; set; }
string Key { get; }
Newtonsoft.Json.Linq.JToken Value { get; set; }
}
}

View file

@ -1,14 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core.Configurations
namespace Core.Configurations
{
public interface IConfigurationRoot
{
internal List<IConfigurationProvider> ConfigurationProviders { get; set; }
}
}

View file

@ -0,0 +1,54 @@
using Core.Exceptions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Core.Configurations.JsonConfigProvider
{
public static class JsonConfigExtension
{
public static IConfigurationBuilder AddJsonFile(this IConfigurationBuilder builder, string configurationFilePath = "appconfiguration.json", bool? optional = true, bool? reloadOnChange = false)
{
return builder.AddProvider(new JsonConfigProvider(builder, configurationFilePath, optional ?? true, reloadOnChange ?? false));
}
}
public interface IHasConfigurationFilePath
{
string ConfigurationFilePath { get; }
}
public class JsonConfigProvider : IConfigurationProvider, IHasConfigurationFilePath
{
private readonly IConfigurationBuilder _builder;
private readonly bool _reloadOnChange;
JObject _configuration;
public string ConfigurationFilePath { get; private set; }
public JsonConfigProvider() { }
public JsonConfigProvider(IConfigurationBuilder builder, string configurationFilePath, bool optional, bool reloadOnChange)
{
if (!optional && !File.Exists(configurationFilePath))
throw new ConfigurationException($"File not found, path: {configurationFilePath}");
if (optional && !File.Exists(configurationFilePath))
return;
ConfigurationFilePath = configurationFilePath;
_builder = builder;
_reloadOnChange = reloadOnChange;
}
public void Build()
{
using (StreamReader file = File.OpenText(ConfigurationFilePath))
using (JsonTextReader reader = new JsonTextReader(file))
{
_configuration = (JObject)JToken.ReadFrom(reader);
}
}
public JObject Configuration()
{
return _configuration;
}
}
}

View file

@ -1,115 +0,0 @@
using Microsoft.Extensions.Configuration;
using Npgsql;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Core.Configurations.PostgresqlConfigurationBuilder
{
public class PostgresConfigurationSource : Microsoft.Extensions.Configuration.IConfigurationSource
{
private readonly string _connectionString;
private readonly string _channel;
private readonly string _configurationQuery;
public PostgresConfigurationSource(string connectionString, string channel, string configurationQuery)
{
_connectionString = connectionString ?? throw new ArgumentNullException(nameof(connectionString));
_channel = channel ?? throw new ArgumentNullException(nameof(channel));
_configurationQuery = configurationQuery ?? throw new ArgumentNullException(nameof(configurationQuery));
}
public Microsoft.Extensions.Configuration.IConfigurationProvider Build(IConfigurationBuilder builder)
{
return new PostgresConfigurationProvider(_connectionString, _channel, _configurationQuery);
}
}
public class PostgresConfigurationProvider : ConfigurationProvider, IDisposable
{
private readonly string _connectionString;
private readonly string _channel;
private readonly string _configurationQuery;
private readonly NpgsqlConnection _listenerConnection;
private bool _disposedValue;
public PostgresConfigurationProvider(string connectionString, string channel, string configurationQuery)
{
_connectionString = connectionString;
_channel = channel;
_configurationQuery = configurationQuery;
_listenerConnection = new NpgsqlConnection(connectionString);
// Start listening for notifications
StartListening();
}
private async void StartListening()
{
try
{
await _listenerConnection.OpenAsync();
_listenerConnection.Notification += OnNotificationReceived;
using var cmd = new NpgsqlCommand($"LISTEN {_channel};", _listenerConnection);
await cmd.ExecuteNonQueryAsync();
}
catch (Exception ex)
{
// Log error and possibly retry
Console.WriteLine($"Error starting listener: {ex.Message}");
}
}
private void OnNotificationReceived(object sender, NpgsqlNotificationEventArgs e)
{
if (e.Channel == _channel)
{
// Reload configuration and notify
Load();
OnReload();
}
}
public override void Load()
{
var data = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
using (var connection = new NpgsqlConnection(_connectionString))
{
connection.Open();
using var cmd = new NpgsqlCommand(_configurationQuery, connection);
using var reader = cmd.ExecuteReader();
while (reader.Read())
{
string key = reader.GetString(0);
string value = reader.GetString(1);
data[key] = value;
}
}
Data = data;
}
protected virtual void Dispose(bool disposing)
{
if (!_disposedValue)
{
if (disposing)
{
_listenerConnection?.Dispose();
}
_disposedValue = true;
}
}
public void Dispose()
{
Dispose(disposing: true);
GC.SuppressFinalize(this);
}
}
}

View file

@ -1,31 +0,0 @@
using Microsoft.Extensions.Configuration;
namespace Core.Configurations.PostgresqlConfigurationBuilder
{
/// <summary>
/// LISTEN / NOTIFY in Postgresql
/// </summary>
public static class PostgresConfigurationExtensions
{
public static IConfigurationBuilder AddPostgresConfiguration(
this IConfigurationBuilder builder,
Action<PostgresConfigurationOptions> setupAction)
{
var options = new PostgresConfigurationOptions();
setupAction(options);
builder.Add(new PostgresConfigurationSource(
options.ConnectionString,
options.Channel,
options.ConfigurationQuery));
return builder;
}
}
public class PostgresConfigurationOptions
{
public string ConnectionString { get; set; }
public string Channel { get; set; }
public string ConfigurationQuery { get; set; }
}
}

View file

@ -0,0 +1,14 @@
namespace Core.Configurations.SmartConfigProvider;
public class AppConfiguration
{
public long Id { get; set; }
public string Key { get; set; }
public object 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; }
}

View file

@ -0,0 +1,31 @@
using System.Data;
using Core.Configurations.SmartConfiguration;
using Insight.Database;
namespace Core.Configurations.SmartConfigProvider;
public class ConfigurationRepository : IConfigurationRepository
{
private readonly IDbConnection _connection;
public ConfigurationRepository(IDbConnection connection)
{
_connection = connection;
}
public ConfigurationRepository(string connectionString)
{
_connection = new Npgsql.NpgsqlConnection(connectionString);
}
public IEnumerable<AppConfiguration> GetActiveConfigurations()
{
const string sql = @"
SELECT id, ""key"", value, label, content_type,
valid_from, expires_at, created_at, modified_at, etag
FROM app_configuration
WHERE CURRENT_TIMESTAMP BETWEEN valid_from AND expires_at
OR (valid_from IS NULL AND expires_at IS NULL)";
return _connection.QuerySql<AppConfiguration>(sql);
}
}

View file

@ -0,0 +1,7 @@
using Core.Configurations.SmartConfigProvider;
namespace Core.Configurations.SmartConfiguration;
public interface IConfigurationRepository
{
IEnumerable<AppConfiguration> GetActiveConfigurations();
}

View file

@ -0,0 +1,74 @@
using Core.Configurations.JsonConfigProvider;
using Core.Configurations.SmartConfigProvider;
using Core.Exceptions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
namespace Core.Configurations.SmartConfig
{
public static class SmartConfigExtension
{
public static IConfigurationBuilder AddSmartConfig(this IConfigurationBuilder builder, string configKey = "DefaultConnection", string path = null)
{
return builder.AddProvider(new SmartConfigProvider(builder, configKey, path));
}
}
public class SmartConfigProvider : IConfigurationProvider
{
string _configKey;
string _connectionString;
string _path;
IConfigurationBuilder _builder;
Newtonsoft.Json.Linq.JObject _configuration;
public SmartConfigProvider() { }
public SmartConfigProvider(IConfigurationBuilder builder, string configKey, string configurationFilePath)
{
_builder = builder;
_configKey = configKey;
_path = configurationFilePath;
SetConnectionString();
}
void SetConnectionString()
{
var carrier = _builder.ConfigurationProviders.OfType<IHasConfigurationFilePath>().SingleOrDefault();
if (carrier?.ConfigurationFilePath is null && _path is null)
throw new ConfigurationException($"Expected a previous added ConfigurationProvider with IHasConfigurationFilePath or a configurationFilePath where to find the appsettingsfile");
_path ??= carrier.ConfigurationFilePath;
if (!File.Exists(_path))
throw new ConfigurationException($"File not found, configurationFilePath: {_path}");
using (StreamReader file = File.OpenText(_path))
using (JsonTextReader reader = new JsonTextReader(file))
{
var jsonConfiguration = (Newtonsoft.Json.Linq.JObject)Newtonsoft.Json.Linq.JToken.ReadFrom(reader);
_connectionString = jsonConfiguration.SelectToken($"ConnectionStrings.{_configKey}")?.ToString();
}
}
public void Build()
{
var repository = new ConfigurationRepository(_connectionString);
var configs = repository.GetActiveConfigurations();
var pairs = configs.Select(x => new KeyValuePair<string, JToken>(x.Key, JToken.Parse(x.Value.ToString())));
_configuration = Common.KeyValueToJson.Convert(pairs);
}
public Newtonsoft.Json.Linq.JObject Configuration()
{
return _configuration;
}
}
}

View file

@ -1,14 +0,0 @@
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; }
}

View file

@ -1,27 +0,0 @@
using Microsoft.Extensions.Configuration;
namespace Core.Configurations.SmartConfiguration
{
public static class ConfigurationExtensions
{
public static T Get<T>(this IConfigurationSection section) where T : class
{
if (section is JsonConfigurationSection jsonSection)
{
var token = jsonSection.GetToken();
return token?.ToObject<T>();
}
throw new InvalidOperationException("Section is not a JsonConfigurationSection");
}
public static T GetValue<T>(this IConfigurationSection section, string key)
{
if (section is JsonConfigurationSection jsonSection)
{
var token = jsonSection.GetToken().SelectToken(key.Replace(":", "."));
return token.ToObject<T>();
}
throw new InvalidOperationException("Section is not a JsonConfigurationSection");
}
}
}

View file

@ -1,24 +0,0 @@
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<IEnumerable<AppConfiguration>> 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<AppConfiguration>(sql);
}
}

View file

@ -1,5 +0,0 @@
namespace Core.Configurations.SmartConfiguration;
public interface IConfigurationRepository
{
Task<IEnumerable<AppConfiguration>> GetActiveConfigurations();
}

View file

@ -1,34 +0,0 @@
using Newtonsoft.Json.Linq;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Primitives;
using System.Data;
namespace Core.Configurations.SmartConfiguration;
public class JsonConfiguration : Microsoft.Extensions.Configuration.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 Microsoft.Extensions.Configuration.IConfigurationSection GetSection(string key) => null;
//new JsonConfigurationSection(_data, key);
public IEnumerable<IConfigurationSection> GetChildren() =>
_data.Properties().Select(p => new JsonConfigurationSection(_data, p.Name));
public IChangeToken GetReloadToken() => throw new NotImplementedException();
IEnumerable<Microsoft.Extensions.Configuration.IConfigurationSection> IConfiguration.GetChildren()
{
throw new NotImplementedException();
}
}

View file

@ -1,80 +0,0 @@
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 Microsoft.Extensions.Configuration.IConfigurationSection GetSection(string key)
{
if (string.IsNullOrEmpty(key))
throw new ArgumentNullException(nameof(key));
return null;// new JsonConfigurationSection(_data, string.IsNullOrEmpty(_path) ? key : $"{_path}:{key}");
}
public JToken GetToken() => _data.SelectToken(_normalizedPath);
public IEnumerable<IConfigurationSection> 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<IConfigurationSection>();
}
public T Get<T>() where T : class
{
var token = _data.SelectToken(_normalizedPath);
return token?.ToObject<T>();
}
public IChangeToken GetReloadToken() => new ConfigurationReloadToken();
private static string NormalizePath(string path)
{
return path?.Replace(":", ".", StringComparison.Ordinal) ?? string.Empty;
}
}
}

View file

@ -1,83 +0,0 @@
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));
}
/// <summary>
/// Loads configurations from the repository and builds the configuration tree.
/// </summary>
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);
}
}
/// <summary>
/// Adds a key-value pair to the configuration tree.
/// </summary>
/// <param name="key">The key to add.</param>
/// <param name="jsonValue">The JSON value to add.</param>
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<JObject>(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);
}
}
/// <summary>
/// Builds the configuration instance.
/// </summary>
/// <returns>The built <see cref="IConfiguration"/> instance.</returns>
public IConfiguration Build()
{
_configuration = new JsonConfiguration(_rootObject);
return _configuration;
}
}
}

View file

@ -1,48 +0,0 @@

using Microsoft.Extensions.Configuration;
namespace Core.Configurations
{
public class SmartConfigManager
{
private static IConfigurationBuilder _configurationBuilder;
/// <summary>
/// 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
/// </summary>
/// <param name="localSettingsFile">
/// Path relative to the base path stored in Microsoft.Extensions.Configuration.IConfigurationBuilder.Properties of builder.
/// </param>
public static IConfigurationBuilder AddSmartConfigBuilder(string localSettingsFile)
{
if (_configurationBuilder == null)
{
var envConfiguration = new Microsoft.Extensions.Configuration.ConfigurationBuilder()
.AddEnvironmentVariables()
.Build();
var appConfigEndpoint = envConfiguration["AppConfigEndpoint"];
var appConfigLabel = envConfiguration["AppConfigLabelFilter"];
_configurationBuilder = new Microsoft.Extensions.Configuration.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;
}
}
}

View file

@ -12,11 +12,12 @@
<PackageReference Include="Insight.Database" Version="8.0.1" />
<PackageReference Include="Insight.Database.Providers.PostgreSQL" Version="8.0.1" />
<PackageReference Include="Microsoft.ApplicationInsights" Version="2.22.0" />
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.AzureAppConfiguration" Version="8.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
<PackageReference Include="npgsql" Version="9.0.2" />
</ItemGroup>
<ItemGroup>
<Folder Include="Configurations\AzureAppConfigurationProvider\" />
<Folder Include="Configurations\PostgresqlConfigurationBuilder\" />
</ItemGroup>
</Project>

View file

@ -0,0 +1,13 @@
using System.Text.RegularExpressions;
namespace Database.Common
{
internal class Validations
{
public static bool IsValidSchemaName(string schema)
{
return !string.IsNullOrEmpty(schema) && Regex.IsMatch(schema, "^[a-zA-Z0-9_]+$");
}
}
}

View file

@ -1,7 +1,7 @@
using Insight.Database;
using System.Data;
namespace Database.AppConfigurationSystem;
namespace Database.ConfigurationManagementSystem;
public class ConfigurationDatabaseSetup
{

View file

@ -1,25 +1,66 @@
using Insight.Database;
using Database.Common;
using Insight.Database;
using System;
using System.Data;
using System.Threading.Tasks;
namespace Database.Tenants
namespace Database.IdentitySystem
{
public class DbSetup
{
private readonly IDbConnection _db;
readonly IDbConnection _db;
IDbTransaction _transaction = null;
string _schema;
public DbSetup(IDbConnection db)
{
_db = db ?? throw new ArgumentNullException(nameof(db));
}
/// <summary>
/// Creates the system tables in the specified schema within a transaction.
/// </summary>
/// <param name="schema">The schema name where the tables will be created.</param>
public async Task CreateSystem(string schema)
{
_schema = schema;
if (!Validations.IsValidSchemaName(schema))
throw new ArgumentException("Invalid schema name", nameof(schema));
using (_transaction = _db.BeginTransaction())
{
try
{
await CreateUsersTable().ConfigureAwait(false);
await CreateTenantsTable().ConfigureAwait(false);
await CreateUserTenantsTable().ConfigureAwait(false);
await SetupRLS().ConfigureAwait(false);
_transaction.Commit();
}
catch (Exception ex)
{
_transaction.Rollback();
throw new InvalidOperationException("Failed to create system tables.", ex);
}
}
}
private async Task ExecuteSqlAsync(string sql)
{
if (string.IsNullOrEmpty(sql))
throw new ArgumentNullException(nameof(sql));
await _db.ExecuteAsync(sql).ConfigureAwait(false);
}
/// <summary>
/// Creates the users table in the ptmain schema.
/// </summary>
public void CreateUsersTable()
public async Task CreateUsersTable()
{
ExecuteInTransaction(@"
var sql = @"
CREATE TABLE IF NOT EXISTS ptmain.users (
id SERIAL PRIMARY KEY,
email VARCHAR(256) NOT NULL UNIQUE,
@ -32,85 +73,75 @@ namespace Database.Tenants
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
last_login_at TIMESTAMPTZ NULL
);");
);";
await ExecuteSqlAsync(sql).ConfigureAwait(false);
}
/// <summary>
/// Creates the tenants table in the ptmain schema.
/// </summary>
public void CreateTenantsTable()
public async Task CreateTenantsTable()
{
ExecuteInTransaction(@"
var sql = @"
CREATE TABLE IF NOT EXISTS ptmain.tenants (
id SERIAL PRIMARY KEY,
connection_string VARCHAR(500) NOT NULL,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
created_by INTEGER NOT NULL REFERENCES ptmain.users(id),
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
);");
);";
await ExecuteSqlAsync(sql).ConfigureAwait(false);
}
/// <summary>
/// Creates the user_tenants table in the ptmain schema.
/// </summary>
public void CreateUserTenantsTable()
public async Task CreateUserTenantsTable()
{
ExecuteInTransaction(@"
var sql = @"
CREATE TABLE IF NOT EXISTS ptmain.user_tenants (
user_id INTEGER NOT NULL REFERENCES ptmain.users(id),
tenant_id INTEGER NOT NULL REFERENCES ptmain.tenants(id),
pin_code VARCHAR(10) NULL,
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (user_id, tenant_id)
);");
);";
await ExecuteSqlAsync(sql).ConfigureAwait(false);
}
/// <summary>
/// Sets up Row Level Security (RLS) for the tenants and user_tenants tables.
/// </summary>
public void SetupRLS()
public async Task SetupRLS()
{
var sql = new[]
{
ExecuteInTransaction(
"ALTER TABLE ptmain.tenants ENABLE ROW LEVEL SECURITY;",
"ALTER TABLE ptmain.user_tenants ENABLE ROW LEVEL SECURITY;",
"DROP POLICY IF EXISTS tenant_access ON ptmain.tenants;",
@"
CREATE POLICY tenant_access ON ptmain.tenants
@"CREATE POLICY tenant_access ON ptmain.tenants
USING (id IN (
SELECT tenant_id
FROM ptmain.user_tenants
WHERE user_id = current_setting('app.user_id', TRUE)::INTEGER
));",
"DROP POLICY IF EXISTS user_tenant_access ON ptmain.user_tenants;",
@"
CREATE POLICY user_tenant_access ON ptmain.user_tenants
@"CREATE POLICY user_tenant_access ON ptmain.user_tenants
USING (user_id = current_setting('app.user_id', TRUE)::INTEGER);"
);
};
foreach (var statement in sql)
{
await ExecuteSqlAsync(statement).ConfigureAwait(false);
}
}
/// <summary>
/// Executes one or more SQL commands within a transaction.
/// </summary>
/// <param name="sqlCommands">The SQL commands to execute.</param>
private void ExecuteInTransaction(params string[] sqlCommands)
{
if (_db.State != ConnectionState.Open)
_db.Open();
using var transaction = _db.BeginTransaction();
try
{
foreach (var sql in sqlCommands)
{
_db.Execute(sql, transaction: transaction);
}
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
throw new InvalidOperationException("Failed to execute SQL commands in transaction.", ex);
}
}
}
}

View file

@ -2,7 +2,7 @@
using Insight.Database;
using System.Data;
namespace Database.Identity
namespace Database.IdentitySystem
{
public class UserService
{

View file

@ -1,8 +1,9 @@
using System.Data;
using System.Text.RegularExpressions;
using Database.Common;
using Insight.Database;
namespace Database.Tenants
namespace Database.RolesPermissionSystem
{
public class Setup
{
@ -19,7 +20,7 @@ namespace Database.Tenants
/// <param name="schema">The schema name where the tables will be created.</param>
public async Task CreateSystem(string schema)
{
if (!IsValidSchemaName(schema))
if (!Validations.IsValidSchemaName(schema))
throw new ArgumentException("Invalid schema name", nameof(schema));
using (var transaction = _db.BeginTransaction())
@ -41,10 +42,6 @@ namespace Database.Tenants
}
}
private bool IsValidSchemaName(string schema)
{
return !string.IsNullOrEmpty(schema) && Regex.IsMatch(schema, "^[a-zA-Z0-9_]+$");
}
private async Task ExecuteSqlAsync(string sql, IDbTransaction transaction)
{

View file

@ -12,7 +12,7 @@
</ItemGroup>
<ItemGroup>
<None Update="appsettings.json">
<None Update="appconfiguration.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

View file

@ -1,7 +1,7 @@
{
"AllowedHosts": "*",
"ConnectionStrings": {
"ptdb": "Host=192.168.1.57;Port=5432;Database=ptdb01;User Id=postgres;Password=3911;"
"ptdb": "Host=192.168.1.57;Port=5432;Database=ptdb01;User Id=sathumper;Password=3911;"
},
"ApplicationInsights": {
"ConnectionString": "InstrumentationKey=6d2e76ee-5343-4691-a5e3-81add43cb584;IngestionEndpoint=https://northeurope-0.in.applicationinsights.azure.com/"

View file

@ -1 +1 @@
{"resources":{"Scripts/Script-2.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptdb01"},"Scripts/grant-privileges.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptdb01"}}}
{"resources":{"Scripts/Script-1.sql":{"default-datasource":"postgres-jdbc-19484872d85-cd2a4a40116e706","default-catalog":"ptdb01","default-schema":"ptmain"},"Scripts/Script-2.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptdb01"},"Scripts/grant-privileges.sql":{"default-datasource":"postgres-jdbc-1948450a8b4-5fc9eec404e65c44","default-catalog":"ptdb01"}}}

View file

@ -0,0 +1,119 @@
using Core.Configurations.SmartConfig;
using Core.Configurations;
using FluentAssertions;
using Newtonsoft.Json.Linq;
using Core.Configurations.JsonConfigProvider;
using Autofac;
using System.Data;
using Insight.Database;
using Npgsql;
namespace Tests.ConfigurationTests
{
[TestClass]
public class JsonConfigurationProviderTests : TestFixture
{
[TestMethod]
public void GetSection_ShouldReturnCorrectFeatureSection()
{
// Arrange
var expectedJObject = JObject.Parse(@"{
'Enabled': true,
'RolloutPercentage': 25,
'AllowedUserGroups': ['beta']
}") as JToken;
var builder = new ConfigurationBuilder()
.AddJsonFile("appconfiguration.dev.json")
.Build();
// Act
var section = builder.GetSection("Feature");
// Assert
section.Should().NotBeNull();
section.Value.Should().BeEquivalentTo(expectedJObject);
}
[TestMethod]
public void Get_ShouldReturnCorrectFeatureObject()
{
// Arrange
var expectedFeature = new Feature
{
Enabled = true,
RolloutPercentage = 25,
AllowedUserGroups = new() { "beta" }
};
var builder = new ConfigurationBuilder()
.AddJsonFile("appconfiguration.dev.json")
.Build();
// Act
var actualFeature = builder.GetSection("Feature").ToObject<Feature>();
var actualFeatureObsoleted = builder.GetSection("Feature").Get<Feature>();
// Assert
actualFeature.Should().BeEquivalentTo(expectedFeature);
actualFeatureObsoleted.Should().BeEquivalentTo(expectedFeature);
}
[TestMethod]
public void Get_ShouldReturnCorrectValueAsString()
{
// Arrange
var expectedFeature = "123";
var builder = new ConfigurationBuilder()
.AddJsonFile("appconfiguration.dev.json")
.Build();
// Act
var actualFeature = builder.GetSection("AnotherSetting").Get<string>("Thresholds:High");
// Assert
actualFeature.Should().BeEquivalentTo(expectedFeature);
}
[TestMethod]
public void Get_ShouldReturnCorrectValueAsInt()
{
// Arrange
var expectedFeature = 22;
var builder = new ConfigurationBuilder()
.AddJsonFile("appconfiguration.dev.json")
.Build();
// Act
var actualFeature = builder.GetSection("AnotherSetting:Temperature").Get<int>("Indoor:Max:Limit");
// Assert
actualFeature.Should().Be(expectedFeature);
}
[TestMethod]
public void Get_ShouldReturnCorrectValueAsBool()
{
// Arrange
var expectedFeature = true;
var configRoot = new ConfigurationBuilder()
.AddJsonFile("appconfiguration.dev.json")
.AddSmartConfig()
.Build();
// Act
var actualFeature = configRoot.Get<bool>("Database:UseSSL");
// Assert
actualFeature.Should().Be(expectedFeature);
}
}
public class Feature
{
public bool Enabled { get; set; }
public int RolloutPercentage { get; set; }
public List<string> AllowedUserGroups { get; set; }
}
}

View file

@ -1,139 +1,78 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Moq;
using Newtonsoft.Json.Linq;
using System.Collections.Generic;
using System.Linq;
namespace Tests.ConfigurationTests
using Tests;
using Core.Configurations.SmartConfig;
using Core.Configurations.Common;
namespace Tests.ConfigurationTests;
[TestClass]
public class ConfigurationTests : TestFixture
{
[TestClass]
public class KeyValueJsonHandlingTests : TestFixture
[TestInitialize]
public void Init()
{
}
[TestMethod]
public void FunMixedTypesTest()
public void ConfigurationSettingsTest()
{
var pairs = new List<KeyValuePair<string, object>>
var pairs = new List<KeyValuePair<string, JToken>>
{
new("developer:coffeeLevel", 9001.5),
new("developer:bugsFixed", 42),
new("developer:awake", false),
new("compiler:errors:semicolon:0", "Missing ;"),
new("compiler:errors:semicolon:1", "Found ; but why?"),
new("computer:ram:status", "Out of memory"),
new("computer:cpu:temperature", 99.9),
new("friday:beer:count", 6),
new("friday:code:working", true),
new("friday:bugs:count", int.MaxValue),
new("real:object", JObject.Parse(@"{
""beer"": {
""count"": 6
},
""code"": {
""working"": true
},
""bugs"": {
""count"": 2147483647
}
}"))
new("Debug", true),
// Database konfiguration
new("Database:ConnectionString", "Server=db.example.com;Port=5432"),
new("Database:Timeout", 30),
new("Database:UseSSL", true),
// Logging konfiguration med JObject
new("Logging:FileOptions", JObject.Parse(@"{
'Path': '/var/logs/app.log',
'MaxSizeMB': 100,
'RetentionDays': 7
}")),
// Feature flags med kompleks konfiguration
new("Features:Experimental", JObject.Parse(@"{
'Enabled': true,
'RolloutPercentage': 25,
'AllowedUserGroups': ['beta']
}")),
// API endpoints med array
new("API:Endpoints", "/api/users"),
new("API:Endpoints", "/api/products")
};
var result = KeyValueToJson.Convert(pairs);
var expected = JObject.Parse(@"{
'developer': {
'coffeeLevel': 9001.5,
'bugsFixed': 42,
'awake': false
'Debug' : true,
'Database': {
'ConnectionString': 'Server=db.example.com;Port=5432',
'Timeout': 30,
'UseSSL': true
},
'compiler': {
'errors': { 'semicolon': ['Missing ;', 'Found ; but why?'] }
'Logging': {
'FileOptions': {
'Path': '/var/logs/app.log',
'MaxSizeMB': 100,
'RetentionDays': 7
}
},
'computer': {
'ram': { 'status': 'Out of memory' },
'cpu': { 'temperature': 99.9 }
'Features': {
'Experimental': {
'Enabled': true,
'RolloutPercentage': 25,
'AllowedUserGroups': ['beta']
}
},
'friday': {
'beer': { 'count': 6 },
'code': { 'working': true },
'bugs': { 'count': 2147483647 }
'API': {
'Endpoints': ['/api/users', '/api/products']
}
}");
Assert.IsTrue(JToken.DeepEquals(expected, result));
}
}
public static class KeyValueToJson
{
public static JObject Convert(List<KeyValuePair<string, object>> pairs)
{
var root = new JObject();
foreach (var pair in pairs)
{
var keys = pair.Key.Split(':');
var current = root;
// Gennemgå hierarkiet og opret underobjekter, hvis de ikke eksisterer
for (int i = 0; i < keys.Length - 1; i++)
{
var key = keys[i];
if (current[key] == null)
{
current[key] = new JObject();
}
current = (JObject)current[key];
}
// Håndter den sidste nøgle og tilføj værdien
var lastKey = keys[keys.Length - 1];
var value = ConvertValue(pair.Value);
// Hvis den sidste nøgle allerede eksisterer, tilføj til en liste
if (current[lastKey] != null)
{
// Hvis den allerede er en liste, tilføj til listen
if (current[lastKey].Type == JTokenType.Array)
{
((JArray)current[lastKey]).Add(value);
}
// Hvis den ikke er en liste, konverter til en liste
else
{
var existingValue = current[lastKey];
current[lastKey] = new JArray { existingValue, value };
}
}
// Ellers tilføj som en enkelt værdi
else
{
current[lastKey] = value;
}
}
return root;
}
private static JToken ConvertValue(object value)
{
// Konverter værdien til det korrekte JToken-format
return value switch
{
int i => new JValue(i),
double d => new JValue(d),
bool b => new JValue(b),
string s => new JValue(s),
_ => new JValue(value.ToString()) // Fallback for andre typer
};
}
}
}

View file

@ -0,0 +1,102 @@
using Core.Configurations.SmartConfig;
using Core.Configurations;
using FluentAssertions;
using Newtonsoft.Json.Linq;
using Core.Configurations.JsonConfigProvider;
using Autofac;
using System.Data;
using Insight.Database;
using Npgsql;
namespace Tests.ConfigurationTests
{
[TestClass]
public class SmartConfigProviderTests : TestFixture
{
[TestMethod]
public void Get_ShouldReturnCorrectValueAsBool()
{
// Arrange
var expectedFeature = true;
var config = new ConfigurationBuilder()
.AddJsonFile("appconfiguration.dev.json")
.AddSmartConfig()
.Build();
// Act
var actualFeature = config.Get<bool>("Database:UseSSL");
// Assert
actualFeature.Should().Be(expectedFeature);
}
[TestMethod]
public void Get_ShouldReturnCorrectValueWhenSelectingIntoValueRowInConfigTable()
{
// Arrange
var expectedFeature = 100;
var builder = new ConfigurationBuilder()
.AddJsonFile("appconfiguration.dev.json")
.AddSmartConfig()
.Build();
// Act
var actualFeature = builder.GetSection("Logging:FileOptions").Get<int>("MaxSizeMB");
var withoutSectionThisAlsoWorks = builder.Get<int>("Logging:FileOptions:MaxSizeMB");
// Assert
actualFeature.Should().Be(expectedFeature);
actualFeature.Should().Be(withoutSectionThisAlsoWorks);
}
[TestMethod]
public void GetPostgresSearchPath()
{
//ALTER USER sathumper SET search_path TO ptmain, public;
var conn = Container.Resolve<IDbConnection>();
var result = conn.QuerySql("SHOW search_path;");
using (var connw = new NpgsqlConnection(conn.ConnectionString + ";Password=3911"))
{
connw.Open();
using (var cmd = new NpgsqlCommand("SHOW search_path; SELECT current_user;", connw))
{
using (var reader = cmd.ExecuteReader())
{
reader.Read();
var r1 = $"Search path: {reader.GetString(0)}";
reader.NextResult();
reader.Read();
var r2 = $"Current schema: {reader.GetString(0)}";
}
}
}
}
[TestMethod]
public void TryGetActiveConfigurations()
{
const string sql = @"
SELECT id, ""key"", value, label, content_type,
valid_from, expires_at, created_at, modified_at, etag
FROM app_configuration
WHERE CURRENT_TIMESTAMP BETWEEN valid_from AND expires_at
OR (valid_from IS NULL AND expires_at IS NULL)";
var conn = Container.Resolve<IDbConnection>();
var result = conn.QuerySql(sql);
}
}
}

50
Tests/PostgresTests.cs Normal file
View file

@ -0,0 +1,50 @@
using Autofac;
using System.Data;
using Insight.Database;
namespace Tests
{
[TestClass]
public class PostgresTests : TestFixture
{
[TestMethod]
public void TestDefaultConnection()
{
var conn = Container.Resolve<IDbConnection>();
//https://www.reddit.com/r/dotnet/comments/6wdoyn/how_to_properly_register_dapper_on_net_core_2_di/
//https://www.code4it.dev/blog/postgres-crud-dapper/
//https://stackoverflow.com/questions/69169247/how-to-create-idbconnection-factory-using-autofac-for-dapper
conn.ExecuteSql("SELECT 1 as p");
}
[TestMethod]
public void TryTenantSetupService()
{
var conn = Container.Resolve<IDbConnection>();
}
[TestMethod]
public async Task TryDbSetup()
{
var conn = Container.Resolve<IDbConnection>();
var dbSetup = new Database.IdentitySystem.DbSetup(conn);
await dbSetup.CreateSystem("swp");
}
[TestMethod]
public void SetupPostgresql_LISTEN()
{
//var builder = new ConfigurationBuilder()
// .AddPostgresConfiguration(options =>
// {
// options.ConnectionString = "Host=192.168.1.57;Database=ptdb01;Username=postgres;Password=3911";
// options.Channel = "config_changes";
// options.ConfigurationQuery = @"select * from dev.app_configuration";
// });
}
}
}

View file

@ -1,173 +0,0 @@
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Moq;
using Newtonsoft.Json.Linq;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Tests;
using Microsoft.Extensions.Configuration;
using Core.Configurations.SmartConfiguration;
namespace Configuration.Core.Tests;
[TestClass]
public class ConfigurationTests : TestFixture
{
private Mock<IConfigurationRepository> _mockRepo;
private KeyValueConfigurationBuilder _builder;
[TestInitialize]
public void Init()
{
_mockRepo = new Mock<IConfigurationRepository>();
_builder = new KeyValueConfigurationBuilder(_mockRepo.Object);
}
[TestMethod]
public async Task LoadConfiguration_WithValidData_BuildsCorrectly()
{
// Arrange
var configurations = new List<AppConfiguration>
{
new AppConfiguration
{
Key = "Email:Templates:Welcome",
Value = @"{""subject"":""Welcome"",""sender"":""test@company.com""}",
ValidFrom = DateTime.UtcNow.AddDays(-1),
ExpiresAt = DateTime.UtcNow.AddDays(1)
}
};
_mockRepo.Setup(r => r.GetActiveConfigurations())
.ReturnsAsync(configurations);
// Act
await _builder.LoadConfiguration();
var config = _builder.Build();
// Assert
Assert.IsNotNull(config);
Assert.AreEqual("Welcome", config["Email:Templates:Welcome:subject"]);
}
[TestMethod]
public async Task LoadConfiguration_WithExpiredData_NotIncluded()
{
// Arrange
var configurations = new List<AppConfiguration>
{
new AppConfiguration
{
Key = "Test:Key",
Value = @"{""value"":""test""}",
ValidFrom = DateTime.UtcNow.AddDays(1),
ExpiresAt = DateTime.UtcNow.AddDays(2)
}
};
_mockRepo.Setup(r => r.GetActiveConfigurations())
.ReturnsAsync(new List<AppConfiguration>());
// Act
await _builder.LoadConfiguration();
var config = _builder.Build();
// Assert
Assert.IsNull(config["Test:Key:value"]);
}
[TestMethod]
public void AddKeyValue_WithNestedKeys_BuildsCorrectHierarchy()
{
// Arrange
var key = "Level1:Level2:Level3";
var value = @"{""setting"":""value""}";
// Act
_builder.AddKeyValue(key, value);
var config = _builder.Build();
// Assert
Assert.AreEqual("value", config["Level1:Level2:Level3:setting"]);
}
[TestMethod]
public async Task GetSection_ReturnsCorrectSubSection()
{
// Arrange
var configurations = new List<AppConfiguration>
{
new AppConfiguration
{
Key = "Parent:Child",
Value = @"{""setting"":""value""}",
ValidFrom = DateTime.UtcNow.AddDays(-1)
}
};
_mockRepo.Setup(r => r.GetActiveConfigurations())
.ReturnsAsync(configurations);
// Act
await _builder.LoadConfiguration();
var config = _builder.Build();
var section = config.GetSection("Parent");
// Assert
Assert.AreEqual("value", section["Child:setting"]);
}
[TestMethod]
public void Configuration_ShouldMapToTypedClass()
{
// Arrange
var configData = new JObject
{
["Email"] = new JObject
{
["Templates"] = new JObject
{
["Welcome"] = new JObject
{
["subject"] = "Welcome to our service",
["template"] = "welcome-template.html",
["sender"] = "noreply@test.com",
["settings"] = new JObject
{
["isEnabled"] = true,
["retryCount"] = 3
}
}
}
}
};
Microsoft.Extensions.Configuration.IConfiguration configuration = null;// new JsonConfiguration(configData, new Microsoft.Extensions.Configuration.ConfigurationReloadToken());
// Act
var welcomeConfig = configuration.GetSection("Email:Templates:Welcome").Get<WelcomeEmailConfig>();
// Assert
Assert.IsNotNull(welcomeConfig);
Assert.AreEqual("Welcome to our service", welcomeConfig.Subject);
Assert.AreEqual("welcome-template.html", welcomeConfig.Template);
Assert.AreEqual("noreply@test.com", welcomeConfig.Sender);
Assert.IsTrue(welcomeConfig.Settings.IsEnabled);
Assert.AreEqual(3, welcomeConfig.Settings.RetryCount);
}
public class WelcomeEmailConfig
{
public string Subject { get; set; }
public string Template { get; set; }
public string Sender { get; set; }
public EmailSettings Settings { get; set; }
}
public class EmailSettings
{
public bool IsEnabled { get; set; }
public int RetryCount { get; set; }
}
}

View file

@ -1,12 +1,11 @@
using System;
using System.Diagnostics;
using Autofac;
using Core.Configurations;
using Core.ModuleRegistry;
using Microsoft.ApplicationInsights;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Core.Configurations.JsonConfigProvider;
namespace Tests
{
/// <summary>
@ -18,23 +17,11 @@ namespace Tests
protected IContainer Container { get; private set; }
protected ContainerBuilder ContainerBuilder { get; private set; }
[AssemblyInitialize]
public static void AssemblySetup(TestContext tc)
{
Trace.Listeners.Add(new TextWriterTraceListener(Console.Out));
var envConfiguration = new ConfigurationBuilder()
.AddEnvironmentVariables()
.Build();
}
public virtual IConfigurationRoot Configuration()
{
IConfigurationBuilder configBuilder = Core.Configurations.AzureConfigurationManager.AppConfigBuilder("appsettings.dev.json");
IConfigurationRoot configuration = configBuilder.Build();
var configuration = new ConfigurationBuilder()
.AddJsonFile("appconfiguration.dev.json")
.Build();
return configuration;
}
@ -66,12 +53,12 @@ namespace Tests
builder.RegisterModule(new Core.ModuleRegistry.DbPostgreSqlModule
{
ConnectionString = configuration.GetConnectionString("ptdb")
ConnectionString = configuration.GetConnectionString("DefaultConnection")
});
builder.RegisterModule(new Core.ModuleRegistry.TelemetryModule
{
TelemetryConfig = configuration.GetSection("ApplicationInsights").Get<Core.ModuleRegistry.TelemetryConfig>()
TelemetryConfig = configuration.GetSection("ApplicationInsights").ToObject<Core.ModuleRegistry.TelemetryConfig>()
});

View file

@ -10,6 +10,7 @@
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="6.0.0" />
<PackageReference Include="FluentAssertions" Version="8.0.1" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
<PackageReference Include="moq" Version="4.20.72" />
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
@ -26,7 +27,7 @@
</ItemGroup>
<ItemGroup>
<None Update="appsettings.dev.json">
<None Update="appconfiguration.dev.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

View file

@ -1,56 +0,0 @@
using Autofac;
using System.Data;
using Insight.Database;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Microsoft.Extensions.Configuration;
using Core.Configurations.PostgresqlConfigurationBuilder;
namespace Tests
{
[TestClass]
public class UnitTest1 : TestFixture
{
[TestMethod]
public void TestDefaultConnection()
{
var conn = Container.Resolve<IDbConnection>();
//https://www.reddit.com/r/dotnet/comments/6wdoyn/how_to_properly_register_dapper_on_net_core_2_di/
//https://www.code4it.dev/blog/postgres-crud-dapper/
//https://stackoverflow.com/questions/69169247/how-to-create-idbconnection-factory-using-autofac-for-dapper
conn.ExecuteSql("SELECT 1 as p");
//var sql = "SELECT * FROM swp.foo";
//var customers = conn.Query(sql, commandType:CommandType.Text);
}
[TestMethod]
public void TryTenantSetupService()
{
var conn = Container.Resolve<IDbConnection>();
}
[TestMethod]
public async Task TryDbSetup()
{
var conn = Container.Resolve<IDbConnection>();
var dbSetup = new Database.AppConfigurationSystem.ConfigurationDatabaseSetup(conn);
//await dbSetup..CreateDatabaseWithSchema("swp");
}
[TestMethod]
public void SetupPostgresql_LISTEN()
{
var builder = new ConfigurationBuilder()
.AddPostgresConfiguration(options =>
{
options.ConnectionString = "Host=192.168.1.57;Database=ptdb01;Username=postgres;Password=3911";
options.Channel = "config_changes";
options.ConfigurationQuery = @"select * from dev.app_configuration";
});
}
}
}

View file

@ -0,0 +1,32 @@
{
"AllowedHosts": "*",
"ConnectionStrings": {
"DefaultConnection": "Host=192.168.1.57;Port=5432;Database=ptdb01;User Id=sathumper;Password=3911;"
},
"ApplicationInsights": {
"ConnectionString": "InstrumentationKey=6d2e76ee-5343-4691-a5e3-81add43cb584;IngestionEndpoint=https://northeurope-0.in.applicationinsights.azure.com/"
},
"Feature": {
"Enabled": true,
"RolloutPercentage": 25,
"AllowedUserGroups": [ "beta" ]
},
"AnotherSetting": {
"Thresholds": {
"High": "123",
"Low": "-1"
},
"Temperature": {
"Indoor": {
"Max": { "Limit": 22 },
"Min": { "Limit": 18 }
},
"Outdoor": {
"Max": { "Limit": 12 },
"Min": { "Limit": 9 }
}
}
}
}

View file

@ -1,9 +0,0 @@
{
"AllowedHosts": "*",
"ConnectionStrings": {
"ptdb": "Host=192.168.1.57;Port=5432;Database=ptdb01;User Id=postgres;Password=3911;"
},
"ApplicationInsights": {
"ConnectionString": "InstrumentationKey=6d2e76ee-5343-4691-a5e3-81add43cb584;IngestionEndpoint=https://northeurope-0.in.applicationinsights.azure.com/"
}
}