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; Console.WriteLine("config is not null"); Console.WriteLine($"fromfile null: {fromFile == null}"); if (fromFile == null) { fromFile = new ExpandoObject(); // var expandoPopulated = new ExpandoObject(); // var dictionaryFromExpandoPopulated = (IDictionary)expandoPopulated; // foreach (var memberInfo in config.GetType().GetMembers()) // { // switch (memberInfo.MemberType) // { // case MemberTypes.Field: // dictionaryFromExpandoPopulated.Add(memberInfo.Name, ((FieldInfo)memberInfo).GetValue(config)); // break; // case MemberTypes.Property: // dictionaryFromExpandoPopulated.Add(memberInfo.Name, ((PropertyInfo)memberInfo).GetValue(config)); // break; // default: // break; // } // } // expandoPopulated = (ExpandoObject)dictionaryFromExpandoPopulated; // fromFile = expandoPopulated; // return; } var dictionaryFromExpandoFromFile = (IDictionary)fromFile; Console.WriteLine($"{config.GetType().GetMembers().Count()} members on {config.GetType()}"); foreach (var memberInfo in config.GetType().GetMembers()) { Console.WriteLine($"checking property: {memberInfo.Name}"); if(memberInfo.DeclaringType == typeof(System.Object)) { Console.WriteLine($"junk from system.object, don't care"); continue; } if(memberInfo.MemberType != MemberTypes.Field && memberInfo.MemberType != MemberTypes.Property) { Console.WriteLine($"type I don't know how to handle ({memberInfo.MemberType})"); continue; } if (dictionaryFromExpandoFromFile.TryGetValue(memberInfo.Name, out object? valueFromDict)) { Console.WriteLine($"dictionary has it - {valueFromDict}"); if (valueFromDict != null) { Console.WriteLine($"value from configuration file is not null"); switch (memberInfo.MemberType) { case MemberTypes.Field: var asField = (FieldInfo)memberInfo; //if ((asField.FieldType.GetMembers(BindingFlags.DeclaredOnly)?.Count() ?? 0) > 0) { Console.WriteLine($"{asField.Name} (as field) has {asField.FieldType.GetMembers() .Count()} members."); Console.WriteLine($"{dictionaryFromExpandoFromFile[asField.Name]}"); var childMember = dictionaryFromExpandoFromFile[asField.Name] as ExpandoObject; if(childMember != null) { populateExpando(asField.GetValue(config), ref childMember); //asField.SetValue(fromFile, childMember); //you don't have to set it back. the expandoobject *implements* IDictionary // so when you cast it, it's not a copy; you just access it as a dictionary. So you can change it dynamically. // and we ref'd it so it got the underlying one. //https://stackoverflow.com/questions/4938397/dynamically-adding-properties-to-an-expandoobject } } break; case MemberTypes.Property: var asProperty = (PropertyInfo)memberInfo; //if ((asProperty.PropertyType.GetMembers(BindingFlags.DeclaredOnly)?.Count() ?? 0) > 0) { Console.WriteLine($"{asProperty.Name} (as property) has {asProperty.PropertyType.GetMembers() .Count()} members."); Console.WriteLine($"{dictionaryFromExpandoFromFile[asProperty.Name]}"); var childMember = dictionaryFromExpandoFromFile[asProperty.Name] as ExpandoObject; // (ExpandoObject)asProperty.GetValue(fromFile); if(childMember != null) { populateExpando(asProperty.GetValue(config), ref childMember); //asProperty.SetValue(fromFile, childMember); } } break; default: Console.WriteLine($"membertype unhanlded. {memberInfo.MemberType}"); break; } } } else { Console.WriteLine($"dictionary didn't have it. so let's add to fromFile"); switch (memberInfo.MemberType) { case MemberTypes.Field: var addTry = fromFile.TryAdd(memberInfo.Name, ((FieldInfo)memberInfo).GetValue(config)); Console.WriteLine($"ostensibly, succeeded? {addTry}"); break; case MemberTypes.Property: var addTry2 = fromFile.TryAdd(memberInfo.Name, ((PropertyInfo)memberInfo).GetValue(config)); Console.WriteLine($"ostensibly, succeeded? {addTry2}"); break; default: break; } } Console.WriteLine($"{memberInfo.Name} handled"); } } 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)); // } // } // } } }