This commit is contained in:
Janus C. H. Knudsen 2026-01-10 20:39:17 +01:00
parent 54b057886c
commit 7fc1ae0650
204 changed files with 4345 additions and 134 deletions

View file

@ -0,0 +1,85 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace PlanTempus.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

@ -0,0 +1,157 @@
using Newtonsoft.Json.Linq;
namespace PlanTempus.Core.Configurations
{
public interface IConfigurationBuilder
{
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)
{
((IConfigurationBuilder)this).ConfigurationProviders.Add(provider);
return this;
}
public IConfigurationRoot Build()
{
foreach (var provider in ConfigurationProviders)
{
provider.Build();
}
//TODO: we need to come up with merge strategy, right now the latest key-path dominates
return new ConfigurationRoot(ConfigurationProviders);
}
}
public class Configuration : IConfiguration
{
List<IConfigurationProvider> _providers = [];
/// <summary>
/// Implements a string-based indexer for backwards compatibility with Microsoft.Extensions.Configuration.
/// This implementation is marked as obsolete and should be replaced with type-safe alternatives.
/// </summary>
/// <param name="key">The configuration key to retrieve.</param>
/// <returns>The configuration value for the specified key.</returns>
/// <exception cref="NotSupportedException">Thrown when attempting to set a value, as this operation is not supported.</exception>
[Obsolete("Use type-safe configuration methods instead")]
public string this[string key]
{
get => GetConfiguration(_providers, key);
set => throw new NotSupportedException();
}
List<IConfigurationProvider> IConfiguration.ConfigurationProviders
{
get { return _providers; }
set { _providers = value; }
}
internal static string GetConfiguration(IList<IConfigurationProvider> providers, string key)
{
string value = null;
foreach (var provider in providers)
{
var test = provider.Configuration().SelectToken(ConfigurationBinder.NormalizePath(key));
if (test != null)
value = test.ToString();
}
return value;
}
}
public class ConfigurationRoot : Configuration, IConfigurationRoot
{
public ConfigurationRoot(List<IConfigurationProvider> configurationProviders)
{
((IConfiguration)this).ConfigurationProviders = configurationProviders;
}
}
public static class ConfigurationBinder
{
public 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, bool optional = true)
{
JToken value = null;
foreach (var provider in configuration.ConfigurationProviders)
{
var test = provider.Configuration().SelectToken(NormalizePath(path));
if (test != null)
value = test;
}
if(value is null && !optional)
throw new Exceptions.ConfigurationException($"Path not found in configuration, path: {path}");
if (value is null)
return default;
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
{
void Build();
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 Path { get; }
string Key { get; }
JToken Value { get; set; }
}
}

View file

@ -0,0 +1,9 @@
namespace PlanTempus.Core.Configurations
{
/// <summary>
/// Marker interface for application configurations that should be automatically registered in the DI container.
/// Classes implementing this interface will be loaded from configuration and registered as singletons.
/// </summary>
public interface IAppConfiguration { }
}

View file

@ -0,0 +1,10 @@
namespace PlanTempus.Core.Configurations
{
public interface IConfigurationRoot : IConfiguration { }
public interface IConfiguration
{
internal List<IConfigurationProvider> ConfigurationProviders { get; set; }
string this[string key] { get; set; }
}
}

View file

@ -0,0 +1,63 @@
using PlanTempus.Core.Exceptions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PlanTempus.Core.Configurations;
namespace PlanTempus.Core.Configurations.JsonConfigProvider
{
public static class JsonConfigExtension
{
/// <summary>
/// Adds a JSON configuration source to the configuration builder.
/// </summary>
/// <param name="builder">The configuration builder to add to</param>
/// <param name="configurationFilePath">Path to the JSON configuration file. Defaults to "appconfiguration.json"</param>
/// <param name="optional">If true, the configuration file is optional. Defaults to true</param>
/// <param name="reloadOnChange">If true, the configuration will be reloaded when the file changes. Defaults to false</param>
/// <returns>The configuration builder</returns>
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

@ -0,0 +1,14 @@
namespace PlanTempus.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,7 @@
namespace PlanTempus.Core.Configurations.SmartConfigProvider;
public interface IConfigurationRepository
{
string ConnectionString { get; set; }
IEnumerable<AppConfiguration> GetActiveConfigurations();
}

View file

@ -0,0 +1,34 @@
using System.Data;
using Insight.Database;
namespace PlanTempus.Core.Configurations.SmartConfigProvider.Repositories;
public class PostgresConfigurationRepository : IConfigurationRepository
{
private IDbConnection _connection;
public string ConnectionString { get; set; }
public PostgresConfigurationRepository(string connectionString)
{
_connection = new Npgsql.NpgsqlConnection(connectionString);
}
public PostgresConfigurationRepository()
{
}
public IEnumerable<AppConfiguration> GetActiveConfigurations()
{
_connection ??= new Npgsql.NpgsqlConnection(ConnectionString);
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,35 @@
using PlanTempus.Core.Configurations;
namespace PlanTempus.Core.Configurations.SmartConfigProvider
{
/// <summary>
/// Extension methods for adding smart configuration providers to IConfigurationBuilder.
/// </summary>
public static class SmartConfigExtension
{
/// <summary>
/// Adds a smart configuration provider using a connection string from appsettings.
/// </summary>
/// <param name="builder">The configuration builder to add to</param>
/// <param name="configKey">The key to find the connection string in the ConnectionStrings section. Defaults to "DefaultConnection"</param>
/// <param name="path">Optional path to configuration file if different from default appsettings location</param>
/// <returns>The configuration builder</returns>
public static IConfigurationBuilder AddSmartConfig(this IConfigurationBuilder builder, string configKey = "DefaultConnection", string path = null)
{
return builder.AddProvider(new SmartConfigProvider(builder, configKey, path));
}
/// <summary>
/// Adds a smart configuration provider with custom configuration options.
/// </summary>
/// <param name="builder">The configuration builder to add to</param>
/// <param name="setupAction">Action to configure the smart configuration options</param>
/// <returns>The configuration builder</returns>
public static IConfigurationBuilder AddSmartConfig(this IConfigurationBuilder builder, Action<SmartConfigOptions> setupAction)
{
var options = new SmartConfigOptions();
setupAction(options);
return builder.AddProvider(new SmartConfigProvider(builder, options));
}
}
}

View file

@ -0,0 +1,45 @@
namespace PlanTempus.Core.Configurations.SmartConfigProvider
{
/// <summary>
/// Configuration options for setting up smart configuration providers.
/// Provides fluent configuration methods for specifying the repository type and settings.
/// </summary>
public class SmartConfigOptions
{
private IConfigurationRepository _repository;
internal string _configKey;
/// <summary>
/// Configures the smart configuration to use PostgreSQL as the configuration store.
/// </summary>
/// <param name="configKey">The configuration key used to find the connection string</param>
/// <returns>The configuration options instance for method chaining</returns>
public SmartConfigOptions UsePostgres(string configKey)
{
_configKey = configKey;
_repository = new Repositories.PostgresConfigurationRepository();
return this;
}
/// <summary>
/// Configures the smart configuration to use SQL Server as the configuration store.
/// </summary>
/// <returns>The configuration options instance for method chaining</returns>
/// <exception cref="NotImplementedException">This feature is not yet implemented</exception>
public SmartConfigOptions UseSqlServer()
{
throw new NotImplementedException();
}
/// <summary>
/// Configures the smart configuration to use a custom configuration repository.
/// </summary>
/// <param name="repository">The configuration repository to use</param>
/// <returns>The configuration options instance for method chaining</returns>
public SmartConfigOptions UseRepository(IConfigurationRepository repository)
{
_repository = repository;
return this;
}
internal IConfigurationRepository GetRepository() => _repository;
}
}

View file

@ -0,0 +1,85 @@
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using PlanTempus.Core.Configurations.JsonConfigProvider;
using PlanTempus.Core.Exceptions;
namespace PlanTempus.Core.Configurations.SmartConfigProvider
{
/// <summary>
/// Configuration provider that loads configuration from a smart configuration source (e.g. database).
/// The provider reads connection details from a JSON file and uses them to connect to a configuration repository.
/// </summary>
/// <remarks>
/// The provider supports multiple initialization methods:
/// - Through SmartConfigOptions for flexible repository configuration
/// - Through direct configuration key and file path
/// Configuration is loaded from the repository during Build() and converted to a JSON structure.
/// </remarks>
public class SmartConfigProvider : IConfigurationProvider
{
string _configKey;
string _connectionString;
string _path;
IConfigurationBuilder _builder;
JObject _configuration;
SmartConfigOptions _smartConfigOptions;
public SmartConfigProvider() { }
public SmartConfigProvider(IConfigurationBuilder builder, SmartConfigOptions smartConfigOptions)
{
_builder = builder;
_smartConfigOptions = smartConfigOptions;
_configKey = smartConfigOptions._configKey;
SetConnectionString();
}
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 = (JObject)JToken.ReadFrom(reader);
_connectionString = jsonConfiguration.SelectToken($"ConnectionStrings.{_configKey}")?.ToString();
}
}
public void Build()
{
var repository = _smartConfigOptions.GetRepository();
repository.ConnectionString = _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 JObject Configuration()
{
return _configuration;
}
}
}