Initial commit: SWP.Core enterprise framework with multi-tenant architecture, configuration management, security, telemetry and comprehensive test suite

This commit is contained in:
Janus C. H. Knudsen 2025-08-02 22:16:39 +02:00
commit 5275a75502
87 changed files with 6140 additions and 0 deletions

View file

@ -0,0 +1,166 @@
using Insight.Database;
using System.Data;
using SWP.Core.Database.ConnectionFactory;
namespace SWP.Core.Database.ConfigurationManagementSystem;
public class SetupConfiguration(IDbConnectionFactory connectionFactory) : IDbConfigure<SetupConfiguration.Command>
{
public class Command { }
public void With(Command notInUse, ConnectionStringParameters parameters = null)
{
using var conn = parameters is null ? connectionFactory.Create() : connectionFactory.Create(parameters);
using var transaction = conn.OpenWithTransaction();
try
{
CreateConfigurationTable(conn);
CreateHistoryTable(conn);
CreateConfigurationIndexes(conn);
CreateModifiedAtTrigger(conn);
CreateNotifyTrigger(conn);
CreateHistoryTrigger(conn);
transaction.Commit();
}
catch (Exception ex)
{
transaction.Rollback();
throw new InvalidOperationException("Failed to SetupConfiguration in Database", ex);
}
}
void CreateConfigurationTable(IDbConnection db)
{
const string sql = @"
CREATE TABLE IF NOT EXISTS app_configuration (
id bigserial NOT NULL,
""key"" varchar(255) NOT NULL,
value text NULL,
""label"" varchar(255) NULL,
content_type varchar(255) DEFAULT 'text/plain'::character varying NULL,
valid_from timestamptz NULL,
expires_at timestamptz NULL,
created_at timestamptz DEFAULT CURRENT_TIMESTAMP NULL,
modified_at timestamptz DEFAULT CURRENT_TIMESTAMP NULL,
etag uuid DEFAULT gen_random_uuid() NULL,
CONSTRAINT app_configuration_pkey PRIMARY KEY (id)
);";
db.ExecuteSql(sql);
}
void CreateHistoryTable(IDbConnection db)
{
const string sql = @"
CREATE TABLE IF NOT EXISTS app_configuration_history (
history_id bigserial NOT NULL,
action_type char(1) NOT NULL,
action_timestamp timestamptz NOT NULL DEFAULT CURRENT_TIMESTAMP,
action_by text NOT NULL DEFAULT CURRENT_USER,
id bigint NOT NULL,
""key"" varchar(255) NOT NULL,
value text NULL,
""label"" varchar(255) NULL,
content_type varchar(255) NULL,
valid_from timestamptz NULL,
expires_at timestamptz NULL,
created_at timestamptz NULL,
modified_at timestamptz NULL,
etag uuid NULL,
CONSTRAINT app_configuration_history_pkey PRIMARY KEY (history_id)
);";
db.ExecuteSql(sql);
}
void CreateConfigurationIndexes(IDbConnection db)
{
const string sql = @"
CREATE INDEX IF NOT EXISTS idx_app_configuration_key ON app_configuration(""key"");
CREATE INDEX IF NOT EXISTS idx_app_configuration_validity ON app_configuration(valid_from, expires_at);";
db.ExecuteSql(sql);
}
void CreateModifiedAtTrigger(IDbConnection db)
{
const string sql = @"
CREATE OR REPLACE FUNCTION update_app_configuration_modified_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.modified_at = CURRENT_TIMESTAMP;
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER trg_app_configuration_modified_at
BEFORE UPDATE ON app_configuration
FOR EACH ROW
EXECUTE FUNCTION update_app_configuration_modified_at();";
db.ExecuteSql(sql);
}
void CreateNotifyTrigger(IDbConnection db)
{
const string sql = @"
CREATE OR REPLACE FUNCTION notify_app_configuration_change()
RETURNS TRIGGER AS $$
BEGIN
PERFORM pg_notify('config_changes', NEW.key);
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER trg_app_configuration_notify
AFTER INSERT OR UPDATE ON app_configuration
FOR EACH ROW
EXECUTE FUNCTION notify_app_configuration_change();";
db.ExecuteSql(sql);
}
void CreateHistoryTrigger(IDbConnection db)
{
const string sql = @"
CREATE OR REPLACE FUNCTION log_app_configuration_changes()
RETURNS TRIGGER AS $$
BEGIN
IF (TG_OP = 'INSERT') THEN
INSERT INTO app_configuration_history (
action_type, id, ""key"", value, label, content_type,
valid_from, expires_at, created_at, modified_at, etag
)
VALUES (
'I', NEW.id, NEW.key, NEW.value, NEW.label, NEW.content_type,
NEW.valid_from, NEW.expires_at, NEW.created_at, NEW.modified_at, NEW.etag
);
ELSIF (TG_OP = 'UPDATE') THEN
INSERT INTO app_configuration_history (
action_type, id, ""key"", value, label, content_type,
valid_from, expires_at, created_at, modified_at, etag
)
VALUES (
'U', OLD.id, OLD.key, OLD.value, OLD.label, OLD.content_type,
OLD.valid_from, OLD.expires_at, OLD.created_at, OLD.modified_at, OLD.etag
);
ELSIF (TG_OP = 'DELETE') THEN
INSERT INTO app_configuration_history (
action_type, id, ""key"", value, label, content_type,
valid_from, expires_at, created_at, modified_at, etag
)
VALUES (
'D', OLD.id, OLD.key, OLD.value, OLD.label, OLD.content_type,
OLD.valid_from, OLD.expires_at, OLD.created_at, OLD.modified_at, OLD.etag
);
END IF;
RETURN NULL;
END;
$$ LANGUAGE plpgsql;
CREATE OR REPLACE TRIGGER trg_app_configuration_history
AFTER INSERT OR UPDATE OR DELETE ON app_configuration
FOR EACH ROW EXECUTE FUNCTION log_app_configuration_changes();";
db.ExecuteSql(sql);
}
}

View file

@ -0,0 +1,8 @@
namespace SWP.Core.Database.ConnectionFactory
{
public interface IDbConnectionFactory
{
System.Data.IDbConnection Create();
System.Data.IDbConnection Create(ConnectionStringParameters connectionStringTemplateParameters);
}
}

View file

@ -0,0 +1,65 @@
using System.Data;
using Npgsql;
namespace SWP.Core.Database.ConnectionFactory
{
public record ConnectionStringParameters(string User, string Pwd);
public class PostgresConnectionFactory : IDbConnectionFactory, IAsyncDisposable
{
private readonly NpgsqlDataSource _baseDataSource;
private readonly Action<NpgsqlDataSourceBuilder> _configureDataSource;
private readonly Microsoft.Extensions.Logging.ILoggerFactory _loggerFactory; //this is not tested nor implemented, I just created it as an idea
public PostgresConnectionFactory(
string connectionString,
Microsoft.Extensions.Logging.ILoggerFactory loggerFactory = null,
Action<NpgsqlDataSourceBuilder> configureDataSource = null)
{
_loggerFactory = loggerFactory;
_configureDataSource = configureDataSource ?? (builder => { });
// Opret base data source med konfiguration
var dataSourceBuilder = new NpgsqlDataSourceBuilder(connectionString);
ConfigureDataSourceBuilder(dataSourceBuilder);
_baseDataSource = dataSourceBuilder.Build();
}
public IDbConnection Create()
{
return _baseDataSource.CreateConnection();
}
public IDbConnection Create(ConnectionStringParameters param)
{
var connectionStringBuilder = new NpgsqlConnectionStringBuilder(
_baseDataSource.ConnectionString)
{
Username = param.User,
Password = param.Pwd
};
var tempDataSourceBuilder = new NpgsqlDataSourceBuilder(
connectionStringBuilder.ToString());
ConfigureDataSourceBuilder(tempDataSourceBuilder);
var tempDataSource = tempDataSourceBuilder.Build();
return tempDataSource.CreateConnection();
}
private void ConfigureDataSourceBuilder(NpgsqlDataSourceBuilder builder)
{
if (_loggerFactory != null)
builder.UseLoggerFactory(_loggerFactory);
_configureDataSource?.Invoke(builder);
}
public async ValueTask DisposeAsync()
{
await _baseDataSource.DisposeAsync();
}
}
}

View file

@ -0,0 +1,38 @@
using System.Data;
using System.Diagnostics;
using Microsoft.ApplicationInsights.DataContracts;
using Microsoft.ApplicationInsights.Extensibility;
namespace SWP.Core.Database;
public class DatabaseScope : IDisposable
{
internal readonly IOperationHolder<DependencyTelemetry> _operation;
private readonly Stopwatch _stopwatch;
public DatabaseScope(IDbConnection connection, IOperationHolder<DependencyTelemetry> operation)
{
Connection = connection;
_operation = operation;
_operation.Telemetry.Success = true;
_operation.Telemetry.Timestamp = DateTimeOffset.UtcNow;
_stopwatch = Stopwatch.StartNew();
}
public IDbConnection Connection { get; }
public void Dispose()
{
_stopwatch.Stop();
_operation.Telemetry.Duration = _stopwatch.Elapsed;
_operation.Dispose();
Connection.Dispose();
}
public void Error(Exception ex)
{
_operation.Telemetry.Success = false;
_operation.Telemetry.Properties["Error"] = ex.Message;
}
}

View file

@ -0,0 +1,10 @@
using System.Data;
namespace SWP.Core.Database;
public interface IDatabaseOperations
{
DatabaseScope CreateScope(string operationName);
Task<T> ExecuteAsync<T>(Func<IDbConnection, Task<T>> operation, string operationName);
Task ExecuteAsync(Func<IDbConnection, Task> operation, string operationName);
}

View file

@ -0,0 +1,9 @@
using SWP.Core.Database.ConnectionFactory;
namespace SWP.Core.Database
{
public interface IDbConfigure<T>
{
void With(T command, ConnectionStringParameters parameters = null);
}
}

View file

@ -0,0 +1,25 @@
using Autofac;
using SWP.Core.Database.ConnectionFactory;
namespace SWP.Core.Database.ModuleRegistry
{
public class DbPostgreSqlModule : Module
{
public required string ConnectionString { get; set; }
protected override void Load(ContainerBuilder builder)
{
Insight.Database.Providers.PostgreSQL.PostgreSQLInsightDbProvider.RegisterProvider();
builder.RegisterType<PostgresConnectionFactory>()
.As<IDbConnectionFactory>()
.WithParameter(new TypedParameter(typeof(string), ConnectionString))
.SingleInstance();
builder.RegisterType<SqlOperations>()
.As<IDatabaseOperations>();
}
}
}

View file

@ -0,0 +1,57 @@
using System.Data;
using Microsoft.ApplicationInsights;
using Microsoft.ApplicationInsights.DataContracts;
using SWP.Core.Database.ConnectionFactory;
namespace SWP.Core.Database;
public class SqlOperations : IDatabaseOperations
{
private readonly IDbConnectionFactory _connectionFactory;
private readonly TelemetryClient _telemetryClient;
public SqlOperations(IDbConnectionFactory connectionFactory, TelemetryClient telemetryClient)
{
_connectionFactory = connectionFactory;
_telemetryClient = telemetryClient;
}
public DatabaseScope CreateScope(string operationName)
{
var connection = _connectionFactory.Create();
var operation = _telemetryClient.StartOperation<DependencyTelemetry>(operationName);
operation.Telemetry.Type = "SQL";
operation.Telemetry.Target = "PostgreSQL";
return new DatabaseScope(connection, operation);
}
public async Task<T> ExecuteAsync<T>(Func<IDbConnection, Task<T>> operation, string operationName)
{
using var scope = CreateScope(operationName);
try
{
var result = await operation(scope.Connection);
return result;
}
catch (Exception ex)
{
scope.Error(ex);
throw;
}
}
public async Task ExecuteAsync(Func<IDbConnection, Task> operation, string operationName)
{
using var scope = CreateScope(operationName);
try
{
await operation(scope.Connection);
}
catch (Exception ex)
{
scope.Error(ex);
throw;
}
}
}