Auto stash before merge of "main" and "origin/main"
This commit is contained in:
parent
21d7128e74
commit
521190475d
41 changed files with 991 additions and 1150 deletions
|
|
@ -1,30 +1,29 @@
|
||||||
using Autofac;
|
using Autofac;
|
||||||
|
using Core.Configurations.JsonConfigProvider;
|
||||||
|
using Core.Configurations;
|
||||||
namespace PlanTempus
|
namespace PlanTempus
|
||||||
{
|
{
|
||||||
public class Startup
|
public class Startup
|
||||||
{
|
{
|
||||||
public Startup(IWebHostEnvironment env)
|
public Startup(IWebHostEnvironment env)
|
||||||
{
|
{
|
||||||
var builder = new ConfigurationBuilder()
|
var builder = new Core.Configurations.ConfigurationBuilder()
|
||||||
.SetBasePath(env.ContentRootPath)
|
.AddJsonFile("appconfiguration.json", optional: true, reloadOnChange: true);
|
||||||
.AddJsonFile("appsettings.json", optional: true, reloadOnChange: true)
|
|
||||||
.AddEnvironmentVariables();
|
|
||||||
|
|
||||||
ConfigurationRoot = builder.Build();
|
ConfigurationRoot = builder.Build();
|
||||||
}
|
}
|
||||||
|
|
||||||
public IConfigurationRoot ConfigurationRoot { get; private set; }
|
public Core.Configurations.IConfigurationRoot ConfigurationRoot { get; private set; }
|
||||||
|
|
||||||
public ILifetimeScope AutofacContainer { get; private set; }
|
public ILifetimeScope AutofacContainer { get; private set; }
|
||||||
|
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
services.AddControllers();
|
services.AddControllers();
|
||||||
services.AddOptions();
|
services.AddOptions();
|
||||||
services.AddRazorPages();
|
services.AddRazorPages();
|
||||||
//services.AddApplicationInsightsTelemetry(ConfigurationRoot["ApplicationInsights:InstrumentationKey"]);
|
//services.AddApplicationInsightsTelemetry(ConfigurationRoot["ApplicationInsights:InstrumentationKey"]);
|
||||||
services.AddAntiforgery(x => x.HeaderName = "X-ANTI-FORGERY-TOKEN");
|
services.AddAntiforgery(x => x.HeaderName = "X-ANTI-FORGERY-TOKEN");
|
||||||
|
|
||||||
services.Configure<Microsoft.AspNetCore.Mvc.Razor.RazorViewEngineOptions>(options =>
|
services.Configure<Microsoft.AspNetCore.Mvc.Razor.RazorViewEngineOptions>(options =>
|
||||||
{
|
{
|
||||||
|
|
@ -32,42 +31,43 @@ namespace PlanTempus
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
public void ConfigureContainer(ContainerBuilder builder)
|
public void ConfigureContainer(ContainerBuilder builder)
|
||||||
{
|
{
|
||||||
builder.RegisterModule(new Core.ModuleRegistry.DbPostgreSqlModule
|
builder.RegisterModule(new Core.ModuleRegistry.DbPostgreSqlModule
|
||||||
{
|
{
|
||||||
ConnectionString = ConfigurationRoot.GetConnectionString("ptdb")
|
ConnectionString = ConfigurationRoot.GetConnectionString("ptdb")
|
||||||
});
|
});
|
||||||
|
|
||||||
|
builder.RegisterModule(new Core.ModuleRegistry.TelemetryModule
|
||||||
|
{
|
||||||
|
TelemetryConfig = ConfigurationRoot.GetSection(nameof(Core.ModuleRegistry.TelemetryConfig)).Get<Core.ModuleRegistry.TelemetryConfig>()
|
||||||
|
});
|
||||||
|
|
||||||
builder.RegisterModule(new Core.ModuleRegistry.TelemetryModule
|
|
||||||
{
|
|
||||||
TelemetryConfig = ConfigurationRoot.GetSection(nameof(Core.ModuleRegistry.TelemetryConfig)).Get<Core.ModuleRegistry.TelemetryConfig>()
|
|
||||||
});
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||||
{
|
{
|
||||||
|
|
||||||
if (env.IsDevelopment())
|
if (env.IsDevelopment())
|
||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
app.UseExceptionHandler("/Error");
|
app.UseExceptionHandler("/Error");
|
||||||
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
|
||||||
app.UseHsts();
|
app.UseHsts();
|
||||||
}
|
}
|
||||||
app.UseHttpsRedirection();
|
app.UseHttpsRedirection();
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
|
||||||
app.UseStaticFiles();
|
app.UseStaticFiles();
|
||||||
app.UseEndpoints(endpoints =>
|
app.UseEndpoints(endpoints =>
|
||||||
{
|
{
|
||||||
//endpoints.MapHub<MotionHub>("/motionhub");
|
//endpoints.MapHub<MotionHub>("/motionhub");
|
||||||
endpoints.MapRazorPages();
|
endpoints.MapRazorPages();
|
||||||
endpoints.MapControllers();
|
endpoints.MapControllers();
|
||||||
});
|
});
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,5 +5,10 @@
|
||||||
},
|
},
|
||||||
"TelemetryConfig": {
|
"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"
|
"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" ]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
85
Core/Configurations/Common/KeyValueToJson.cs
Normal file
85
Core/Configurations/Common/KeyValueToJson.cs
Normal 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())
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,61 +1,116 @@
|
||||||
using System;
|
using Newtonsoft.Json.Linq;
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Core.Configurations
|
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 ConfigurationBuilder AddProvider(IConfigurationProvider provider)
|
public class ConfigurationBuilder : IConfigurationBuilder
|
||||||
{
|
{
|
||||||
_providers.Add(provider);
|
public List<IConfigurationProvider> ConfigurationProviders { get; private set; } = [];
|
||||||
return this;
|
|
||||||
}
|
|
||||||
|
|
||||||
public IConfigurationRoot Build()
|
public ConfigurationBuilder AddProvider(IConfigurationProvider provider)
|
||||||
{
|
{
|
||||||
// Her kan du implementere din egen sammenlægningslogik
|
((IConfigurationBuilder)this).ConfigurationProviders.Add(provider);
|
||||||
return new ConfigurationRoot(_providers);
|
return this;
|
||||||
}
|
}
|
||||||
}
|
public IConfigurationRoot Build()
|
||||||
|
{
|
||||||
|
foreach (var provider in ConfigurationProviders)
|
||||||
|
{
|
||||||
|
provider.Build();
|
||||||
|
}
|
||||||
|
//TODO: we need to come up with merge strategy
|
||||||
|
|
||||||
public class ConfigurationRoot : IConfigurationRoot
|
return new ConfigurationRoot(ConfigurationProviders);
|
||||||
{
|
}
|
||||||
public ConfigurationRoot(List<IConfigurationProvider> configurationProviders)
|
}
|
||||||
{
|
|
||||||
|
|
||||||
}
|
public class ConfigurationRoot : IConfigurationRoot
|
||||||
public T GetSection<T>(string key)
|
{
|
||||||
{
|
List<IConfigurationProvider> IConfigurationRoot.ConfigurationProviders { get; set; }
|
||||||
throw new NotImplementedException();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
public static class ConfigurationPredicateExtensions
|
|
||||||
{
|
|
||||||
|
|
||||||
public static IConfigurationSection GetSection(this IConfigurationRoot configurationSection, string key)
|
public ConfigurationRoot(List<IConfigurationProvider> configurationProviders)
|
||||||
{
|
{
|
||||||
return null;
|
((IConfigurationRoot)this).ConfigurationProviders = configurationProviders;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static T Get<T>(this IConfigurationSection configuration, string key)
|
}
|
||||||
{
|
public static class ConfigurationBinder
|
||||||
return default(T);
|
{
|
||||||
}
|
private static string NormalizePath(string path)
|
||||||
}
|
{
|
||||||
public interface IConfigurationProvider
|
return path?.Replace(":", ".", StringComparison.Ordinal) ?? string.Empty;
|
||||||
{
|
}
|
||||||
Dictionary<string, object> Configuration();
|
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));
|
||||||
|
|
||||||
public interface IConfigurationSection
|
if (test != null)
|
||||||
{
|
value = test;
|
||||||
string Key { get; }
|
}
|
||||||
string Path { get; }
|
|
||||||
string Value { get; set; }
|
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();
|
||||||
|
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 Path { get; }
|
||||||
|
string Key { get; }
|
||||||
|
Newtonsoft.Json.Linq.JToken Value { get; set; }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,14 +1,7 @@
|
||||||
using System;
|
namespace Core.Configurations
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
|
|
||||||
namespace Core.Configurations
|
|
||||||
{
|
{
|
||||||
public interface IConfigurationRoot
|
public interface IConfigurationRoot
|
||||||
{
|
{
|
||||||
|
internal List<IConfigurationProvider> ConfigurationProviders { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
14
Core/Configurations/SmartConfigProvider/AppConfiguration.cs
Normal file
14
Core/Configurations/SmartConfigProvider/AppConfiguration.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,7 @@
|
||||||
|
using Core.Configurations.SmartConfigProvider;
|
||||||
|
|
||||||
|
namespace Core.Configurations.SmartConfiguration;
|
||||||
|
public interface IConfigurationRepository
|
||||||
|
{
|
||||||
|
IEnumerable<AppConfiguration> GetActiveConfigurations();
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
|
|
@ -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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,5 +0,0 @@
|
||||||
namespace Core.Configurations.SmartConfiguration;
|
|
||||||
public interface IConfigurationRepository
|
|
||||||
{
|
|
||||||
Task<IEnumerable<AppConfiguration>> GetActiveConfigurations();
|
|
||||||
}
|
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +1,23 @@
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net8.0</TargetFramework>
|
<TargetFramework>net8.0</TargetFramework>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Akka" Version="1.5.32" />
|
<PackageReference Include="Akka" Version="1.5.32" />
|
||||||
<PackageReference Include="Autofac" Version="8.1.1" />
|
<PackageReference Include="Autofac" Version="8.1.1" />
|
||||||
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="10.0.0" />
|
<PackageReference Include="Autofac.Extensions.DependencyInjection" Version="10.0.0" />
|
||||||
<PackageReference Include="Insight.Database" Version="8.0.1" />
|
<PackageReference Include="Insight.Database" Version="8.0.1" />
|
||||||
<PackageReference Include="Insight.Database.Providers.PostgreSQL" 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.ApplicationInsights" Version="2.22.0" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration" Version="9.0.0" />
|
<PackageReference Include="npgsql" Version="9.0.2" />
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.AzureAppConfiguration" Version="8.0.0" />
|
</ItemGroup>
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.EnvironmentVariables" Version="9.0.0" />
|
|
||||||
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="9.0.0" />
|
<ItemGroup>
|
||||||
<PackageReference Include="npgsql" Version="9.0.2" />
|
<Folder Include="Configurations\AzureAppConfigurationProvider\" />
|
||||||
</ItemGroup>
|
<Folder Include="Configurations\PostgresqlConfigurationBuilder\" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|
|
||||||
13
Database/Common/Validations.cs
Normal file
13
Database/Common/Validations.cs
Normal 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_]+$");
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
using Insight.Database;
|
using Insight.Database;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
|
||||||
namespace Database.AppConfigurationSystem;
|
namespace Database.ConfigurationManagementSystem;
|
||||||
|
|
||||||
public class ConfigurationDatabaseSetup
|
public class ConfigurationDatabaseSetup
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,25 +1,66 @@
|
||||||
using Insight.Database;
|
using Database.Common;
|
||||||
|
using Insight.Database;
|
||||||
using System;
|
using System;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Threading.Tasks;
|
using System.Threading.Tasks;
|
||||||
|
|
||||||
namespace Database.Tenants
|
namespace Database.IdentitySystem
|
||||||
{
|
{
|
||||||
public class DbSetup
|
public class DbSetup
|
||||||
{
|
{
|
||||||
private readonly IDbConnection _db;
|
readonly IDbConnection _db;
|
||||||
|
IDbTransaction _transaction = null;
|
||||||
|
string _schema;
|
||||||
|
|
||||||
public DbSetup(IDbConnection db)
|
public DbSetup(IDbConnection db)
|
||||||
{
|
{
|
||||||
_db = db ?? throw new ArgumentNullException(nameof(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>
|
/// <summary>
|
||||||
/// Creates the users table in the ptmain schema.
|
/// Creates the users table in the ptmain schema.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void CreateUsersTable()
|
public async Task CreateUsersTable()
|
||||||
{
|
{
|
||||||
ExecuteInTransaction(@"
|
var sql = @"
|
||||||
CREATE TABLE IF NOT EXISTS ptmain.users (
|
CREATE TABLE IF NOT EXISTS ptmain.users (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
email VARCHAR(256) NOT NULL UNIQUE,
|
email VARCHAR(256) NOT NULL UNIQUE,
|
||||||
|
|
@ -32,85 +73,75 @@ namespace Database.Tenants
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
last_login_at TIMESTAMPTZ NULL
|
last_login_at TIMESTAMPTZ NULL
|
||||||
);");
|
);";
|
||||||
|
|
||||||
|
await ExecuteSqlAsync(sql).ConfigureAwait(false);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates the tenants table in the ptmain schema.
|
/// Creates the tenants table in the ptmain schema.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void CreateTenantsTable()
|
public async Task CreateTenantsTable()
|
||||||
{
|
{
|
||||||
ExecuteInTransaction(@"
|
var sql = @"
|
||||||
CREATE TABLE IF NOT EXISTS ptmain.tenants (
|
CREATE TABLE IF NOT EXISTS ptmain.tenants (
|
||||||
id SERIAL PRIMARY KEY,
|
id SERIAL PRIMARY KEY,
|
||||||
connection_string VARCHAR(500) NOT NULL,
|
connection_string VARCHAR(500) NOT NULL,
|
||||||
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
is_active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
created_by INTEGER NOT NULL REFERENCES ptmain.users(id),
|
created_by INTEGER NOT NULL REFERENCES ptmain.users(id),
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP
|
||||||
);");
|
);";
|
||||||
|
|
||||||
|
await ExecuteSqlAsync(sql).ConfigureAwait(false);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Creates the user_tenants table in the ptmain schema.
|
/// Creates the user_tenants table in the ptmain schema.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void CreateUserTenantsTable()
|
public async Task CreateUserTenantsTable()
|
||||||
{
|
{
|
||||||
ExecuteInTransaction(@"
|
var sql = @"
|
||||||
CREATE TABLE IF NOT EXISTS ptmain.user_tenants (
|
CREATE TABLE IF NOT EXISTS ptmain.user_tenants (
|
||||||
user_id INTEGER NOT NULL REFERENCES ptmain.users(id),
|
user_id INTEGER NOT NULL REFERENCES ptmain.users(id),
|
||||||
tenant_id INTEGER NOT NULL REFERENCES ptmain.tenants(id),
|
tenant_id INTEGER NOT NULL REFERENCES ptmain.tenants(id),
|
||||||
pin_code VARCHAR(10) NULL,
|
pin_code VARCHAR(10) NULL,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
created_at TIMESTAMPTZ NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||||
PRIMARY KEY (user_id, tenant_id)
|
PRIMARY KEY (user_id, tenant_id)
|
||||||
);");
|
);";
|
||||||
|
|
||||||
|
await ExecuteSqlAsync(sql).ConfigureAwait(false);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Sets up Row Level Security (RLS) for the tenants and user_tenants tables.
|
/// Sets up Row Level Security (RLS) for the tenants and user_tenants tables.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
public void SetupRLS()
|
public async Task SetupRLS()
|
||||||
{
|
{
|
||||||
ExecuteInTransaction(
|
var sql = new[]
|
||||||
"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
|
|
||||||
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
|
|
||||||
USING (user_id = current_setting('app.user_id', TRUE)::INTEGER);"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// <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);
|
"ALTER TABLE ptmain.tenants ENABLE ROW LEVEL SECURITY;",
|
||||||
}
|
"ALTER TABLE ptmain.user_tenants ENABLE ROW LEVEL SECURITY;",
|
||||||
transaction.Commit();
|
"DROP POLICY IF EXISTS tenant_access ON ptmain.tenants;",
|
||||||
}
|
@"CREATE POLICY tenant_access ON ptmain.tenants
|
||||||
catch (Exception ex)
|
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
|
||||||
|
USING (user_id = current_setting('app.user_id', TRUE)::INTEGER);"
|
||||||
|
};
|
||||||
|
|
||||||
|
foreach (var statement in sql)
|
||||||
{
|
{
|
||||||
transaction.Rollback();
|
await ExecuteSqlAsync(statement).ConfigureAwait(false);
|
||||||
throw new InvalidOperationException("Failed to execute SQL commands in transaction.", ex);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
using Insight.Database;
|
using Insight.Database;
|
||||||
using System.Data;
|
using System.Data;
|
||||||
|
|
||||||
namespace Database.Identity
|
namespace Database.IdentitySystem
|
||||||
{
|
{
|
||||||
public class UserService
|
public class UserService
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
using System.Data;
|
using System.Data;
|
||||||
using System.Text.RegularExpressions;
|
using System.Text.RegularExpressions;
|
||||||
|
using Database.Common;
|
||||||
using Insight.Database;
|
using Insight.Database;
|
||||||
|
|
||||||
namespace Database.Tenants
|
namespace Database.RolesPermissionSystem
|
||||||
{
|
{
|
||||||
public class Setup
|
public class Setup
|
||||||
{
|
{
|
||||||
|
|
@ -19,7 +20,7 @@ namespace Database.Tenants
|
||||||
/// <param name="schema">The schema name where the tables will be created.</param>
|
/// <param name="schema">The schema name where the tables will be created.</param>
|
||||||
public async Task CreateSystem(string schema)
|
public async Task CreateSystem(string schema)
|
||||||
{
|
{
|
||||||
if (!IsValidSchemaName(schema))
|
if (!Validations.IsValidSchemaName(schema))
|
||||||
throw new ArgumentException("Invalid schema name", nameof(schema));
|
throw new ArgumentException("Invalid schema name", nameof(schema));
|
||||||
|
|
||||||
using (var transaction = _db.BeginTransaction())
|
using (var transaction = _db.BeginTransaction())
|
||||||
|
|
@ -29,7 +30,7 @@ namespace Database.Tenants
|
||||||
await CreateRolesTable(schema, transaction).ConfigureAwait(false);
|
await CreateRolesTable(schema, transaction).ConfigureAwait(false);
|
||||||
await CreatePermissionsTable(schema, transaction).ConfigureAwait(false);
|
await CreatePermissionsTable(schema, transaction).ConfigureAwait(false);
|
||||||
await CreatePermissionTypesTable(schema, transaction).ConfigureAwait(false);
|
await CreatePermissionTypesTable(schema, transaction).ConfigureAwait(false);
|
||||||
await CreateRolePermissionsTable(schema, transaction).ConfigureAwait(false);
|
await CreateRolePermissionsTable(schema, transaction).ConfigureAwait(false);
|
||||||
|
|
||||||
transaction.Commit();
|
transaction.Commit();
|
||||||
}
|
}
|
||||||
|
|
@ -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)
|
private async Task ExecuteSqlAsync(string sql, IDbTransaction transaction)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -12,7 +12,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="appsettings.json">
|
<None Update="appconfiguration.json">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,7 @@
|
||||||
{
|
{
|
||||||
"AllowedHosts": "*",
|
"AllowedHosts": "*",
|
||||||
"ConnectionStrings": {
|
"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": {
|
"ApplicationInsights": {
|
||||||
"ConnectionString": "InstrumentationKey=6d2e76ee-5343-4691-a5e3-81add43cb584;IngestionEndpoint=https://northeurope-0.in.applicationinsights.azure.com/"
|
"ConnectionString": "InstrumentationKey=6d2e76ee-5343-4691-a5e3-81add43cb584;IngestionEndpoint=https://northeurope-0.in.applicationinsights.azure.com/"
|
||||||
|
|
@ -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"}}}
|
||||||
119
Tests/ConfigurationTests/JsonConfigurationProviderTests.cs
Normal file
119
Tests/ConfigurationTests/JsonConfigurationProviderTests.cs
Normal 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; }
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -1,139 +1,78 @@
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Moq;
|
||||||
using System;
|
|
||||||
using System.Collections.Generic;
|
|
||||||
using System.Linq;
|
|
||||||
using System.Text;
|
|
||||||
using System.Threading.Tasks;
|
|
||||||
using Newtonsoft.Json.Linq;
|
using Newtonsoft.Json.Linq;
|
||||||
using System.Collections.Generic;
|
using Tests;
|
||||||
using System.Linq;
|
using Core.Configurations.SmartConfig;
|
||||||
namespace Tests.ConfigurationTests
|
using Core.Configurations.Common;
|
||||||
|
|
||||||
|
namespace Tests.ConfigurationTests;
|
||||||
|
|
||||||
|
[TestClass]
|
||||||
|
public class ConfigurationTests : TestFixture
|
||||||
{
|
{
|
||||||
[TestClass]
|
[TestInitialize]
|
||||||
public class KeyValueJsonHandlingTests : TestFixture
|
public void Init()
|
||||||
{
|
{
|
||||||
|
}
|
||||||
[TestMethod]
|
|
||||||
public void FunMixedTypesTest()
|
|
||||||
{
|
|
||||||
var pairs = new List<KeyValuePair<string, object>>
|
|
||||||
{
|
|
||||||
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
|
|
||||||
}
|
|
||||||
}"))
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
[TestMethod]
|
||||||
|
public void ConfigurationSettingsTest()
|
||||||
|
{
|
||||||
|
var pairs = new List<KeyValuePair<string, JToken>>
|
||||||
|
{
|
||||||
|
new("Debug", true),
|
||||||
|
// Database konfiguration
|
||||||
|
new("Database:ConnectionString", "Server=db.example.com;Port=5432"),
|
||||||
|
new("Database:Timeout", 30),
|
||||||
|
new("Database:UseSSL", true),
|
||||||
|
|
||||||
var result = KeyValueToJson.Convert(pairs);
|
// Logging konfiguration med JObject
|
||||||
|
new("Logging:FileOptions", JObject.Parse(@"{
|
||||||
|
'Path': '/var/logs/app.log',
|
||||||
|
'MaxSizeMB': 100,
|
||||||
|
'RetentionDays': 7
|
||||||
|
}")),
|
||||||
|
|
||||||
var expected = JObject.Parse(@"{
|
// Feature flags med kompleks konfiguration
|
||||||
'developer': {
|
new("Features:Experimental", JObject.Parse(@"{
|
||||||
'coffeeLevel': 9001.5,
|
'Enabled': true,
|
||||||
'bugsFixed': 42,
|
'RolloutPercentage': 25,
|
||||||
'awake': false
|
'AllowedUserGroups': ['beta']
|
||||||
},
|
}")),
|
||||||
'compiler': {
|
|
||||||
'errors': { 'semicolon': ['Missing ;', 'Found ; but why?'] }
|
// API endpoints med array
|
||||||
},
|
new("API:Endpoints", "/api/users"),
|
||||||
'computer': {
|
new("API:Endpoints", "/api/products")
|
||||||
'ram': { 'status': 'Out of memory' },
|
};
|
||||||
'cpu': { 'temperature': 99.9 }
|
|
||||||
},
|
var result = KeyValueToJson.Convert(pairs);
|
||||||
'friday': {
|
|
||||||
'beer': { 'count': 6 },
|
var expected = JObject.Parse(@"{
|
||||||
'code': { 'working': true },
|
'Debug' : true,
|
||||||
'bugs': { 'count': 2147483647 }
|
'Database': {
|
||||||
|
'ConnectionString': 'Server=db.example.com;Port=5432',
|
||||||
|
'Timeout': 30,
|
||||||
|
'UseSSL': true
|
||||||
|
},
|
||||||
|
'Logging': {
|
||||||
|
'FileOptions': {
|
||||||
|
'Path': '/var/logs/app.log',
|
||||||
|
'MaxSizeMB': 100,
|
||||||
|
'RetentionDays': 7
|
||||||
}
|
}
|
||||||
}");
|
},
|
||||||
|
'Features': {
|
||||||
|
'Experimental': {
|
||||||
|
'Enabled': true,
|
||||||
|
'RolloutPercentage': 25,
|
||||||
|
'AllowedUserGroups': ['beta']
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'API': {
|
||||||
|
'Endpoints': ['/api/users', '/api/products']
|
||||||
|
}
|
||||||
|
}");
|
||||||
|
|
||||||
Assert.IsTrue(JToken.DeepEquals(expected, result));
|
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
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
102
Tests/ConfigurationTests/SmartConfigProviderTests.cs
Normal file
102
Tests/ConfigurationTests/SmartConfigProviderTests.cs
Normal 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
50
Tests/PostgresTests.cs
Normal 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";
|
||||||
|
// });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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; }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,97 +1,84 @@
|
||||||
using System;
|
using System;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using Autofac;
|
using Autofac;
|
||||||
|
using Core.Configurations;
|
||||||
using Core.ModuleRegistry;
|
using Core.ModuleRegistry;
|
||||||
using Microsoft.ApplicationInsights;
|
using Microsoft.ApplicationInsights;
|
||||||
using Microsoft.Extensions.Configuration;
|
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.VisualStudio.TestTools.UnitTesting;
|
using Core.Configurations.JsonConfigProvider;
|
||||||
|
|
||||||
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; }
|
||||||
|
|
||||||
|
public virtual IConfigurationRoot Configuration()
|
||||||
|
{
|
||||||
|
var configuration = new ConfigurationBuilder()
|
||||||
|
.AddJsonFile("appconfiguration.dev.json")
|
||||||
|
.Build();
|
||||||
|
|
||||||
|
return configuration;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// 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
|
||||||
|
/// </summary>
|
||||||
|
[TestInitialize]
|
||||||
|
public void Setup()
|
||||||
|
{
|
||||||
|
CreateContainerBuilder();
|
||||||
|
Container = ContainerBuilder.Build();
|
||||||
|
Insight.Database.Providers.PostgreSQL.PostgreSQLInsightDbProvider.RegisterProvider();
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
[AssemblyInitialize]
|
protected virtual void CreateContainerBuilder()
|
||||||
public static void AssemblySetup(TestContext tc)
|
{
|
||||||
{
|
IConfigurationRoot configuration = Configuration();
|
||||||
Trace.Listeners.Add(new TextWriterTraceListener(Console.Out));
|
|
||||||
|
|
||||||
var envConfiguration = new ConfigurationBuilder()
|
var builder = new ContainerBuilder();
|
||||||
.AddEnvironmentVariables()
|
builder.RegisterInstance(new LoggerFactory())
|
||||||
.Build();
|
.As<ILoggerFactory>();
|
||||||
|
|
||||||
}
|
builder.RegisterGeneric(typeof(Logger<>))
|
||||||
|
.As(typeof(ILogger<>))
|
||||||
|
.SingleInstance();
|
||||||
|
|
||||||
public virtual IConfigurationRoot Configuration()
|
builder.RegisterModule(new Core.ModuleRegistry.DbPostgreSqlModule
|
||||||
{
|
{
|
||||||
|
ConnectionString = configuration.GetConnectionString("DefaultConnection")
|
||||||
|
});
|
||||||
|
|
||||||
IConfigurationBuilder configBuilder = Core.Configurations.AzureConfigurationManager.AppConfigBuilder("appsettings.dev.json");
|
builder.RegisterModule(new Core.ModuleRegistry.TelemetryModule
|
||||||
IConfigurationRoot configuration = configBuilder.Build();
|
{
|
||||||
|
TelemetryConfig = configuration.GetSection("ApplicationInsights").ToObject<Core.ModuleRegistry.TelemetryConfig>()
|
||||||
return configuration;
|
});
|
||||||
}
|
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// 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
|
|
||||||
/// </summary>
|
|
||||||
[TestInitialize]
|
|
||||||
public void Setup()
|
|
||||||
{
|
|
||||||
CreateContainerBuilder();
|
|
||||||
Container = ContainerBuilder.Build();
|
|
||||||
Insight.Database.Providers.PostgreSQL.PostgreSQLInsightDbProvider.RegisterProvider();
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
protected virtual void CreateContainerBuilder()
|
|
||||||
{
|
|
||||||
IConfigurationRoot configuration = Configuration();
|
|
||||||
|
|
||||||
var builder = new ContainerBuilder();
|
|
||||||
builder.RegisterInstance(new LoggerFactory())
|
|
||||||
.As<ILoggerFactory>();
|
|
||||||
|
|
||||||
builder.RegisterGeneric(typeof(Logger<>))
|
|
||||||
.As(typeof(ILogger<>))
|
|
||||||
.SingleInstance();
|
|
||||||
|
|
||||||
builder.RegisterModule(new Core.ModuleRegistry.DbPostgreSqlModule
|
|
||||||
{
|
|
||||||
ConnectionString = configuration.GetConnectionString("ptdb")
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.RegisterModule(new Core.ModuleRegistry.TelemetryModule
|
|
||||||
{
|
|
||||||
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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -10,6 +10,7 @@
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="coverlet.collector" Version="6.0.0" />
|
<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="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||||
<PackageReference Include="moq" Version="4.20.72" />
|
<PackageReference Include="moq" Version="4.20.72" />
|
||||||
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
|
<PackageReference Include="MSTest.TestAdapter" Version="3.1.1" />
|
||||||
|
|
@ -26,7 +27,7 @@
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<None Update="appsettings.dev.json">
|
<None Update="appconfiguration.dev.json">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
</None>
|
</None>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
|
||||||
|
|
@ -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";
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
32
Tests/appconfiguration.dev.json
Normal file
32
Tests/appconfiguration.dev.json
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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/"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue