using System; using System.Dynamic; using System.Reflection; using System.Runtime.CompilerServices; using Newtonsoft.Json; [assembly: InternalsVisibleTo("deployment.tests")] namespace greyn.Deployment { public static class ConfigurationBootstrapper { private const string confpath = "appsettings.json"; public static T Load() where T : new() { if (File.Exists("appsettings.json")) { /* "aaaahhh this is the most abstract and reflective thing in the goddamn world aaaahhhhh next you'll make an ECS on a dynamic aaaahhh" * Ok, yes. I share these fears. but. * this way we can read a *very* arbitrary configuration file. * some random external file's configuration values is a good reason to aim for very high flexibility, imo * if the configuration expects new values we write them in, * if you left other junk for whatever reason, we don't delete them. */ var actualConfig = JsonConvert.DeserializeObject(File.ReadAllText(confpath)) ?? new ExpandoObject(); var toReturn = new T(); PopulateExpando(toReturn, ref actualConfig); File.WriteAllText(confpath, JsonConvert.SerializeObject(actualConfig, Formatting.Indented)); readFromExpando(ref toReturn, actualConfig); return toReturn; } else { var toReturn = new T(); File.WriteAllText(confpath, JsonConvert.SerializeObject(toReturn)); return toReturn; } } //TODO: make private but get tests to cooperate... I think that's a c# limitation internal static void PopulateExpando(T config, ref ExpandoObject fromFile) { if (config == null) return; if (fromFile == null) { fromFile = new ExpandoObject(); } var dictionaryFromExpandoFromFile = (IDictionary)fromFile; foreach (var memberInfo in config.GetType().GetMembers()) { if(memberInfo.DeclaringType == typeof(System.Object)) { continue; } if(memberInfo.MemberType != MemberTypes.Field && memberInfo.MemberType != MemberTypes.Property) { continue; } if (dictionaryFromExpandoFromFile.TryGetValue(memberInfo.Name, out object? valueFromDict)) { if (valueFromDict != null) { switch (memberInfo.MemberType) { case MemberTypes.Field: var asField = (FieldInfo)memberInfo; if (dictionaryFromExpandoFromFile[asField.Name] is ExpandoObject childMemberField) { PopulateExpando(asField.GetValue(config), ref childMemberField); } break; case MemberTypes.Property: var asProperty = (PropertyInfo)memberInfo; if (dictionaryFromExpandoFromFile[asProperty.Name] is ExpandoObject childMemberProperty) { PopulateExpando(asProperty.GetValue(config), ref childMemberProperty); } break; default: break; } } } else { switch (memberInfo.MemberType) { case MemberTypes.Field: fromFile.TryAdd(memberInfo.Name, ((FieldInfo)memberInfo).GetValue(config)); break; case MemberTypes.Property: fromFile.TryAdd(memberInfo.Name, ((PropertyInfo)memberInfo).GetValue(config)); break; default: break; } } } } internal static void readFromExpando(ref T config, ExpandoObject readFromFile) { //TODO: read from Expando } // private static T sync(ref T config, ref ExpandoObject readFromFile) // { // var fieldsFoundThisLevel = new List(); // if(config == null) // { // //TODO: it's entirely possible this is null. // throw new ArgumentNullException(); // } // if(readFromFile == null) // { // //TODO: it's entirely possible this is null. // throw new ArgumentNullException(); // } // var readUnExpandoed = (IDictionary)readFromFile; // //if it's present on what was read // //set it on Config // //if it's not found on what was read, or type is different // foreach (var prop in config.GetType().GetProperties()) // { // fieldsFoundThisLevel.Add(prop.Name); // if(readUnExpandoed.TryGetValue(prop.Name, out object? valueFromFile)) //so a configuration file can *set* a value *to null* // { // if(valueFromFile != null) // { // var defaultConfigValue = prop.GetValue(config); // if (defaultConfigValue != null) // { // //expandofy, since we objectified it. // var expandoFromFile = new ExpandoObject(); // var dictionaryFromExpandoFromFile = (IDictionary)expandoFromFile; // foreach (var property in valueFromFile.GetType().GetProperties()) // dictionaryFromExpandoFromFile.Add(property.Name, property.GetValue(valueFromFile)); // expandoFromFile = (ExpandoObject)dictionaryFromExpandoFromFile; // //dive in, repeat // sync(ref defaultConfigValue, ref expandoFromFile); // //we've now Sync'd expandoFromFile and defaultConfigValue // prop.SetValue(readFromFile, expandoFromFile); // prop.SetValue(config, expandoFromFile); // } // } // } // else // { // readFromFile.TryAdd(prop.Name, prop.GetValue(config)); // } // } // } } }