diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Abstractions/Allegro.Extensions.Configuration.Abstractions.csproj b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Abstractions/Allegro.Extensions.Configuration.Abstractions.csproj
new file mode 100644
index 0000000..2e556e3
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Abstractions/Allegro.Extensions.Configuration.Abstractions.csproj
@@ -0,0 +1,13 @@
+
+
+
+ true
+ Allegro.Extensions.Configuration
+
+
+
+
+
+
+
+
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Abstractions/IConfigurationProviderWrapper.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Abstractions/IConfigurationProviderWrapper.cs
new file mode 100644
index 0000000..5dab578
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Abstractions/IConfigurationProviderWrapper.cs
@@ -0,0 +1,16 @@
+using Microsoft.Extensions.Configuration;
+
+namespace Allegro.Extensions.Configuration
+{
+ ///
+ /// wrapper, that simplifies traversing the providers' graph
+ /// by exposing the property.
+ ///
+ public interface IConfigurationProviderWrapper : IConfigurationProvider
+ {
+ ///
+ /// Inner being wrapped.
+ ///
+ IConfigurationProvider Inner { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Abstractions/ISensitiveConfigurationProviderWrapper.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Abstractions/ISensitiveConfigurationProviderWrapper.cs
new file mode 100644
index 0000000..3ecf2a8
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Abstractions/ISensitiveConfigurationProviderWrapper.cs
@@ -0,0 +1,12 @@
+using Microsoft.Extensions.Configuration;
+
+namespace Allegro.Extensions.Configuration
+{
+ ///
+ /// wrapper that marks the inner provider as sensitive
+ /// (meaning that it may contain secret values).
+ ///
+ public interface ISensitiveConfigurationProviderWrapper : IConfigurationProviderWrapper
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Abstractions/ITraversableChainedConfigurationProviderWrapper.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Abstractions/ITraversableChainedConfigurationProviderWrapper.cs
new file mode 100644
index 0000000..1363dad
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Abstractions/ITraversableChainedConfigurationProviderWrapper.cs
@@ -0,0 +1,16 @@
+using Microsoft.Extensions.Configuration;
+
+namespace Allegro.Extensions.Configuration
+{
+ ///
+ /// wrapper, that simplifies traversing the providers' graph
+ /// by exposing the property.
+ ///
+ public interface ITraversableChainedConfigurationProviderWrapper : IConfigurationProvider
+ {
+ ///
+ /// Inner being wrapped.
+ ///
+ IConfigurationRoot ConfigurationRoot { get; }
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Allegro.Extensions.Configuration.Api.csproj b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Allegro.Extensions.Configuration.Api.csproj
new file mode 100644
index 0000000..1ef66be
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Allegro.Extensions.Configuration.Api.csproj
@@ -0,0 +1,19 @@
+
+
+ false
+ enable
+ $(NoWarn);1591
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Configuration/SecretsConfiguration.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Configuration/SecretsConfiguration.cs
new file mode 100644
index 0000000..9cb9a2f
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Configuration/SecretsConfiguration.cs
@@ -0,0 +1,6 @@
+namespace Allegro.Extensions.Configuration.Api.Configuration;
+
+public class SecretsConfiguration
+{
+ public string SecretsPath { get; set; } = null!;
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Controllers/GlobalConfigurationController.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Controllers/GlobalConfigurationController.cs
new file mode 100644
index 0000000..1202b76
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Controllers/GlobalConfigurationController.cs
@@ -0,0 +1,34 @@
+using Allegro.Extensions.Configuration.Api.Services;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Allegro.Extensions.Configuration.Api.Controllers
+{
+ [ApiController]
+ [Route("global-configuration")]
+ public class GlobalConfigurationController : ControllerBase
+ {
+ private readonly IGlobalConfigurationProvider _globalConfigurationProvider;
+
+ public GlobalConfigurationController(IGlobalConfigurationProvider globalConfigurationProvider)
+ {
+ _globalConfigurationProvider = globalConfigurationProvider;
+ }
+
+ [HttpGet("context-groups")]
+ public IActionResult GetGlobalConfiguration([FromQuery] string? serviceName)
+ {
+ return Ok(_globalConfigurationProvider.GetGlobalConfiguration(serviceName));
+ }
+
+ [HttpGet("context-groups/{contextGroupName}/contexts/{contextName}")]
+ public IActionResult GetGlobalConfigurationContext(
+ [FromRoute] string contextGroupName,
+ [FromRoute] string contextName)
+ {
+ return File(
+ _globalConfigurationProvider.GetContext(contextGroupName, contextName),
+ "application/json",
+ contextName);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Controllers/SecretsController.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Controllers/SecretsController.cs
new file mode 100644
index 0000000..0af7225
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Controllers/SecretsController.cs
@@ -0,0 +1,30 @@
+using Allegro.Extensions.AspNetCore.Attributes;
+using Allegro.Extensions.Configuration.Api.Services;
+using Allegro.Extensions.Configuration.DataContracts;
+using Microsoft.AspNetCore.Authorization;
+using Microsoft.AspNetCore.Mvc;
+
+namespace Allegro.Extensions.Configuration.Api.Controllers
+{
+ [ApiController]
+ [Authorize]
+ [Route("secrets")]
+ [SkipOnProd]
+ public class SecretsController : ControllerBase
+ {
+ private readonly ISecretsProvider _secretsProvider;
+
+ public SecretsController(ISecretsProvider secretsProvider)
+ {
+ _secretsProvider = secretsProvider;
+ }
+
+ [HttpPost]
+ public IActionResult GetSecrets([FromBody] GetSecretsRequest request)
+ {
+ return Content(
+ _secretsProvider.GetSecretsAsJson(request.KeyVaultPrefixes),
+ "application/json");
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Exceptions/NotAllowedOutsideTestEnvException.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Exceptions/NotAllowedOutsideTestEnvException.cs
new file mode 100644
index 0000000..bda7cb2
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Exceptions/NotAllowedOutsideTestEnvException.cs
@@ -0,0 +1,8 @@
+namespace Allegro.Extensions.Configuration.Api.Exceptions;
+
+public class NotAllowedOutsideTestEnvException : Exception
+{
+ public NotAllowedOutsideTestEnvException() : base("Not allowed outside of a test environment")
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Properties/launchSettings.json b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Properties/launchSettings.json
new file mode 100644
index 0000000..3b41a3b
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Properties/launchSettings.json
@@ -0,0 +1,16 @@
+{
+ "$schema": "http://json.schemastore.org/launchsettings.json",
+ "profiles": {
+ "Vabank.Confeature.Service": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "launchUrl": "swagger",
+ "applicationUrl": "https://localhost:6001;http://localhost:6000",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development",
+ "GlobalConfiguration__ContextGroups__0__Name": "vabank-configuration",
+ "GlobalConfiguration__ContextGroups__0__Path": "/Users/szymon.adach/Repos/global-config"
+ }
+ }
+ }
+}
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Services/GlobalConfigurationProvider.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Services/GlobalConfigurationProvider.cs
new file mode 100644
index 0000000..730e5fa
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Services/GlobalConfigurationProvider.cs
@@ -0,0 +1,66 @@
+using Allegro.Extensions.Configuration.DataContracts;
+using Allegro.Extensions.Configuration.Extensions;
+using Allegro.Extensions.Configuration.GlobalConfiguration;
+using Microsoft.Extensions.Options;
+
+namespace Allegro.Extensions.Configuration.Api.Services;
+
+public interface IGlobalConfigurationProvider
+{
+ GetGlobalConfigurationResponse GetGlobalConfiguration(string? serviceName = null);
+ Stream GetContext(string contextGroupName, string contextName);
+}
+
+public class GlobalConfigurationProvider : IGlobalConfigurationProvider
+{
+ private readonly ContextGroupsConfiguration _configuration;
+
+ public GlobalConfigurationProvider(IOptions configuration)
+ {
+ _configuration = configuration.Value;
+ }
+
+ public GetGlobalConfigurationResponse GetGlobalConfiguration(string? serviceName = null)
+ {
+#pragma warning disable CSE001
+ var result = new GetGlobalConfigurationResponse();
+#pragma warning restore CSE001
+
+ foreach (var contextGroupConfiguration in _configuration.ContextGroups)
+ {
+ var contexts = GetContextsList(contextGroupConfiguration, serviceName).OrderBy(c => c).ToList();
+ if (contexts.Count == 0)
+ {
+ continue;
+ }
+
+ result.ContextGroups.Add(
+ new ContextGroupModel { Name = contextGroupConfiguration.Name, Contexts = contexts });
+ }
+
+ return result;
+ }
+
+ public Stream GetContext(string contextGroupName, string contextName)
+ {
+ var contextGroup = _configuration.ContextGroups
+ .Single(x => x.Name.Equals(contextGroupName, StringComparison.OrdinalIgnoreCase));
+ return File.OpenRead(Path.Combine(contextGroup.Path, $"{contextName}.json"));
+ }
+
+ private static IEnumerable GetContextsList(
+ ContextGroupConfiguration contextGroupConfiguration,
+ string? serviceName)
+ {
+ foreach (var file in Directory.EnumerateFiles(contextGroupConfiguration.Path, "*.json"))
+ {
+ if (!string.IsNullOrEmpty(serviceName) &&
+ !ConfigurationContextExtensions.IsServiceListedForContext(File.OpenRead(file), serviceName))
+ {
+ continue;
+ }
+
+ yield return Path.GetFileNameWithoutExtension(file);
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Services/SecretsProvider.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Services/SecretsProvider.cs
new file mode 100644
index 0000000..894071e
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/Services/SecretsProvider.cs
@@ -0,0 +1,52 @@
+using Allegro.Extensions.Configuration.Api.Configuration;
+using Allegro.Extensions.Configuration.Api.Exceptions;
+using Allegro.Extensions.Configuration.Models;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.Options;
+using Newtonsoft.Json;
+
+namespace Allegro.Extensions.Configuration.Api.Services;
+
+public interface ISecretsProvider
+{
+ string GetSecretsAsJson(IEnumerable keyVaultPrefixes);
+}
+
+public class SecretsProvider : ISecretsProvider
+{
+ private readonly EnvironmentConfiguration _environmentConfiguration;
+ private readonly SecretsConfiguration _secretsConfiguration;
+
+ public SecretsProvider(
+ IOptions environmentConfiguration,
+ IOptions secretsConfiguration)
+ {
+ _environmentConfiguration = environmentConfiguration.Value;
+ _secretsConfiguration = secretsConfiguration.Value;
+ }
+
+ public string GetSecretsAsJson(IEnumerable keyVaultPrefixes)
+ {
+ if (!_environmentConfiguration.IsTestEnvironment)
+ {
+ throw new NotAllowedOutsideTestEnvException();
+ }
+
+ var secretsConfiguration =
+ new ConfigurationBuilder()
+ .AddKeyPerFile(_secretsConfiguration.SecretsPath)
+ .Build();
+
+ var prefixes = NormalizeKeyVaultPrefixes(keyVaultPrefixes);
+ return JsonConvert.SerializeObject(
+ secretsConfiguration
+ .AsEnumerable()
+ .Where(x => prefixes.Any(p => x.Key.StartsWith(p, StringComparison.InvariantCultureIgnoreCase)))
+ .ToDictionary(x => x.Key, x => x.Value));
+ }
+
+ private static string[] NormalizeKeyVaultPrefixes(IEnumerable prefixes)
+ => prefixes
+ .Select(p => p.Replace("--", ConfigurationPath.KeyDelimiter))
+ .ToArray();
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/StartupExtensions.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/StartupExtensions.cs
new file mode 100644
index 0000000..75821c6
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Api/StartupExtensions.cs
@@ -0,0 +1,24 @@
+using Allegro.Extensions.Configuration.Api.Services;
+using Allegro.Extensions.Configuration.Configuration;
+using Allegro.Extensions.Configuration.Extensions;
+using Microsoft.Extensions.DependencyInjection;
+
+// ReSharper disable ConvertClosureToMethodGroup
+
+namespace Allegro.Extensions.Configuration.Api;
+
+public static class StartupExtensions
+{
+ ///
+ /// Registers Confeature V2 dependencies for a Fallback Service.
+ ///
+ public static IServiceCollection AddConfeatureFallbackService(
+ this IServiceCollection services,
+ ConfeatureOptions confeatureOptions)
+ {
+ services.AddConfeature(confeatureOptions);
+ services.AddSingleton();
+ services.AddSingleton();
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Client/Allegro.Extensions.Configuration.Client.csproj b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Client/Allegro.Extensions.Configuration.Client.csproj
new file mode 100644
index 0000000..af5d443
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Client/Allegro.Extensions.Configuration.Client.csproj
@@ -0,0 +1,19 @@
+
+
+
+ true
+ $(NoWarn);1591
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Client/ConfeatureServiceClient.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Client/ConfeatureServiceClient.cs
new file mode 100644
index 0000000..0044968
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Client/ConfeatureServiceClient.cs
@@ -0,0 +1,67 @@
+using System.Net.Http;
+using Allegro.Extensions.Configuration.DataContracts;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Converters;
+using Newtonsoft.Json.Serialization;
+using RestEase;
+
+// ReSharper disable UnusedMember.Global
+
+namespace Allegro.Extensions.Configuration.Client;
+
+public interface IConfeatureServiceClientRest
+{
+ [Get("global-configuration/context-groups")]
+ Task GetGlobalConfiguration([Query] string serviceName);
+
+ [Get("global-configuration/context-groups/{contextGroupName}/contexts/{contextName}")]
+ Task GetGlobalConfigurationContext([Path] string contextGroupName, [Path] string contextName);
+}
+
+public interface IConfeatureServiceClient
+{
+ Task GetGlobalConfiguration(string serviceName);
+ Task GetGlobalConfigurationContext(string contextGroupName, string contextName);
+}
+
+public class ConfeatureServiceClient : IConfeatureServiceClient
+{
+ private readonly IConfeatureServiceClientRest _restClient;
+
+ private static JsonSerializerSettings SerializerSettings
+ {
+ get
+ {
+ var settings = new JsonSerializerSettings
+ {
+ NullValueHandling = NullValueHandling.Ignore,
+ ContractResolver = new DefaultContractResolver
+ {
+ NamingStrategy = new CamelCaseNamingStrategy()
+ }
+ };
+ settings.Converters.Add(new StringEnumConverter
+ {
+ NamingStrategy = new CamelCaseNamingStrategy()
+ });
+ return settings;
+ }
+ }
+
+ public ConfeatureServiceClient(HttpClient httpClient)
+ {
+ _restClient =
+ new RestClient(httpClient) { JsonSerializerSettings = SerializerSettings }
+ .For();
+ }
+
+ public Task GetGlobalConfiguration(string serviceName)
+ {
+ return _restClient.GetGlobalConfiguration(serviceName);
+ }
+
+ public Task GetGlobalConfigurationContext(string contextGroupName, string contextName)
+ {
+ return _restClient.GetGlobalConfigurationContext(contextGroupName, contextName);
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Client/StartupExtensions.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Client/StartupExtensions.cs
new file mode 100644
index 0000000..370c2cf
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Client/StartupExtensions.cs
@@ -0,0 +1,22 @@
+using System.Net.Http;
+using Microsoft.Extensions.DependencyInjection;
+
+// ReSharper disable UnusedMember.Global
+// ReSharper disable UnusedType.Global
+
+namespace Allegro.Extensions.Configuration.Client;
+
+public static class StartupExtensions
+{
+ public static IServiceCollection AddConfeatureServiceClient(
+ this IServiceCollection services,
+ Func httpClientFactory)
+ {
+ return services.AddSingleton(
+ sp =>
+ {
+ var httpClient = httpClientFactory(sp);
+ return new ConfeatureServiceClient(httpClient);
+ });
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.DataContracts/Allegro.Extensions.Configuration.DataContracts.csproj b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.DataContracts/Allegro.Extensions.Configuration.DataContracts.csproj
new file mode 100644
index 0000000..91b68ed
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.DataContracts/Allegro.Extensions.Configuration.DataContracts.csproj
@@ -0,0 +1,8 @@
+
+
+
+ true
+
+
+
+
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.DataContracts/GetGlobalConfigurationResponse.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.DataContracts/GetGlobalConfigurationResponse.cs
new file mode 100644
index 0000000..caf7ee6
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.DataContracts/GetGlobalConfigurationResponse.cs
@@ -0,0 +1,28 @@
+namespace Allegro.Extensions.Configuration.DataContracts;
+
+///
+/// Global configuration response
+///
+public class GetGlobalConfigurationResponse
+{
+ ///
+ /// List of context groups
+ ///
+ public List ContextGroups { get; init; } = new();
+}
+
+///
+/// Context group representation
+///
+public class ContextGroupModel
+{
+ ///
+ /// Name of the Context Group
+ ///
+ public string Name { get; init; } = null!;
+
+ ///
+ /// Contexts defined within the context group
+ ///
+ public List Contexts { get; init; } = new();
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.DataContracts/GetSecretsRequest.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.DataContracts/GetSecretsRequest.cs
new file mode 100644
index 0000000..6809495
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.DataContracts/GetSecretsRequest.cs
@@ -0,0 +1,12 @@
+namespace Allegro.Extensions.Configuration.DataContracts;
+
+///
+/// Get secrets request
+///
+public class GetSecretsRequest
+{
+ ///
+ /// List of key vault prefixes to include in response
+ ///
+ public List KeyVaultPrefixes { get; init; } = new();
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/Allegro.Extensions.Configuration.Demo.FallbackService.csproj b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/Allegro.Extensions.Configuration.Demo.FallbackService.csproj
new file mode 100644
index 0000000..09ba5b1
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/Allegro.Extensions.Configuration.Demo.FallbackService.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+ <_ContentIncludedByDefault Remove="global-config\TestGlobalConfig.json" />
+
+
+
+
+ Always
+
+
+
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/Program.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/Program.cs
new file mode 100644
index 0000000..fca52b3
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/Program.cs
@@ -0,0 +1,43 @@
+using Allegro.Extensions.Configuration.Api;
+using Allegro.Extensions.Configuration.Api.Services;
+using Allegro.Extensions.Configuration.Configuration;
+using Allegro.Extensions.Configuration.Extensions;
+
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+
+namespace Allegro.Extensions.Configuration.Demo.FallbackService;
+
+public class Program
+{
+ public static void Main(string[] args)
+ {
+ var builder = WebApplication.CreateBuilder(args);
+
+ // Basic Confeature settings
+ var confeatureOptions = new ConfeatureOptions
+ {
+ IsEnabled = true,
+ ServiceName = "demo",
+ AuthorizationPolicy = null,
+ };
+
+ // Add the global configuration source
+ builder.Configuration.AddGlobalConfiguration(
+ confeatureOptions,
+ builder.Environment);
+
+ // Register the Confeature
+ builder.Services.AddConfeatureFallbackService(confeatureOptions);
+
+ builder.Services.AddControllers()
+ .AddApplicationPart(typeof(ISecretsProvider).Assembly);
+ builder.Services.AddHttpClient();
+ builder.Services.AddEndpointsApiExplorer();
+
+ var app = builder.Build();
+
+ app.UseAuthorization();
+ app.MapControllers();
+ app.Run();
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/Properties/launchSettings.json b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/Properties/launchSettings.json
new file mode 100644
index 0000000..134e5f0
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "profiles": {
+ "Allegro.Extensions.Configuration.Demo.FallbackService": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5284",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/appsettings.Development.json b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/appsettings.Development.json
new file mode 100644
index 0000000..e86e05d
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/appsettings.Development.json
@@ -0,0 +1,10 @@
+{
+ "GlobalConfiguration": {
+ "ContextGroups": [
+ {
+ "Name": "global-config",
+ "Path": "global-config"
+ }
+ ]
+ }
+}
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/appsettings.json b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/appsettings.json
new file mode 100644
index 0000000..2c63c08
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/appsettings.json
@@ -0,0 +1,2 @@
+{
+}
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/global-config/TestGlobalConfig.json b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/global-config/TestGlobalConfig.json
new file mode 100644
index 0000000..e34f2ce
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo.FallbackService/global-config/TestGlobalConfig.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "Test": 5,
+ "Message": "Hello from global config"
+ },
+ "metadata": {
+ "services": [
+ "demo"
+ ]
+ }
+}
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Allegro.Extensions.Configuration.Demo.csproj b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Allegro.Extensions.Configuration.Demo.csproj
new file mode 100644
index 0000000..0fa851a
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Allegro.Extensions.Configuration.Demo.csproj
@@ -0,0 +1,22 @@
+
+
+
+ net6.0
+ enable
+ enable
+
+
+
+
+
+
+
+
+
+
+
+
+ Always
+
+
+
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Configuration/TestGlobalConfig.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Configuration/TestGlobalConfig.cs
new file mode 100644
index 0000000..02317bc
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Configuration/TestGlobalConfig.cs
@@ -0,0 +1,10 @@
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+namespace Allegro.Extensions.Configuration.Demo.Configuration;
+
+[GlobalConfigurationContext("TestGlobalConfig")]
+public class TestGlobalConfig : IGlobalConfigurationMarker
+{
+ public int Test { get; set; }
+
+ public string? Message { get; set; }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Configuration/TestLocalConfig.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Configuration/TestLocalConfig.cs
new file mode 100644
index 0000000..6b021c6
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Configuration/TestLocalConfig.cs
@@ -0,0 +1,11 @@
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+namespace Allegro.Extensions.Configuration.Demo.Configuration;
+
+public class TestLocalConfig : IConfigurationMarker
+{
+ public int Test { get; set; }
+
+ public string? Message { get; set; }
+
+ public string? Secret { get; set; }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Controllers/TestController.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Controllers/TestController.cs
new file mode 100644
index 0000000..3f1df90
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Controllers/TestController.cs
@@ -0,0 +1,35 @@
+using Allegro.Extensions.Configuration.Demo.Configuration;
+using Microsoft.AspNetCore.Mvc;
+using Microsoft.Extensions.Options;
+
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+
+namespace Allegro.Extensions.Configuration.Demo.Controllers;
+
+[ApiController]
+[Route("[controller]")]
+public class TestController : ControllerBase
+{
+ private readonly IOptions _testGlobalConfig;
+ private readonly IOptions _testLocalConfig;
+
+ public TestController(
+ IOptions testGlobalConfig,
+ IOptions testLocalConfig)
+ {
+ _testGlobalConfig = testGlobalConfig;
+ _testLocalConfig = testLocalConfig;
+ }
+
+ [HttpGet("global")]
+ public Task GetGlobal()
+ {
+ return Task.FromResult(_testGlobalConfig.Value);
+ }
+
+ [HttpGet("local")]
+ public Task GetLocal()
+ {
+ return Task.FromResult(_testLocalConfig.Value);
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Controllers/WeatherForecastController.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Controllers/WeatherForecastController.cs
new file mode 100644
index 0000000..1f70277
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Controllers/WeatherForecastController.cs
@@ -0,0 +1,29 @@
+using Microsoft.AspNetCore.Mvc;
+
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+
+namespace Allegro.Extensions.Configuration.Demo.Controllers;
+
+public record WeatherForecast(DateTime Date, int TemperatureC, string Summary);
+
+[ApiController]
+[Route("[controller]")]
+public class WeatherForecastController : ControllerBase
+{
+ private static readonly string[] Summaries = new[]
+ {
+ "Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
+ };
+
+ [HttpGet(Name = "GetWeatherForecast")]
+ public Task> Get()
+ {
+ return Task.FromResult>(
+ Enumerable.Range(1, 5).Select(
+ index => new WeatherForecast(
+ DateTime.Now.AddDays(index),
+ Random.Shared.Next(-20, 55),
+ Summaries[Random.Shared.Next(Summaries.Length)]))
+ .ToArray());
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Program.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Program.cs
new file mode 100644
index 0000000..4de5f79
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Program.cs
@@ -0,0 +1,50 @@
+using Allegro.Extensions.Configuration.Configuration;
+using Allegro.Extensions.Configuration.Demo.Configuration;
+using Allegro.Extensions.Configuration.Extensions;
+using Allegro.Extensions.Configuration.Wrappers;
+
+#pragma warning disable CS1591 // Missing XML comment for publicly visible type or member
+
+namespace Allegro.Extensions.Configuration.Demo;
+
+public class Program
+{
+ public static void Main(string[] args)
+ {
+ var builder = WebApplication.CreateBuilder(args);
+
+ // Basic Confeature settings
+ var confeatureOptions = new ConfeatureOptions
+ {
+ IsEnabled = true,
+ ServiceName = "demo",
+ AuthorizationPolicy = null,
+ };
+
+ // This is just an example of marking configuration source as sensitive.
+ // In real-world scenario this would be something like a K8S Secret mounted on volume.
+ // Values from this source will not be visible in /configuration endpoint response.
+ builder.Configuration.WrapSensitive().AddJsonFile("appsettings.Secret.json");
+
+ // Add the global configuration source
+ builder.Configuration.AddGlobalConfiguration(
+ confeatureOptions,
+ builder.Environment);
+
+ // Register the Confeature and all configuration classes (available later using IOptions<> pattern)
+ builder.Services
+ .AddConfeature(confeatureOptions)
+ .RegisterConfig(builder.Configuration, "TestConfig")
+ .RegisterGlobalConfig(builder.Configuration, confeatureOptions);
+
+ builder.Services.AddControllers();
+ builder.Services.AddHttpClient();
+ builder.Services.AddEndpointsApiExplorer();
+
+ var app = builder.Build();
+
+ app.UseAuthorization();
+ app.MapControllers();
+ app.Run();
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Properties/launchSettings.json b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Properties/launchSettings.json
new file mode 100644
index 0000000..fce5896
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/Properties/launchSettings.json
@@ -0,0 +1,13 @@
+{
+ "profiles": {
+ "Allegro.Extensions.Configuration.Demo": {
+ "commandName": "Project",
+ "dotnetRunMessages": true,
+ "launchBrowser": true,
+ "applicationUrl": "http://localhost:5283",
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ }
+ }
+ }
+}
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/appsettings.Development.json b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/appsettings.Development.json
new file mode 100644
index 0000000..e86e05d
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/appsettings.Development.json
@@ -0,0 +1,10 @@
+{
+ "GlobalConfiguration": {
+ "ContextGroups": [
+ {
+ "Name": "global-config",
+ "Path": "global-config"
+ }
+ ]
+ }
+}
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/appsettings.Secret.json b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/appsettings.Secret.json
new file mode 100644
index 0000000..cda6286
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/appsettings.Secret.json
@@ -0,0 +1,5 @@
+{
+ "TestConfig": {
+ "Secret": "P@ssw0rd"
+ }
+}
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/appsettings.json b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/appsettings.json
new file mode 100644
index 0000000..8bbef1a
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/appsettings.json
@@ -0,0 +1,6 @@
+{
+ "TestConfig": {
+ "Test": 10,
+ "Message": "Hello from local config!"
+ }
+}
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/global-config/TestGlobalConfig.json b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/global-config/TestGlobalConfig.json
new file mode 100644
index 0000000..e34f2ce
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Demo/global-config/TestGlobalConfig.json
@@ -0,0 +1,11 @@
+{
+ "config": {
+ "Test": 5,
+ "Message": "Hello from global config"
+ },
+ "metadata": {
+ "services": [
+ "demo"
+ ]
+ }
+}
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.FluentValidation/Allegro.Extensions.Configuration.FluentValidation.csproj b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.FluentValidation/Allegro.Extensions.Configuration.FluentValidation.csproj
new file mode 100644
index 0000000..ac7c1fa
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.FluentValidation/Allegro.Extensions.Configuration.FluentValidation.csproj
@@ -0,0 +1,24 @@
+
+
+
+ Confeature v2 fluent validations library
+ enable
+
+ $(NoWarn);1591
+ true
+
+ $(AllowedOutputExtensionsInPackageBuildOutputFolder);.pdb
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.FluentValidation/StartupExtensions.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.FluentValidation/StartupExtensions.cs
new file mode 100644
index 0000000..cbea1c4
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.FluentValidation/StartupExtensions.cs
@@ -0,0 +1,43 @@
+using Allegro.Extensions.Configuration.Services;
+using Allegro.Extensions.Validators;
+using FluentValidation;
+using Microsoft.Extensions.Configuration;
+using Microsoft.Extensions.DependencyInjection;
+
+namespace Allegro.Extensions.Configuration.FluentValidation;
+
+public static class StartupExtensions
+{
+ ///
+ /// Registers the configuration using the configuration section name
+ /// passed as the sectionName. If sectionName is null, configuration root will be used.
+ /// This extension also enables fluent validation for the configuration.
+ ///
+ /// Configuration class. It should implement
+ /// the interface.
+ /// Validator class
+ public static IServiceCollection RegisterConfig(
+ this IServiceCollection services,
+ IConfiguration configuration,
+ string? sectionName = null)
+ where TOptions : class, IConfigurationMarker
+ where TValidator : class, IValidator
+ {
+ var configurationSection =
+ !string.IsNullOrWhiteSpace(sectionName)
+ ? configuration.GetRequiredSection(sectionName)
+ : configuration as IConfigurationSection;
+
+ services.AddScoped, TValidator>();
+
+ services.Configure(cr => cr.RegisterOptions(configurationSection?.Path));
+ services
+ .AddOptions()
+ .ValidateDataAnnotations()
+ .ValidateFluentValidation()
+ .ValidateOnStart()
+ .Bind(configurationSection ?? configuration, c => c.BindNonPublicProperties = true);
+
+ return services;
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/Allegro.Extensions.Configuration.Tests.Integration.csproj b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/Allegro.Extensions.Configuration.Tests.Integration.csproj
new file mode 100644
index 0000000..747847d
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/Allegro.Extensions.Configuration.Tests.Integration.csproj
@@ -0,0 +1,18 @@
+
+
+ net6.0
+ false
+ $(NoWarn);1591
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/DemoSmokeTestsFixture.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/DemoSmokeTestsFixture.cs
new file mode 100644
index 0000000..e3c7dad
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/DemoSmokeTestsFixture.cs
@@ -0,0 +1,16 @@
+using Allegro.Extensions.Configuration.Demo;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Vabank.Confeature.Tests;
+
+namespace Vabank.Confeature.Integration.Tests;
+
+public class DemoSmokeTestsFixture : ConfeatureSmokeTest
+{
+ public DemoSmokeTestsFixture(WebApplicationFactory factory)
+ : base(
+ factory.WithWebHostBuilder(
+ cfg => cfg.ConfigureServices(sc => sc.Configure(o => o.SomeValue = 1))))
+ {
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/Helpers/CustomWebApplicationFactory.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/Helpers/CustomWebApplicationFactory.cs
new file mode 100644
index 0000000..9cbf6dd
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/Helpers/CustomWebApplicationFactory.cs
@@ -0,0 +1,35 @@
+using Allegro.Extensions.Configuration.Demo;
+using Microsoft.AspNetCore.Hosting;
+using Microsoft.AspNetCore.Mvc.Testing;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Hosting;
+using Vabank.Confeature.Integration.Tests;
+
+namespace Allegro.Extensions.Configuration.Tests.Integration.Helpers;
+
+public class CustomWebApplicationFactory : WebApplicationFactory
+{
+ public string AspNetEnvironment { get; set; } = Environments.Development;
+
+ ///
+ /// If error on dispose please relate to https://github.com/djluck/prometheus-net.DotNetRuntime/issues/65
+ /// and catch it in Dispose as a workaround
+ ///
+ protected override IHostBuilder CreateHostBuilder()
+ {
+ Environment.SetEnvironmentVariable("IntegrationTesting", "true");
+ // we want to control the environment with UseEnvironment instead of env var
+ Environment.SetEnvironmentVariable("ASPNETCORE_ENVIRONMENT", "");
+
+ return base.CreateHostBuilder()
+ .ConfigureServices(
+ services =>
+ {
+ services.AddSingleton();
+ })
+ .ConfigureWebHostDefaults(
+ webBuilder =>
+ webBuilder.UseEnvironment(AspNetEnvironment)
+ );
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/Helpers/CustomWebApplicationFactoryCollection.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/Helpers/CustomWebApplicationFactoryCollection.cs
new file mode 100644
index 0000000..8724507
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/Helpers/CustomWebApplicationFactoryCollection.cs
@@ -0,0 +1,16 @@
+using Xunit;
+
+namespace Allegro.Extensions.Configuration.Tests.Integration.Helpers;
+
+///
+/// This class has no code, and is never instantiated. Its purpose is simply
+/// to be the place to apply [CollectionDefinition] and all the
+/// ICollectionFixture interfaces.
+///
+[CollectionDefinition(Name)]
+#pragma warning disable CA1711
+public class CustomWebApplicationFactoryCollection : ICollectionFixture
+#pragma warning restore CA1711
+{
+ public const string Name = "Options Registration Validator collection";
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/OptionsRegistrationValidatorTests.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/OptionsRegistrationValidatorTests.cs
new file mode 100644
index 0000000..f51fd24
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/OptionsRegistrationValidatorTests.cs
@@ -0,0 +1,83 @@
+using Allegro.Extensions.Configuration;
+using Allegro.Extensions.Configuration.Tests.Integration.Helpers;
+using FluentAssertions;
+using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Xunit;
+
+namespace Vabank.Confeature.Integration.Tests;
+
+[Collection(CustomWebApplicationFactoryCollection.Name)]
+public class OptionsRegistrationValidatorTests
+{
+ private const string ShouldSkipTests = "Integration tests skipped as they're not working on the build agents";
+
+ private readonly CustomWebApplicationFactory _factory;
+
+ // Set this to null in order to enable integration tests
+
+ public OptionsRegistrationValidatorTests(CustomWebApplicationFactory factory)
+ {
+ _factory = factory;
+ }
+
+ [Fact(Skip = ShouldSkipTests)]
+ public void ShouldThrowAnException_WhenConfigurationDtoIsNotRegistered()
+ {
+ // Arrange
+ Environment.SetEnvironmentVariable("KUBERNETES_SERVICE_HOST", "");
+ var func = () => _factory.CreateClient();
+
+ // Act & Assert
+ func
+ .Should()
+ .Throw(because: "Configuration DTO is not registered properly")
+ .WithMessage($"*{nameof(TestService)}*{nameof(TestConfig)}*DisableOptionsRegistrationValidation*");
+ }
+
+ [Fact(Skip = ShouldSkipTests)]
+ public void ShouldSkipValidation_WhenEnvironmentVariableIsSet()
+ {
+ // Arrange
+ Environment.SetEnvironmentVariable("KUBERNETES_SERVICE_HOST", "");
+ Environment.SetEnvironmentVariable("DisableOptionsRegistrationValidation", bool.TrueString);
+ var func = () => _factory.CreateClient();
+
+ // Act & Assert
+ func
+ .Should()
+ .NotThrow(because: "Validation is turned off using the feature flag");
+ }
+
+ [Fact(Skip = ShouldSkipTests)]
+ public void ShouldSkipGenericTypes()
+ {
+ // Arrange
+ // We're adding the TestConfig options registration to prevent them from causing an exception
+ // We only want to check the generic IOptions validation behaviour
+ Environment.SetEnvironmentVariable("KUBERNETES_SERVICE_HOST", "");
+ var func = () => _factory
+ .WithWebHostBuilder(cfg => cfg.ConfigureServices(sc => sc.Configure(o => o.SomeValue = 1)))
+ .CreateClient();
+
+ // Act & Assert
+ func
+ .Should()
+ .NotThrow(because: "Generic types should be skipped");
+ }
+}
+
+public class TestService
+{
+ public TestService(IOptions testConfig) { }
+}
+
+public class TestConfig : IConfigurationMarker
+{
+ public int SomeValue { get; set; }
+}
+
+public class GenericTestService where T : TestConfig
+{
+ public GenericTestService(IOptions testConfig) { }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/Properties/launchSettings.json b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/Properties/launchSettings.json
new file mode 100644
index 0000000..5a98a0a
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/Properties/launchSettings.json
@@ -0,0 +1,12 @@
+{
+ "profiles": {
+ "Vabank.Confeature.Integration.Tests": {
+ "commandName": "Project",
+ "launchBrowser": true,
+ "environmentVariables": {
+ "ASPNETCORE_ENVIRONMENT": "Development"
+ },
+ "applicationUrl": "https://localhost:53245;http://localhost:53246"
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/paket.references b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/paket.references
new file mode 100644
index 0000000..351e661
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Integration/paket.references
@@ -0,0 +1,7 @@
+FluentAssertions
+Microsoft.AspNetCore.Mvc.Testing
+Microsoft.NET.Test.Sdk
+Moq
+Serilog.Sinks.XUnit
+xunit
+xunit.runner.visualstudio
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Unit/Allegro.Extensions.Configuration.Tests.Unit.csproj b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Unit/Allegro.Extensions.Configuration.Tests.Unit.csproj
new file mode 100644
index 0000000..3e62c2f
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Unit/Allegro.Extensions.Configuration.Tests.Unit.csproj
@@ -0,0 +1,40 @@
+
+
+ net6.0
+ false
+ $(NoWarn);1591
+
+
+
+ PreserveNewest
+
+
+
+
+ true
+ PreserveNewest
+ PreserveNewest
+
+
+ true
+ Always
+ PreserveNewest
+
+
+
+ PreserveNewest
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Unit/ConfigurationHelperTests.cs b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Unit/ConfigurationHelperTests.cs
new file mode 100644
index 0000000..49aa94d
--- /dev/null
+++ b/src/Allegro.Extensions.Configuration/Allegro.Extensions.Configuration.Tests.Unit/ConfigurationHelperTests.cs
@@ -0,0 +1,174 @@
+// using Allegro.Extensions.Configuration.Extensions;
+// using Allegro.Extensions.Configuration.Models;
+// using FinAi.Platform.Demo.Configuration;
+//
+// namespace Vabank.Confeature.Tests.Unit;
+//
+// using System;
+// using System.Linq;
+// using DeepEqual.Syntax;
+// using FinAi.Platform;
+// using FluentAssertions;
+// using Microsoft.Extensions.Configuration;
+// using Microsoft.Extensions.Configuration.Json;
+// using Microsoft.Extensions.DependencyInjection;
+// using Vabank.Confeature.Extensions;
+// using Vabank.Confeature.Models;
+// using Vabank.Confeature.Services;
+// using Xunit;
+//
+// public class ConfigurationHelperTests
+// {
+// [Fact]
+// public void TestJsonMetadata()
+// {
+// // arrange
+// var services = ConfigureServices(b => b
+// .AddJsonFile("appsettings.json")
+// .AddJsonFile("appsettings.Development.json"));
+//
+// // act
+// var response = ConfigurationHelper.GetConfiguration(services);
+//
+// // assert
+// response.Providers.Should().HaveCount(2);
+//
+// var appsettings =
+// response.Providers.Single(x => x.Value.DisplayName == "appsettings.json");
+// var appsettingsDevelopment =
+// response.Providers.Single(x => x.Value.DisplayName == "appsettings.Development.json");
+//
+// appsettings.Value.ShouldDeepEqual(new ConfigurationProviderMetadata(
+// "appsettings.json",
+// nameof(JsonConfigurationProvider),
+// "appsettings.json",
+// false,
+// true));
+// appsettingsDevelopment.Value.ShouldDeepEqual(new ConfigurationProviderMetadata(
+// "appsettings.Development.json",
+// nameof(JsonConfigurationProvider),
+// "appsettings.Development.json",
+// false,
+// true));
+// }
+//
+// [Fact]
+// public void TestSecretProvider()
+// {
+// // arrange
+// var services = ConfigureServices(b => b
+// .AddJsonFile("appsettings.json")
+// .WrapSensitive()
+// .AddJsonFile("appsettings.Development.json"));
+//
+// // act
+// var response = ConfigurationHelper.GetConfiguration(services);
+//
+// // assert
+// response.Providers.Should().HaveCount(2);
+//
+// var appsettings =
+// response.Providers.Single(x => x.Value.DisplayName == "appsettings.json");
+// var appsettingsDevelopment =
+// response.Providers.Single(x => x.Value.DisplayName == "appsettings.Development.json");
+//
+// appsettings.Value.IsSecret.Should().Be(false);
+// appsettingsDevelopment.Value.IsSecret.Should().Be(true);
+//
+// var appsettingsValues = response.Configuration
+// .SelectMany(x => x.Value)
+// .Where(x => x.ProviderId == appsettings.Key)
+// .ToList();
+// var appsettingsDevelopmentValues = response.Configuration
+// .SelectMany(x => x.Value)
+// .Where(x => x.ProviderId == appsettingsDevelopment.Key)
+// .ToList();
+//
+// appsettingsValues.Should().NotBeEmpty();
+// appsettingsValues.ForEach(x => x.Value.Should().NotBeNull());
+//
+// appsettingsDevelopmentValues.Should().NotBeEmpty();
+// appsettingsDevelopmentValues.ForEach(x => x.Value.Should().BeNull());
+// }
+//
+// [Fact]
+// public void TestScheduledValueCalculation()
+// {
+// // arrange
+// var services = ConfigureServices(b => b
+// .AddJsonFile("appsettings.json"));
+//
+// // act
+// var response = ConfigurationHelper.GetConfiguration(services);
+// var scheduledValue = response.Configuration
+// .FirstOrDefault(
+// kv => kv.Key == $"{nameof(DemoConfig)}:" +
+// $"{nameof(DemoConfig.ScheduledIntegerFlag)}:" +
+// $"{nameof(ScheduledConfigurationWrapper