using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Reflection; namespace Config.Net.Core.Box { static class BoxFactory { public static Dictionary Discover(Type t, ValueHandler valueHandler, string? basePath) { var result = new Dictionary(); DiscoverProperties(t, valueHandler, result, basePath); DiscoverMethods(t, valueHandler, result); return result; } private static void DiscoverProperties(Type t, ValueHandler valueHandler, Dictionary result, string? basePath) { IEnumerable properties = GetHierarchyPublicProperties(t); foreach (PropertyInfo pi in properties) { Type propertyType = pi.PropertyType; ResultBox rbox; bool isCollection = false; if(ResultBox.TryGetCollection(propertyType, out propertyType)) { if(pi.SetMethod != null) { throw new NotSupportedException($"Collection properties cannot have a setter. Detected at '{OptionPath.Combine(basePath, pi.Name)}'"); } isCollection = true; } if(propertyType.GetTypeInfo().IsInterface) { rbox = new ProxyResultBox(pi.Name, propertyType); } else { rbox = new PropertyResultBox(pi.Name, propertyType); } ValidateSupportedType(rbox, valueHandler); AddAttributes(rbox, pi, valueHandler); //adjust to collection if(isCollection) { rbox = new CollectionResultBox(pi.Name, rbox); AddAttributes(rbox, pi, valueHandler); } result[pi.Name] = rbox; } } private static void DiscoverMethods(Type t, ValueHandler valueHandler, Dictionary result) { TypeInfo ti = t.GetTypeInfo(); IEnumerable methods = ti.DeclaredMethods.Where(m => !m.IsSpecialName); foreach (MethodInfo method in methods) { var mbox = new MethodResultBox(method); AddAttributes(mbox, method, valueHandler); result[mbox.Name] = mbox; } } private static void ValidateSupportedType(ResultBox rb, ValueHandler valueHandler) { Type? t = null; if (rb is PropertyResultBox pbox) t = rb.ResultBaseType; if (t != null && !valueHandler.IsSupported(t)) { throw new NotSupportedException($"type {t} on object '{rb.Name}' is not supported."); } } private static object? GetDefaultValue(Type t) { if (t.GetTypeInfo().IsValueType) return Activator.CreateInstance(t); return null; } private static void AddAttributes(ResultBox box, PropertyInfo pi, ValueHandler valueHandler) { AddAttributes(box, valueHandler, pi.GetCustomAttribute(), pi.GetCustomAttribute()); } private static void AddAttributes(ResultBox box, MethodInfo mi, ValueHandler valueHandler) { AddAttributes(box, valueHandler, mi.GetCustomAttribute(), mi.GetCustomAttribute()); } private static void AddAttributes(ResultBox box, ValueHandler valueHandler, params Attribute?[] attributes) { OptionAttribute? optionAttribute = attributes.OfType().FirstOrDefault(); DefaultValueAttribute? defaultValueAttribute = attributes.OfType().FirstOrDefault(); if (optionAttribute?.Alias != null) { box.StoreByName = optionAttribute.Alias; } box.DefaultResult = GetDefaultValue(optionAttribute?.DefaultValue, box, valueHandler) ?? GetDefaultValue(defaultValueAttribute?.Value, box, valueHandler) ?? GetDefaultValue(box.ResultType); } private static object? GetDefaultValue(object? defaultValue, ResultBox box, ValueHandler valueHandler) { object? result = null; if (defaultValue != null) { //validate that types for default value match Type dvt = defaultValue.GetType(); if (dvt != box.ResultType && dvt != typeof(string)) { throw new InvalidCastException($"Default value for option {box.Name} is of type {dvt.FullName} whereas the property has type {box.ResultType.FullName}. To fix this, either set default value to type {box.ResultType.FullName} or a string parseable to the target type."); } if (box.ResultType != typeof(string) && dvt == typeof(string)) { valueHandler.TryParse(box.ResultType, (string?)defaultValue, out result); } } if (result == null) { result = defaultValue; } return result; } private static PropertyInfo[] GetHierarchyPublicProperties(Type type) { var propertyInfos = new List(); var considered = new List(); var queue = new Queue(); considered.Add(type.GetTypeInfo()); queue.Enqueue(type.GetTypeInfo()); while (queue.Count > 0) { TypeInfo typeInfo = queue.Dequeue(); //add base interfaces to the queue foreach (Type subInterface in typeInfo.ImplementedInterfaces) { TypeInfo subInterfaceTypeInfo = subInterface.GetTypeInfo(); if (considered.Contains(subInterfaceTypeInfo)) continue; considered.Add(subInterfaceTypeInfo); queue.Enqueue(subInterfaceTypeInfo); } //add base classes to the queue if (typeInfo.BaseType != null) { TypeInfo baseType = typeInfo.BaseType.GetTypeInfo(); if (!considered.Contains(baseType)) { considered.Add(baseType); queue.Enqueue(baseType); } } //get properties from the current type IEnumerable newProperties = typeInfo.DeclaredProperties.Where(p => !propertyInfos.Contains(p)); propertyInfos.InsertRange(0, newProperties); } return propertyInfos.ToArray(); } } }