Adds Configuration Manager + tests
This commit is contained in:
parent
55e65a1b21
commit
384cc3c6fd
16 changed files with 657 additions and 137 deletions
2
.kdtooling/crm_connections.kdco
Normal file
2
.kdtooling/crm_connections.kdco
Normal file
|
|
@ -0,0 +1,2 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<ArrayOfCrmConnectionInfo xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://kdtooling.kupp.at/kdtooling/schema_1.0" />
|
||||||
|
|
@ -1,11 +1,9 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
namespace Core.Configurations
|
namespace Core.Configurations
|
||||||
{
|
{
|
||||||
public class ConfigurationManager
|
public class AzureConfigurationManager
|
||||||
{
|
{
|
||||||
private static IConfigurationBuilder _configurationBuilder;
|
private static IConfigurationBuilder _configurationBuilder;
|
||||||
|
|
||||||
|
|
@ -1,57 +0,0 @@
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Core.Configurations
|
|
||||||
{
|
|
||||||
using Newtonsoft.Json;
|
|
||||||
using Newtonsoft.Json.Linq;
|
|
||||||
|
|
||||||
public class KeyValueNester
|
|
||||||
{
|
|
||||||
private JObject _rootObject = new JObject();
|
|
||||||
|
|
||||||
public void AddKeyValue(string key, string jsonValue)
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
// Parse input JSON value
|
|
||||||
var valueObject = JsonConvert.DeserializeObject<JObject>(jsonValue);
|
|
||||||
var parts = key.Split(':');
|
|
||||||
|
|
||||||
// Start med root object eller nuværende struktur
|
|
||||||
JObject current = _rootObject;
|
|
||||||
|
|
||||||
// Gennemgå hver del af key'en
|
|
||||||
for (int i = 0; i < parts.Length - 1; i++)
|
|
||||||
{
|
|
||||||
string part = parts[i];
|
|
||||||
if (!current.ContainsKey(part))
|
|
||||||
{
|
|
||||||
current[part] = new JObject();
|
|
||||||
}
|
|
||||||
current = (JObject)current[part];
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tilføj den sidste værdi
|
|
||||||
current[parts[^1]] = valueObject;
|
|
||||||
}
|
|
||||||
catch (JsonReaderException ex)
|
|
||||||
{
|
|
||||||
throw new ArgumentException("Invalid JSON value", nameof(jsonValue), ex);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public JObject GetResult()
|
|
||||||
{
|
|
||||||
return _rootObject;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Clear()
|
|
||||||
{
|
|
||||||
_rootObject = new JObject();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
Core/Configurations/ConfigurationManager/AppConfiguration.cs
Normal file
14
Core/Configurations/ConfigurationManager/AppConfiguration.cs
Normal file
|
|
@ -0,0 +1,14 @@
|
||||||
|
namespace Configuration.Core;
|
||||||
|
public class AppConfiguration
|
||||||
|
{
|
||||||
|
public long Id { get; set; }
|
||||||
|
public string Key { get; set; }
|
||||||
|
public string Value { get; set; }
|
||||||
|
public string Label { get; set; }
|
||||||
|
public string ContentType { get; set; }
|
||||||
|
public DateTime? ValidFrom { get; set; }
|
||||||
|
public DateTime? ExpiresAt { get; set; }
|
||||||
|
public DateTime? CreatedAt { get; set; }
|
||||||
|
public DateTime? ModifiedAt { get; set; }
|
||||||
|
public Guid? Etag { get; set; }
|
||||||
|
}
|
||||||
28
Core/Configurations/ConfigurationManager/Class1.cs
Normal file
28
Core/Configurations/ConfigurationManager/Class1.cs
Normal file
|
|
@ -0,0 +1,28 @@
|
||||||
|
using Configuration.Core;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace Core.Configurations.ConfigurationManager
|
||||||
|
{
|
||||||
|
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
using System.Data;
|
||||||
|
using Insight.Database;
|
||||||
|
|
||||||
|
namespace Configuration.Core;
|
||||||
|
public class ConfigurationRepository : IConfigurationRepository
|
||||||
|
{
|
||||||
|
private readonly IDbConnection _connection;
|
||||||
|
|
||||||
|
public ConfigurationRepository(IDbConnection connection)
|
||||||
|
{
|
||||||
|
_connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task<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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,5 @@
|
||||||
|
namespace Configuration.Core;
|
||||||
|
public interface IConfigurationRepository
|
||||||
|
{
|
||||||
|
Task<IEnumerable<AppConfiguration>> GetActiveConfigurations();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,31 @@
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using System.Data;
|
||||||
|
|
||||||
|
namespace Configuration.Core;
|
||||||
|
public class JsonConfiguration : IConfiguration
|
||||||
|
{
|
||||||
|
private readonly JObject _data;
|
||||||
|
private readonly IChangeToken _changeToken;
|
||||||
|
|
||||||
|
public JsonConfiguration(JObject data, IChangeToken changeToken)
|
||||||
|
{
|
||||||
|
_data = data;
|
||||||
|
_changeToken = changeToken;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string this[string key]
|
||||||
|
{
|
||||||
|
get => _data.SelectToken(key.Replace(":", "."))?.ToString();
|
||||||
|
set => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConfigurationSection GetSection(string key) =>
|
||||||
|
new JsonConfigurationSection(_data, key);
|
||||||
|
|
||||||
|
public IEnumerable<IConfigurationSection> GetChildren() =>
|
||||||
|
_data.Properties().Select(p => new JsonConfigurationSection(_data, p.Name));
|
||||||
|
|
||||||
|
public IChangeToken GetReloadToken() => _changeToken;
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
using Microsoft.Extensions.Primitives;
|
||||||
|
using System.Data;
|
||||||
|
|
||||||
|
namespace Configuration.Core;
|
||||||
|
public class JsonConfigurationSection : IConfigurationSection
|
||||||
|
{
|
||||||
|
private readonly JObject _data;
|
||||||
|
private readonly string _path;
|
||||||
|
|
||||||
|
public JsonConfigurationSection(JObject data, string path)
|
||||||
|
{
|
||||||
|
_data = data;
|
||||||
|
_path = path;
|
||||||
|
}
|
||||||
|
|
||||||
|
public string this[string key]
|
||||||
|
{
|
||||||
|
get => _data.SelectToken($"{_path.Replace(":", ".")}.{key.Replace(":", ".")}")?.ToString();
|
||||||
|
set => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
public string Key => _path.Split(':').Last();
|
||||||
|
public string Path => _path;
|
||||||
|
public string Value
|
||||||
|
{
|
||||||
|
get => _data.SelectToken(_path.Replace(":", "."))?.ToString();
|
||||||
|
set => throw new NotImplementedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public IConfigurationSection GetSection(string key) =>
|
||||||
|
new JsonConfigurationSection(_data, string.IsNullOrEmpty(_path) ? key : $"{_path}:{key}");
|
||||||
|
|
||||||
|
public JToken GetToken() => _data.SelectToken(_path.Replace(":", "."));
|
||||||
|
|
||||||
|
public IEnumerable<IConfigurationSection> GetChildren()
|
||||||
|
{
|
||||||
|
var token = _data.SelectToken(_path.Replace(":", "."));
|
||||||
|
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(_path.Replace(":", "."));
|
||||||
|
return token?.ToObject<T>();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IChangeToken GetReloadToken() => new ConfigurationReloadToken();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,56 @@
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using Microsoft.Extensions.Configuration;
|
||||||
|
|
||||||
|
namespace Configuration.Core;
|
||||||
|
public class KeyValueConfigurationBuilder
|
||||||
|
{
|
||||||
|
private readonly IConfigurationRepository _repository;
|
||||||
|
private readonly JObject _rootObject = new();
|
||||||
|
private ConfigurationReloadToken _reloadToken = new();
|
||||||
|
private IConfiguration _configuration;
|
||||||
|
|
||||||
|
public KeyValueConfigurationBuilder(IConfigurationRepository repository)
|
||||||
|
{
|
||||||
|
_repository = repository;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LoadConfiguration()
|
||||||
|
{
|
||||||
|
var configurations = await _repository.GetActiveConfigurations();
|
||||||
|
foreach (var config in configurations)
|
||||||
|
{
|
||||||
|
AddKeyValue(config.Key, config.Value);
|
||||||
|
}
|
||||||
|
OnReload();
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddKeyValue(string key, string jsonValue)
|
||||||
|
{
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnReload()
|
||||||
|
{
|
||||||
|
var previousToken = Interlocked.Exchange(ref _reloadToken, new ConfigurationReloadToken());
|
||||||
|
previousToken.OnReload();
|
||||||
|
_configuration = null;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IConfiguration Build() =>
|
||||||
|
_configuration ??= new JsonConfiguration(_rootObject, _reloadToken);
|
||||||
|
}
|
||||||
154
Database/AppConfigurationSystem/ConfigurationDatabaseSetup.cs
Normal file
154
Database/AppConfigurationSystem/ConfigurationDatabaseSetup.cs
Normal file
|
|
@ -0,0 +1,154 @@
|
||||||
|
using Insight.Database;
|
||||||
|
using System.Data;
|
||||||
|
|
||||||
|
namespace Database.AppConfigurationSystem;
|
||||||
|
|
||||||
|
public class ConfigurationDatabaseSetup
|
||||||
|
{
|
||||||
|
private readonly IDbConnection _connection;
|
||||||
|
|
||||||
|
public ConfigurationDatabaseSetup(IDbConnection connection)
|
||||||
|
{
|
||||||
|
_connection = connection;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateConfigurationTable()
|
||||||
|
{
|
||||||
|
const string sql = @"
|
||||||
|
CREATE TABLE prod.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)
|
||||||
|
);";
|
||||||
|
await _connection.ExecuteAsync(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateHistoryTable()
|
||||||
|
{
|
||||||
|
const string sql = @"
|
||||||
|
CREATE TABLE prod.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)
|
||||||
|
);";
|
||||||
|
await _connection.ExecuteAsync(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateConfigurationIndexes()
|
||||||
|
{
|
||||||
|
const string sql = @"
|
||||||
|
CREATE INDEX idx_app_configuration_key ON prod.app_configuration(""key"");
|
||||||
|
CREATE INDEX idx_app_configuration_validity ON prod.app_configuration(valid_from, expires_at);";
|
||||||
|
await _connection.ExecuteAsync(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateModifiedAtTrigger()
|
||||||
|
{
|
||||||
|
const string sql = @"
|
||||||
|
CREATE OR REPLACE FUNCTION prod.update_app_configuration_modified_at()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
NEW.modified_at = CURRENT_TIMESTAMP;
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_app_configuration_modified_at
|
||||||
|
BEFORE UPDATE ON prod.app_configuration
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION prod.update_app_configuration_modified_at();";
|
||||||
|
await _connection.ExecuteAsync(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateNotifyTrigger()
|
||||||
|
{
|
||||||
|
const string sql = @"
|
||||||
|
CREATE OR REPLACE FUNCTION prod.notify_app_configuration_change()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
PERFORM pg_notify('config_changes', NEW.key);
|
||||||
|
RETURN NEW;
|
||||||
|
END;
|
||||||
|
$$ LANGUAGE plpgsql;
|
||||||
|
|
||||||
|
CREATE TRIGGER trg_app_configuration_notify
|
||||||
|
AFTER INSERT OR UPDATE ON prod.app_configuration
|
||||||
|
FOR EACH ROW
|
||||||
|
EXECUTE FUNCTION prod.notify_app_configuration_change();";
|
||||||
|
await _connection.ExecuteAsync(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateHistoryTrigger()
|
||||||
|
{
|
||||||
|
const string sql = @"
|
||||||
|
CREATE OR REPLACE FUNCTION prod.log_app_configuration_changes()
|
||||||
|
RETURNS TRIGGER AS $$
|
||||||
|
BEGIN
|
||||||
|
IF (TG_OP = 'INSERT') THEN
|
||||||
|
INSERT INTO prod.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 prod.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 prod.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 TRIGGER trg_app_configuration_history
|
||||||
|
AFTER INSERT OR UPDATE OR DELETE ON prod.app_configuration
|
||||||
|
FOR EACH ROW EXECUTE FUNCTION prod.log_app_configuration_changes();";
|
||||||
|
await _connection.ExecuteAsync(sql);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task CreateDatabaseStructure(IDbConnection connection)
|
||||||
|
{
|
||||||
|
await CreateConfigurationTable();
|
||||||
|
await CreateHistoryTable();
|
||||||
|
await CreateConfigurationIndexes();
|
||||||
|
await CreateModifiedAtTrigger();
|
||||||
|
await CreateNotifyTrigger();
|
||||||
|
await CreateHistoryTrigger();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -10,9 +10,7 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Application", "Application\
|
||||||
EndProject
|
EndProject
|
||||||
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Database", "Database\Database.csproj", "{D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Database", "Database\Database.csproj", "{D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}"
|
||||||
EndProject
|
EndProject
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestPostgresql", "TestPostgresLISTEN\TestPostgresql.csproj", "{743EF625-6C74-419C-A492-AA069956F471}"
|
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SetupInfrastructure", "SetupInfrastructure\SetupInfrastructure.csproj", "{48300227-BCBB-45A3-8359-9064DA85B1F9}"
|
||||||
EndProject
|
|
||||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SetupInfrastructure", "SetupInfrastructure\SetupInfrastructure.csproj", "{48300227-BCBB-45A3-8359-9064DA85B1F9}"
|
|
||||||
EndProject
|
EndProject
|
||||||
Global
|
Global
|
||||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||||
|
|
@ -20,14 +18,6 @@ Global
|
||||||
Release|Any CPU = Release|Any CPU
|
Release|Any CPU = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
GlobalSection(ProjectConfigurationPlatforms) = postSolution
|
||||||
{48300227-BCBB-45A3-8359-9064DA85B1F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{48300227-BCBB-45A3-8359-9064DA85B1F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{48300227-BCBB-45A3-8359-9064DA85B1F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{48300227-BCBB-45A3-8359-9064DA85B1F9}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{743EF625-6C74-419C-A492-AA069956F471}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
|
||||||
{743EF625-6C74-419C-A492-AA069956F471}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
|
||||||
{743EF625-6C74-419C-A492-AA069956F471}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
|
||||||
{743EF625-6C74-419C-A492-AA069956F471}.Release|Any CPU.Build.0 = Release|Any CPU
|
|
||||||
{7B554252-1CE4-44BD-B108-B0BDCCB24742}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
{7B554252-1CE4-44BD-B108-B0BDCCB24742}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
{7B554252-1CE4-44BD-B108-B0BDCCB24742}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{7B554252-1CE4-44BD-B108-B0BDCCB24742}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{7B554252-1CE4-44BD-B108-B0BDCCB24742}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{7B554252-1CE4-44BD-B108-B0BDCCB24742}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
|
@ -44,6 +34,10 @@ Global
|
||||||
{D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
{D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
{D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
{D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
{D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}.Release|Any CPU.Build.0 = Release|Any CPU
|
{D5096A7F-E6D4-4C87-874E-2D9A6BEAD57F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
|
{48300227-BCBB-45A3-8359-9064DA85B1F9}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||||
|
{48300227-BCBB-45A3-8359-9064DA85B1F9}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||||
|
{48300227-BCBB-45A3-8359-9064DA85B1F9}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||||
|
{48300227-BCBB-45A3-8359-9064DA85B1F9}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||||
EndGlobalSection
|
EndGlobalSection
|
||||||
GlobalSection(SolutionProperties) = preSolution
|
GlobalSection(SolutionProperties) = preSolution
|
||||||
HideSolutionNode = FALSE
|
HideSolutionNode = FALSE
|
||||||
|
|
|
||||||
43
Tests/CodeSnippets/TestPostgresLISTENNOTIFY.cs
Normal file
43
Tests/CodeSnippets/TestPostgresLISTENNOTIFY.cs
Normal file
|
|
@ -0,0 +1,43 @@
|
||||||
|
using Npgsql;
|
||||||
|
|
||||||
|
class TestPostgresLISTENNOTIFY
|
||||||
|
{
|
||||||
|
static async Task Main(string[] args)
|
||||||
|
{
|
||||||
|
var connectionString = "Host=192.168.1.57;Database=ptdb01;Username=postgres;Password=3911";
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
await using NpgsqlConnection conn = new NpgsqlConnection(connectionString);
|
||||||
|
await conn.OpenAsync();
|
||||||
|
|
||||||
|
Console.WriteLine("Forbundet til databasen. Lytter efter notifikationer...");
|
||||||
|
|
||||||
|
conn.Notification += (o, e) =>
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Notifikation modtaget:");
|
||||||
|
Console.WriteLine($" PID: {e.PID}");
|
||||||
|
Console.WriteLine($" Kanal: {e.Channel}");
|
||||||
|
Console.WriteLine($" Payload: {e.Payload}");
|
||||||
|
Console.WriteLine("------------------------");
|
||||||
|
};
|
||||||
|
|
||||||
|
await using (var cmd = new NpgsqlCommand("LISTEN config_changes;", conn))
|
||||||
|
{
|
||||||
|
await cmd.ExecuteNonQueryAsync();
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.WriteLine("Tryk på en tast for at stoppe...");
|
||||||
|
|
||||||
|
while (!Console.KeyAvailable)
|
||||||
|
{
|
||||||
|
await conn.WaitAsync();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.WriteLine($"Der opstod en fejl: {ex.Message}");
|
||||||
|
Console.WriteLine($"Stack trace: {ex.StackTrace}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
171
Tests/TestConfigurationManagement.cs
Normal file
171
Tests/TestConfigurationManagement.cs
Normal file
|
|
@ -0,0 +1,171 @@
|
||||||
|
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
using Moq;
|
||||||
|
using Newtonsoft.Json.Linq;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Tests;
|
||||||
|
using Core.Configurations.ConfigurationManager;
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
var configuration = 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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,89 +9,89 @@ using Microsoft.VisualStudio.TestTools.UnitTesting;
|
||||||
|
|
||||||
namespace Tests
|
namespace Tests
|
||||||
{
|
{
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Act as base class for tests. Avoids duplication of test setup code
|
/// Act as base class for tests. Avoids duplication of test setup code
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[TestClass]
|
[TestClass]
|
||||||
public abstract partial class TestFixture
|
public abstract partial class TestFixture
|
||||||
{
|
{
|
||||||
protected IContainer Container { get; private set; }
|
protected IContainer Container { get; private set; }
|
||||||
protected ContainerBuilder ContainerBuilder { get; private set; }
|
protected ContainerBuilder ContainerBuilder { get; private set; }
|
||||||
|
|
||||||
|
|
||||||
[AssemblyInitialize]
|
[AssemblyInitialize]
|
||||||
public static void AssemblySetup(TestContext tc)
|
public static void AssemblySetup(TestContext tc)
|
||||||
{
|
{
|
||||||
Trace.Listeners.Add(new TextWriterTraceListener(Console.Out));
|
Trace.Listeners.Add(new TextWriterTraceListener(Console.Out));
|
||||||
|
|
||||||
var envConfiguration = new ConfigurationBuilder()
|
var envConfiguration = new ConfigurationBuilder()
|
||||||
.AddEnvironmentVariables()
|
.AddEnvironmentVariables()
|
||||||
.Build();
|
.Build();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public virtual IConfigurationRoot Configuration()
|
public virtual IConfigurationRoot Configuration()
|
||||||
{
|
{
|
||||||
|
|
||||||
IConfigurationBuilder configBuilder = Core.Configurations.ConfigurationManager.AppConfigBuilder("appsettings.dev.json");
|
IConfigurationBuilder configBuilder = Core.Configurations.AzureConfigurationManager.AppConfigBuilder("appsettings.dev.json");
|
||||||
IConfigurationRoot configuration = configBuilder.Build();
|
IConfigurationRoot configuration = configBuilder.Build();
|
||||||
|
|
||||||
return configuration;
|
return configuration;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Should not be overriden. Rather override PreArrangeAll to setup data needed for a test class.
|
/// Should not be overriden. Rather override PreArrangeAll to setup data needed for a test class.
|
||||||
/// Override PrebuildContainer with a method that does nothing to prevent early build of IOC container
|
/// Override PrebuildContainer with a method that does nothing to prevent early build of IOC container
|
||||||
/// </summary>
|
/// </summary>
|
||||||
[TestInitialize]
|
[TestInitialize]
|
||||||
public void Setup()
|
public void Setup()
|
||||||
{
|
{
|
||||||
CreateContainerBuilder();
|
CreateContainerBuilder();
|
||||||
Container = ContainerBuilder.Build();
|
Container = ContainerBuilder.Build();
|
||||||
Insight.Database.Providers.PostgreSQL.PostgreSQLInsightDbProvider.RegisterProvider();
|
Insight.Database.Providers.PostgreSQL.PostgreSQLInsightDbProvider.RegisterProvider();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
protected virtual void CreateContainerBuilder()
|
protected virtual void CreateContainerBuilder()
|
||||||
{
|
{
|
||||||
IConfigurationRoot configuration = Configuration();
|
IConfigurationRoot configuration = Configuration();
|
||||||
|
|
||||||
var builder = new ContainerBuilder();
|
var builder = new ContainerBuilder();
|
||||||
builder.RegisterInstance(new LoggerFactory())
|
builder.RegisterInstance(new LoggerFactory())
|
||||||
.As<ILoggerFactory>();
|
.As<ILoggerFactory>();
|
||||||
|
|
||||||
builder.RegisterGeneric(typeof(Logger<>))
|
builder.RegisterGeneric(typeof(Logger<>))
|
||||||
.As(typeof(ILogger<>))
|
.As(typeof(ILogger<>))
|
||||||
.SingleInstance();
|
.SingleInstance();
|
||||||
|
|
||||||
builder.RegisterModule(new Core.ModuleRegistry.DbPostgreSqlModule
|
builder.RegisterModule(new Core.ModuleRegistry.DbPostgreSqlModule
|
||||||
{
|
{
|
||||||
ConnectionString = configuration.GetConnectionString("ptdb")
|
ConnectionString = configuration.GetConnectionString("ptdb")
|
||||||
});
|
});
|
||||||
|
|
||||||
builder.RegisterModule(new Core.ModuleRegistry.TelemetryModule
|
builder.RegisterModule(new Core.ModuleRegistry.TelemetryModule
|
||||||
{
|
{
|
||||||
TelemetryConfig = configuration.GetSection("ApplicationInsights").Get<Core.ModuleRegistry.TelemetryConfig>()
|
TelemetryConfig = configuration.GetSection("ApplicationInsights").Get<Core.ModuleRegistry.TelemetryConfig>()
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
ContainerBuilder = builder;
|
ContainerBuilder = builder;
|
||||||
}
|
}
|
||||||
|
|
||||||
[TestCleanup]
|
[TestCleanup]
|
||||||
public void CleanUp()
|
public void CleanUp()
|
||||||
{
|
{
|
||||||
Trace.Flush();
|
Trace.Flush();
|
||||||
var telemetryClient = Container.Resolve<TelemetryClient>();
|
var telemetryClient = Container.Resolve<TelemetryClient>();
|
||||||
telemetryClient.Flush();
|
telemetryClient.Flush();
|
||||||
|
|
||||||
if (Container != null)
|
if (Container != null)
|
||||||
{
|
{
|
||||||
Container.Dispose();
|
Container.Dispose();
|
||||||
Container = null;
|
Container = null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -11,6 +11,7 @@
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
||||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
<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" />
|
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
|
||||||
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
|
<PackageReference Include="MSTest.TestFramework" Version="3.1.1" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue