Initial commit: SWP.Core enterprise framework with multi-tenant architecture, configuration management, security, telemetry and comprehensive test suite
This commit is contained in:
commit
5275a75502
87 changed files with 6140 additions and 0 deletions
75
Core/Configurations/Common/KeyValueToJson.cs
Normal file
75
Core/Configurations/Common/KeyValueToJson.cs
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace SWP.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())
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
152
Core/Configurations/ConfigurationBuilder.cs
Normal file
152
Core/Configurations/ConfigurationBuilder.cs
Normal file
|
|
@ -0,0 +1,152 @@
|
|||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace SWP.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)
|
||||
{
|
||||
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
|
||||
{
|
||||
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; }
|
||||
}
|
||||
}
|
||||
9
Core/Configurations/IAppConfiguration.cs
Normal file
9
Core/Configurations/IAppConfiguration.cs
Normal file
|
|
@ -0,0 +1,9 @@
|
|||
namespace SWP.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 { }
|
||||
|
||||
}
|
||||
10
Core/Configurations/IConfigurationRoot.cs
Normal file
10
Core/Configurations/IConfigurationRoot.cs
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
namespace SWP.Core.Configurations
|
||||
{
|
||||
public interface IConfigurationRoot : IConfiguration { }
|
||||
|
||||
public interface IConfiguration
|
||||
{
|
||||
internal List<IConfigurationProvider> ConfigurationProviders { get; set; }
|
||||
string this[string key] { get; set; }
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
using SWP.Core.Exceptions;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
|
||||
namespace SWP.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
14
Core/Configurations/SmartConfigProvider/AppConfiguration.cs
Normal file
14
Core/Configurations/SmartConfigProvider/AppConfiguration.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
|||
namespace SWP.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; }
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
namespace SWP.Core.Configurations.SmartConfigProvider;
|
||||
public interface IConfigurationRepository
|
||||
{
|
||||
string ConnectionString { get; set; }
|
||||
IEnumerable<AppConfiguration> GetActiveConfigurations();
|
||||
}
|
||||
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
using System.Data;
|
||||
using Insight.Database;
|
||||
using SWP.Core.Configurations.SmartConfigProvider;
|
||||
|
||||
namespace SWP.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);
|
||||
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,33 @@
|
|||
namespace SWP.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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
namespace SWP.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;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,85 @@
|
|||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Linq;
|
||||
using SWP.Core.Exceptions;
|
||||
using SWP.Core.Configurations.JsonConfigProvider;
|
||||
|
||||
namespace SWP.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;
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue