tools/code/common/Configuration.cs (180 lines of code) (raw):
using LanguageExt;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions;
using Microsoft.Extensions.Hosting;
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text.Json;
using System.Text.Json.Nodes;
using Yaml2JsonNode;
using YamlDotNet.RepresentationModel;
namespace common;
public record ConfigurationJson
{
private static readonly JsonNodeOptions nodeOptions = new() { PropertyNameCaseInsensitive = true };
public required JsonObject Value { get; init; }
public static ConfigurationJson From(IConfiguration configuration) =>
new()
{
Value = SerializeConfiguration(configuration) is JsonObject configurationJsonObject
? configurationJsonObject
: new JsonObject(nodeOptions)
};
private static JsonNode? SerializeConfiguration(IConfiguration configuration)
{
var jsonObject = new JsonObject(nodeOptions);
foreach (var child in configuration.GetChildren())
{
if (child.Path.EndsWith(":0", StringComparison.Ordinal))
{
var jsonArray = new JsonArray(nodeOptions);
foreach (var arrayChild in configuration.GetChildren())
{
jsonArray.Add(SerializeConfiguration(arrayChild));
}
return jsonArray;
}
else
{
jsonObject.Add(child.Key, SerializeConfiguration(child));
}
}
if (jsonObject.Count == 0 && configuration is IConfigurationSection configurationSection)
{
string? sectionValue = configurationSection.Value;
if (bool.TryParse(sectionValue, out var boolValue))
{
return JsonValue.Create(boolValue);
}
else if (decimal.TryParse(sectionValue, out var decimalValue))
{
return JsonValue.Create(decimalValue);
}
else if (long.TryParse(sectionValue, out var longValue))
{
return JsonValue.Create(longValue);
}
else
{
return JsonValue.Create(sectionValue);
}
}
else
{
return jsonObject;
}
}
public static ConfigurationJson FromYaml(TextReader textReader) =>
new()
{
Value = YamlToJson(textReader)
};
private static JsonObject YamlToJson(TextReader reader)
{
var yamlStream = new YamlStream();
yamlStream.Load(reader);
return yamlStream.Documents switch
{
[] => new JsonObject(nodeOptions),
[var document] => document.ToJsonNode()?.AsObject() ?? throw new JsonException("Failed to convert YAML to JSON."),
_ => throw new JsonException("More than one YAML document was found.")
};
}
public ConfigurationJson MergeWith(ConfigurationJson other) =>
new()
{
Value = OverrideWith(Value, other.Value)
};
private static JsonObject OverrideWith(JsonObject current, JsonObject other)
{
var merged = new JsonObject(nodeOptions);
foreach (var property in current)
{
string propertyName = property.Key;
var currentPropertyValue = property.Value;
if (other.TryGetPropertyValue(propertyName, out var otherPropertyValue))
{
if (currentPropertyValue is JsonObject currentObject && otherPropertyValue is JsonObject otherObject)
{
merged[propertyName] = OverrideWith(currentObject, otherObject);
}
else
{
merged[propertyName] = otherPropertyValue?.DeepClone();
}
}
else
{
merged[propertyName] = currentPropertyValue?.DeepClone();
}
}
foreach (var property in other)
{
string propertyName = property.Key;
if (current.ContainsKey(propertyName) is false)
{
merged[propertyName] = property.Value?.DeepClone();
}
}
return merged;
}
}
public static class ConfigurationExtensions
{
public static string GetValue(this IConfiguration configuration, string key) =>
configuration.TryGetValue(key)
.IfNone(() => throw new KeyNotFoundException($"Could not find '{key}' in configuration."));
public static Option<string> TryGetValue(this IConfiguration configuration, string key) =>
configuration.TryGetSection(key)
.Where(section => section.Value is not null)
.Select(section => section.Value!);
public static Option<IConfigurationSection> TryGetSection(this IConfiguration configuration, string key)
{
ArgumentNullException.ThrowIfNull(configuration);
var section = configuration.GetSection(key);
return section.Exists()
? Option<IConfigurationSection>.Some(section)
: Option<IConfigurationSection>.None;
}
public static IConfigurationBuilder AddUserSecretsWithLowestPriority(this IConfigurationBuilder builder, Assembly assembly, bool optional = true) =>
builder.AddWithLowestPriority(b => b.AddUserSecrets(assembly, optional));
private static IConfigurationBuilder AddWithLowestPriority(this IConfigurationBuilder builder, Func<IConfigurationBuilder, IConfigurationBuilder> adder)
{
// Configuration sources added last have the highest priority. We empty existing sources,
// add the new sources, and then add the existing sources back.
var adderSources = adder(new ConfigurationBuilder()).Sources;
var existingSources = builder.Sources;
var sources = adderSources.Concat(existingSources)
.ToImmutableArray();
builder.Sources.Clear();
sources.Iter(source => builder.Add(source));
return builder;
}
}
public static class ConfigurationModule
{
public static void ConfigureConfigurationJson(IHostApplicationBuilder builder)
{
builder.Services.TryAddSingleton(GetConfigurationJson);
}
private static ConfigurationJson GetConfigurationJson(IServiceProvider provider)
{
var configuration = provider.GetRequiredService<IConfiguration>();
var configurationJson = ConfigurationJson.From(configuration);
return TryGetConfigurationJsonFromYaml(configuration)
.Map(configurationJson.MergeWith)
.IfNone(configurationJson);
}
private static Option<ConfigurationJson> TryGetConfigurationJsonFromYaml(IConfiguration configuration) =>
configuration.TryGetValue("CONFIGURATION_YAML_PATH")
.Map(path => new FileInfo(path))
.Where(file => file.Exists)
.Map(file =>
{
using var reader = File.OpenText(file.FullName);
return ConfigurationJson.FromYaml(reader);
});
}