diff --git a/Config.Net/Config.Net.csproj b/Config.Net/Config.Net.csproj
new file mode 100644
index 0000000..04b7f21
--- /dev/null
+++ b/Config.Net/Config.Net.csproj
@@ -0,0 +1,76 @@
+
+
+ Copyright (c) 2015-2022 by Ivan Gavryliuk
+ Config.Net
+ Ivan Gavryliuk (@aloneguid)
+ Config.Net
+ Config.Net
+ 4.0.0.0
+ 4.7.3.0
+ 4.7.3
+ Super simple configuration framework for .NET focused on developer ergonomics and strong typing. Supports multiple configuration sources such as .ini, .json, .xml files, as well as external providers pluggable by other NuGet packages.
+ true
+
+ icon.png
+ Apache-2.0
+
+ enable
+ latest
+ https://github.com/aloneguid/config
+ https://github.com/aloneguid/config
+ README.md
+
+
+
+
+
+ netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0
+
+
+
+
+ netstandard2.0;netstandard2.1;netcoreapp3.1;net5.0;net6.0;net7.0
+
+
+
+
+
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ True
+ \
+
+
+ True
+
+
+
+
+
+
+ all
+ runtime; build; native; contentfiles; analyzers
+
+
+
\ No newline at end of file
diff --git a/Config.Net/ConfigurationBuilder.cs b/Config.Net/ConfigurationBuilder.cs
new file mode 100644
index 0000000..a946b58
--- /dev/null
+++ b/Config.Net/ConfigurationBuilder.cs
@@ -0,0 +1,72 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+using Castle.DynamicProxy;
+using Config.Net.Core;
+
+namespace Config.Net
+{
+ public class ConfigurationBuilder where T : class
+ {
+ private readonly ProxyGenerator _generator = new ProxyGenerator();
+ private List _stores = new List();
+ private TimeSpan _cacheInterval = TimeSpan.Zero;
+ private readonly List _customParsers = new List();
+
+ public ConfigurationBuilder()
+ {
+ TypeInfo ti = typeof(T).GetTypeInfo();
+
+ if (!ti.IsInterface) throw new ArgumentException($"{ti.FullName} must be an interface", ti.FullName);
+ }
+
+ ///
+ /// Creates an instance of the configuration interface
+ ///
+ ///
+ public T Build()
+ {
+ var valueHandler = new ValueHandler(_customParsers);
+ var ioHandler = new IoHandler(_stores, valueHandler, _cacheInterval);
+
+ T instance = _generator.CreateInterfaceProxyWithoutTarget(new InterfaceInterceptor(typeof(T), ioHandler));
+
+ return instance;
+ }
+
+ ///
+ /// Set to anything different from to add caching for values. By default
+ /// Config.Net doesn't cache any values
+ ///
+ ///
+ ///
+ public ConfigurationBuilder CacheFor(TimeSpan time)
+ {
+ _cacheInterval = time;
+
+ return this;
+ }
+
+ public ConfigurationBuilder UseConfigStore(IConfigStore store)
+ {
+ _stores.Add(store);
+ return this;
+ }
+
+ ///
+ /// Adds a custom type parser
+ ///
+ public ConfigurationBuilder UseTypeParser(ITypeParser parser)
+ {
+ if (parser == null)
+ {
+ throw new ArgumentNullException(nameof(parser));
+ }
+
+ _customParsers.Add(parser);
+
+ return this;
+ }
+ }
+}
diff --git a/Config.Net/ConfigurationExtensions.cs b/Config.Net/ConfigurationExtensions.cs
new file mode 100644
index 0000000..72ab7e0
--- /dev/null
+++ b/Config.Net/ConfigurationExtensions.cs
@@ -0,0 +1,146 @@
+using System.Reflection;
+using Config.Net.Stores;
+using System.Collections.Generic;
+using Config.Net.Stores.Impl.CommandLine;
+
+namespace Config.Net
+{
+ ///
+ /// Configuration extensions
+ ///
+ public static class ConfigurationExtensions
+ {
+ ///
+ /// In-memory dictionary. Optionally you can pass pre-created dictionary, otherwise it will be created internally as empty.
+ ///
+ public static ConfigurationBuilder UseInMemoryDictionary(
+ this ConfigurationBuilder builder,
+ IDictionary? container = null) where TInterface : class
+ {
+ builder.UseConfigStore(new DictionaryConfigStore(container));
+ return builder;
+ }
+
+ ///
+ /// Standard app.config (web.config) builder store. Read-only.
+ ///
+ public static ConfigurationBuilder UseAppConfig(this ConfigurationBuilder builder) where TInterface : class
+ {
+ builder.UseConfigStore(new AppConfigStore());
+ return builder;
+ }
+
+ ///
+ /// Reads builder from the .dll.config or .exe.config file.
+ ///
+ ///
+ /// Reference to the assembly to look for
+ ///
+ public static ConfigurationBuilder UseAssemblyConfig(this ConfigurationBuilder builder, Assembly assembly) where TInterface : class
+ {
+ builder.UseConfigStore(new AssemblyConfigStore(assembly));
+ return builder;
+ }
+
+ ///
+ /// Uses system environment variables
+ ///
+ public static ConfigurationBuilder UseEnvironmentVariables(this ConfigurationBuilder builder) where TInterface : class
+ {
+ builder.UseConfigStore(new EnvironmentVariablesStore());
+ return builder;
+ }
+
+
+ ///
+ /// Simple INI storage.
+ ///
+ ///
+ /// File does not have to exist, however it will be created as soon as you try to write to it.
+ /// When true, inline comments are parsed. It is set to false by default so inline comments are considered a part of the value.
+ ///
+ public static ConfigurationBuilder UseIniFile(this ConfigurationBuilder builder,
+ string iniFilePath,
+ bool parseInlineComments = false) where TInterface : class
+ {
+ builder.UseConfigStore(new IniFileConfigStore(iniFilePath, true, parseInlineComments));
+ return builder;
+ }
+
+ ///
+ /// Simple INI storage.
+ ///
+ ///
+ /// File contents
+ /// When true, inline comments are parsed. It is set to false by default so inline comments are considered a part of the value
+ ///
+ public static ConfigurationBuilder UseIniString(this ConfigurationBuilder builder,
+ string iniString,
+ bool parseInlineComments = false) where TInterface : class
+ {
+ builder.UseConfigStore(new IniFileConfigStore(iniString, false, parseInlineComments));
+ return builder;
+ }
+
+ ///
+ /// Accepts builder from the command line arguments. This is not intended to replace a command line parsing framework but rather
+ /// complement it in a builder like way. Uses current process' command line parameters automatically
+ ///
+ /// Configuration object
+ /// When true argument names are case sensitive, false by default
+ /// Changed builder
+ public static ConfigurationBuilder UseCommandLineArgs(this ConfigurationBuilder builder,
+ bool isCaseSensitive = false,
+ params KeyValuePair[] parameterNameToPosition)
+ where TInterface : class
+ {
+ builder.UseConfigStore(new CommandLineConfigStore(null, isCaseSensitive, parameterNameToPosition));
+ return builder;
+ }
+
+ public static ConfigurationBuilder UseCommandLineArgs(this ConfigurationBuilder builder,
+ bool isCaseSensitive = false,
+ string[]? args = null,
+ params KeyValuePair[] parameterNameToPosition)
+ where TInterface : class
+ {
+ builder.UseConfigStore(new CommandLineConfigStore(args, isCaseSensitive, parameterNameToPosition));
+ return builder;
+ }
+
+ public static ConfigurationBuilder UseCommandLineArgs(this ConfigurationBuilder builder,
+ params KeyValuePair[] parameterNameToPosition)
+ where TInterface : class
+ {
+ builder.UseConfigStore(new CommandLineConfigStore(null, false, parameterNameToPosition));
+ return builder;
+ }
+
+ ///
+ /// Uses JSON file as a builder storage.
+ ///
+ /// Configuration object.
+ /// Full path to json storage file.
+ /// Changed builder.
+ /// Storage file does not have to exist, however it will be created as soon as first write performed.
+ public static ConfigurationBuilder UseJsonFile(this ConfigurationBuilder builder, string jsonFilePath) where TInterface : class
+ {
+ builder.UseConfigStore(new JsonConfigStore(jsonFilePath, true));
+ return builder;
+ }
+
+ ///
+ /// Uses JSON file as a builder storage.
+ ///
+ /// Configuration object.
+ /// Json document.
+ /// Changed builder.
+ /// Storage file does not have to exist, however it will be created as soon as first write performed.
+ public static ConfigurationBuilder UseJsonString(this ConfigurationBuilder builder, string jsonString) where TInterface : class
+ {
+ builder.UseConfigStore(new JsonConfigStore(jsonString, false));
+ return builder;
+ }
+
+ }
+}
diff --git a/Config.Net/Core/Box/BoxFactory.cs b/Config.Net/Core/Box/BoxFactory.cs
new file mode 100644
index 0000000..6e99479
--- /dev/null
+++ b/Config.Net/Core/Box/BoxFactory.cs
@@ -0,0 +1,204 @@
+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();
+ }
+
+
+ }
+}
diff --git a/Config.Net/Core/Box/CollectionResultBox.cs b/Config.Net/Core/Box/CollectionResultBox.cs
new file mode 100644
index 0000000..7f2f943
--- /dev/null
+++ b/Config.Net/Core/Box/CollectionResultBox.cs
@@ -0,0 +1,109 @@
+using System;
+using System.Collections;
+using System.Collections.Generic;
+
+namespace Config.Net.Core.Box
+{
+ class CollectionResultBox : ResultBox
+ {
+ private readonly ResultBox _elementResultBox;
+ private string? _basePath;
+ private DynamicReader? _reader;
+
+ public CollectionResultBox(string name, ResultBox elementBox) : base(name, elementBox.ResultType, null)
+ {
+ _elementResultBox = elementBox;
+ }
+
+ public ResultBox ElementResultBox => _elementResultBox;
+
+ public bool IsInitialised { get; private set; }
+
+ public IEnumerable? CollectionInstance { get; private set; }
+
+ public void Initialise(string? basePath, int length, DynamicReader reader)
+ {
+ _basePath = basePath;
+ _reader = reader;
+
+ CollectionInstance = CreateGenericEnumerable(length);
+
+ IsInitialised = true;
+ }
+
+ private IEnumerable? CreateGenericEnumerable(int count)
+ {
+ Type t = typeof(DynamicEnumerable<>);
+ t = t.MakeGenericType(ResultType);
+
+ IEnumerable? instance = (IEnumerable?)Activator.CreateInstance(t, count, this);
+
+ return instance;
+ }
+
+ private object? ReadAt(int index)
+ {
+ return _reader?.Read(ElementResultBox, index);
+ }
+
+ private class DynamicEnumerable : IEnumerable
+ {
+ private readonly int _count;
+ private readonly CollectionResultBox _parent;
+
+ public DynamicEnumerable(int count, CollectionResultBox parent)
+ {
+ _count = count;
+ _parent = parent;
+ }
+
+ public IEnumerator GetEnumerator()
+ {
+ return new DynamicEnumerator(_count, _parent);
+ }
+
+ IEnumerator IEnumerable.GetEnumerator()
+ {
+ return new DynamicEnumerator(_count, _parent);
+ }
+ }
+
+ private class DynamicEnumerator : IEnumerator
+ {
+ private int _index = -1;
+ private readonly int _count;
+ private readonly CollectionResultBox _parent;
+ private T? _current;
+
+ public DynamicEnumerator(int count, CollectionResultBox parent)
+ {
+ _count = count;
+ _parent = parent;
+ }
+
+#pragma warning disable CS8603 // Possible null reference return.
+ public T Current => _current ?? default(T);
+
+ object IEnumerator.Current => _current;
+#pragma warning restore CS8603 // Possible null reference return.
+
+ public void Dispose()
+ {
+ }
+
+ public bool MoveNext()
+ {
+ _index += 1;
+
+ _current = (T?)_parent.ReadAt(_index);
+
+ return _index < _count;
+ }
+
+ public void Reset()
+ {
+ _index = -1;
+ }
+ }
+ }
+}
diff --git a/Config.Net/Core/Box/MethodResultBox.cs b/Config.Net/Core/Box/MethodResultBox.cs
new file mode 100644
index 0000000..adf29c3
--- /dev/null
+++ b/Config.Net/Core/Box/MethodResultBox.cs
@@ -0,0 +1,87 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+
+namespace Config.Net.Core.Box
+{
+ class MethodResultBox : ResultBox
+ {
+ public MethodResultBox(MethodInfo methodInfo) : base(GetName(methodInfo), GetReturnType(methodInfo), null)
+ {
+ StoreByName = GetStoreName(methodInfo);
+ IsGettter = IsGet(methodInfo);
+ }
+
+ public bool IsGettter { get; private set; }
+
+ ///
+ /// Composes a uniqueue method name using method name itself and parameter type names, separated by underscore
+ ///
+ public static string GetName(MethodInfo mi)
+ {
+ ParameterInfo[] parameters = mi.GetParameters();
+ var sb = new StringBuilder();
+ sb.Append(mi.Name);
+ foreach (ParameterInfo pi in parameters)
+ {
+ sb.Append("-");
+ sb.Append(pi.ParameterType.ToString());
+ }
+ return sb.ToString();
+ }
+
+ public string GetValuePath(object[] arguments)
+ {
+ var sb = new StringBuilder();
+ sb.Append(StoreByName);
+ bool ignoreLast = !IsGettter;
+
+ for (int i = 0; i < arguments.Length - (ignoreLast ? 1 : 0); i++)
+ {
+ object value = arguments[i];
+ if (value == null) continue;
+
+ if (sb.Length > 0)
+ {
+ sb.Append(OptionPath.Separator);
+ }
+ sb.Append(value.ToString());
+ }
+
+ return sb.ToString();
+ }
+
+ private static string GetStoreName(MethodInfo mi)
+ {
+ string name = mi.Name;
+
+ if (name.StartsWith("get", StringComparison.OrdinalIgnoreCase) ||
+ name.StartsWith("set", StringComparison.OrdinalIgnoreCase))
+ {
+ name = name.Substring(3);
+ }
+
+ return name;
+ }
+
+ private static bool IsGet(MethodInfo mi)
+ {
+ return mi.ReturnType != typeof(void);
+ }
+
+ private static Type GetReturnType(MethodInfo mi)
+ {
+ ParameterInfo[] parameters = mi.GetParameters();
+
+ if (parameters == null || parameters.Length == 0)
+ {
+ throw new InvalidOperationException($"method {mi.Name} must have at least one parameter");
+ }
+
+ Type returnType = IsGet(mi) ? mi.ReturnType : parameters[parameters.Length - 1].ParameterType;
+
+ return returnType;
+ }
+ }
+}
diff --git a/Config.Net/Core/Box/PropertyResultBox.cs b/Config.Net/Core/Box/PropertyResultBox.cs
new file mode 100644
index 0000000..0bef2c2
--- /dev/null
+++ b/Config.Net/Core/Box/PropertyResultBox.cs
@@ -0,0 +1,51 @@
+using System;
+using System.Reflection;
+
+namespace Config.Net.Core.Box
+{
+ class PropertyResultBox : ResultBox
+ {
+ public PropertyResultBox(string name, Type resultType) : base(name, resultType, null)
+ {
+ }
+
+ public static bool IsProperty(MethodInfo mi, out bool isGetter, out string? name)
+ {
+ if (mi.Name.StartsWith("get_"))
+ {
+ isGetter = true;
+ name = mi.Name.Substring(4);
+ return true;
+ }
+
+ if (mi.Name.StartsWith("set_"))
+ {
+ isGetter = false;
+ name = mi.Name.Substring(4);
+ return true;
+ }
+
+ isGetter = false;
+ name = null;
+ return false;
+ }
+
+ public static bool IsProperty(MethodInfo mi, out string? name)
+ {
+ if (mi.Name.StartsWith("get_") || mi.Name.StartsWith("set_"))
+ {
+ name = mi.Name.Substring(4);
+ return true;
+ }
+
+ name = null;
+ return false;
+ }
+
+ public static bool IsGetProperty(MethodInfo mi)
+ {
+ return mi.Name.StartsWith("get_");
+ }
+
+ }
+}
diff --git a/Config.Net/Core/Box/ProxyResultBox.cs b/Config.Net/Core/Box/ProxyResultBox.cs
new file mode 100644
index 0000000..cff7c5b
--- /dev/null
+++ b/Config.Net/Core/Box/ProxyResultBox.cs
@@ -0,0 +1,34 @@
+using System;
+using System.Collections.Generic;
+using Castle.DynamicProxy;
+
+namespace Config.Net.Core.Box
+{
+ class ProxyResultBox : ResultBox
+ {
+ private static readonly ProxyGenerator ProxyGenerator = new ProxyGenerator();
+ private readonly Dictionary _indexToProxyInstance = new Dictionary();
+
+ public ProxyResultBox(string name, Type interfaceType) : base(name, interfaceType, null)
+ {
+ }
+
+ public bool IsInitialisedAt(int index)
+ {
+ return _indexToProxyInstance.ContainsKey(index);
+ }
+
+ public object GetInstanceAt(int index)
+ {
+ return _indexToProxyInstance[index];
+ }
+
+ public void InitialiseAt(int index, IoHandler ioHandler, string prefix)
+ {
+ object instance = ProxyGenerator.CreateInterfaceProxyWithoutTarget(ResultBaseType,
+ new InterfaceInterceptor(ResultBaseType, ioHandler, prefix));
+
+ _indexToProxyInstance[index] = instance;
+ }
+ }
+}
diff --git a/Config.Net/Core/Box/ResultBox.cs b/Config.Net/Core/Box/ResultBox.cs
new file mode 100644
index 0000000..bfd96b4
--- /dev/null
+++ b/Config.Net/Core/Box/ResultBox.cs
@@ -0,0 +1,73 @@
+using System;
+using System.Collections.Generic;
+using System.Reflection;
+using System.Text;
+
+namespace Config.Net.Core.Box
+{
+ abstract class ResultBox
+ {
+ private string? _storeByName;
+
+ protected ResultBox(string name, Type resultType, object? defaultResult)
+ {
+ Name = name;
+ ResultType = resultType;
+ ResultBaseType = GetBaseType(resultType);
+ DefaultResult = defaultResult;
+ }
+
+ public string Name { get; }
+
+ public string StoreByName
+ {
+ get => _storeByName ?? Name;
+ set => _storeByName = value;
+ }
+
+ public Type ResultType { get; }
+
+ public Type ResultBaseType { get; }
+
+ public object? DefaultResult { get; set; }
+
+ #region [ Utility Methods ]
+
+ private static Type GetBaseType(Type t)
+ {
+ TypeInfo ti = t.GetTypeInfo();
+ if (ti.IsClass)
+ {
+ return t;
+ }
+ else
+ {
+ if (ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(Nullable<>))
+ {
+ return ti.GenericTypeArguments[0];
+ }
+ else
+ {
+ return t;
+ }
+ }
+
+ }
+
+ internal static bool TryGetCollection(Type t, out Type elementType)
+ {
+ TypeInfo ti = t.GetTypeInfo();
+
+ if(ti.IsGenericType && ti.GetGenericTypeDefinition() == typeof(IEnumerable<>))
+ {
+ elementType = ti.GenericTypeArguments[0];
+ return true;
+ }
+
+ elementType = t;
+ return false;
+ }
+
+ #endregion
+ }
+}
diff --git a/Config.Net/Core/DynamicReader.cs b/Config.Net/Core/DynamicReader.cs
new file mode 100644
index 0000000..81674c9
--- /dev/null
+++ b/Config.Net/Core/DynamicReader.cs
@@ -0,0 +1,74 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Config.Net.Core.Box;
+
+namespace Config.Net.Core
+{
+ class DynamicReader
+ {
+ private readonly string? _basePath;
+ private readonly IoHandler _ioHandler;
+
+ public DynamicReader(string? basePath, IoHandler ioHandler)
+ {
+ _basePath = basePath;
+ _ioHandler = ioHandler;
+ }
+
+ public object? Read(ResultBox rbox, int index = -1, params object[] arguments)
+ {
+ if (rbox is PropertyResultBox pbox) return ReadProperty(pbox, index);
+
+ if (rbox is ProxyResultBox xbox) return ReadProxy(xbox, index);
+
+ if (rbox is CollectionResultBox cbox) return ReadCollection(cbox, index);
+
+ if (rbox is MethodResultBox mbox) return ReadMethod(mbox, arguments);
+
+ throw new NotImplementedException($"don't know how to read {rbox.GetType()}");
+ }
+
+ private object? ReadProperty(PropertyResultBox pbox, int index)
+ {
+ string path = OptionPath.Combine(index, _basePath, pbox.StoreByName);
+
+ return _ioHandler.Read(pbox.ResultBaseType, path, pbox.DefaultResult);
+ }
+
+ private object ReadProxy(ProxyResultBox xbox, int index)
+ {
+ if (!xbox.IsInitialisedAt(index))
+ {
+ string prefix = OptionPath.Combine(index, _basePath, xbox.StoreByName);
+
+ xbox.InitialiseAt(index, _ioHandler, prefix);
+ }
+
+ return xbox.GetInstanceAt(index);
+ }
+
+ private object? ReadCollection(CollectionResultBox cbox, int index)
+ {
+ string lengthPath = OptionPath.Combine(index, _basePath, cbox.StoreByName);
+ lengthPath = OptionPath.AddLength(lengthPath);
+
+ if (!cbox.IsInitialised)
+ {
+ int length = (int?)_ioHandler.Read(typeof(int), lengthPath, 0) ?? 0;
+
+ cbox.Initialise(_basePath, length, this);
+ }
+
+ return cbox.CollectionInstance;
+ }
+
+ private object? ReadMethod(MethodResultBox mbox, object[] arguments)
+ {
+ string path = mbox.GetValuePath(arguments);
+ path = OptionPath.Combine(_basePath, path);
+
+ return _ioHandler.Read(mbox.ResultBaseType, path, mbox.DefaultResult);
+ }
+ }
+}
diff --git a/Config.Net/Core/DynamicWriter.cs b/Config.Net/Core/DynamicWriter.cs
new file mode 100644
index 0000000..18039a8
--- /dev/null
+++ b/Config.Net/Core/DynamicWriter.cs
@@ -0,0 +1,48 @@
+using System;
+using Config.Net.Core.Box;
+
+namespace Config.Net.Core
+{
+ class DynamicWriter
+ {
+ private readonly string? _basePath;
+ private readonly IoHandler _ioHandler;
+
+ public DynamicWriter(string? basePath, IoHandler ioHandler)
+ {
+ _basePath = basePath;
+ _ioHandler = ioHandler;
+ }
+
+ public void Write(ResultBox rbox, object[] arguments)
+ {
+ if (rbox is PropertyResultBox pbox) WriteProperty(pbox, arguments);
+
+ else if (rbox is MethodResultBox mbox) WriteMethod(mbox, arguments);
+
+ else if (rbox is ProxyResultBox xbox) WriteProxy(xbox, arguments);
+
+ else throw new NotImplementedException($"don't know how to write {rbox.GetType()}");
+ }
+
+ private void WriteProperty(PropertyResultBox pbox, object[] arguments)
+ {
+ string path = OptionPath.Combine(_basePath, pbox.StoreByName);
+
+ _ioHandler.Write(pbox.ResultBaseType, path, arguments[0]);
+ }
+
+ private void WriteMethod(MethodResultBox mbox, object[] arguments)
+ {
+ object value = arguments[arguments.Length - 1];
+ string path = mbox.GetValuePath(arguments);
+
+ _ioHandler.Write(mbox.ResultBaseType, path, value);
+ }
+
+ private void WriteProxy(ProxyResultBox xbox, object[] arguments)
+ {
+ throw new NotSupportedException("cannot assign values to interface properties");
+ }
+ }
+}
diff --git a/Config.Net/Core/Extensions.cs b/Config.Net/Core/Extensions.cs
new file mode 100644
index 0000000..646d92e
--- /dev/null
+++ b/Config.Net/Core/Extensions.cs
@@ -0,0 +1,16 @@
+using System.Collections.Generic;
+
+namespace Config.Net.Core
+{
+ static class Extensions
+ {
+ public static TValue? GetValueOrDefaultInternal(this IDictionary dictionary, TKey key)
+ where TKey: notnull
+ where TValue: class
+ {
+ if (!dictionary.TryGetValue(key, out TValue? value)) return default(TValue);
+
+ return value;
+ }
+ }
+}
diff --git a/Config.Net/Core/FlatArrays.cs b/Config.Net/Core/FlatArrays.cs
new file mode 100644
index 0000000..e22bf56
--- /dev/null
+++ b/Config.Net/Core/FlatArrays.cs
@@ -0,0 +1,58 @@
+using System;
+using System.Collections.Generic;
+using System.Text;
+using Config.Net.Core;
+using Config.Net.TypeParsers;
+
+namespace Config.Net.Core
+{
+ ///
+ /// Helper class to implement flat arrays
+ ///
+ public static class FlatArrays
+ {
+ public static bool IsArrayLength(string? key, Func getValue, out int length)
+ {
+ if (!OptionPath.TryStripLength(key, out key))
+ {
+ length = 0;
+ return false;
+ }
+
+ string? value = key == null ? null : getValue(key);
+ if (value == null)
+ {
+ length = 0;
+ return false;
+ }
+
+ if (!StringArrayParser.TryParse(value, out string[]? ar))
+ {
+ length = 0;
+ return false;
+ }
+
+ length = ar?.Length ?? 0;
+ return true;
+ }
+
+ public static bool IsArrayElement(string? key, Func getValue, out string? value)
+ {
+ if(!OptionPath.TryStripIndex(key, out key, out int index))
+ {
+ value = null;
+ return false;
+ }
+
+ string? arrayString = key == null ? null : getValue(key);
+ if (!StringArrayParser.TryParse(arrayString, out string[]? array) || index >= array?.Length)
+ {
+ value = null;
+ return false;
+ }
+
+ value = array?[index] ?? null;
+ return true;
+ }
+ }
+}
diff --git a/Config.Net/Core/InterfaceInterceptor.cs b/Config.Net/Core/InterfaceInterceptor.cs
new file mode 100644
index 0000000..b02d3b3
--- /dev/null
+++ b/Config.Net/Core/InterfaceInterceptor.cs
@@ -0,0 +1,103 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Reflection;
+using System.Text;
+using Castle.DynamicProxy;
+using Config.Net.Core.Box;
+
+namespace Config.Net.Core
+{
+ class InterfaceInterceptor : IInterceptor
+ {
+ private readonly Dictionary _boxes;
+ private IoHandler _ioHandler;
+ private readonly string? _prefix;
+ private readonly DynamicReader _reader;
+ private readonly DynamicWriter _writer;
+ private readonly bool _isInpc;
+ private PropertyChangedEventHandler? _inpcHandler;
+
+ public InterfaceInterceptor(Type interfaceType, IoHandler ioHandler, string? prefix = null)
+ {
+ _boxes = BoxFactory.Discover(interfaceType, ioHandler.ValueHandler, prefix);
+ _ioHandler = ioHandler;
+ _prefix = prefix;
+ _reader = new DynamicReader(prefix, ioHandler);
+ _writer = new DynamicWriter(prefix, ioHandler);
+ _isInpc = interfaceType.GetInterface(nameof(INotifyPropertyChanged)) != null;
+ }
+
+ private ResultBox FindBox(IInvocation invocation)
+ {
+ if (PropertyResultBox.IsProperty(invocation.Method, out string? propertyName) && propertyName != null)
+ {
+ return _boxes[propertyName];
+ }
+ else //method
+ {
+ string name = MethodResultBox.GetName(invocation.Method);
+ return _boxes[name];
+ }
+ }
+
+ public void Intercept(IInvocation invocation)
+ {
+ if (TryInterceptInpc(invocation)) return;
+
+ ResultBox rbox = FindBox(invocation);
+
+ bool isRead =
+ (rbox is PropertyResultBox && PropertyResultBox.IsGetProperty(invocation.Method)) ||
+ (rbox is ProxyResultBox && PropertyResultBox.IsGetProperty(invocation.Method)) ||
+ (rbox is MethodResultBox mbox && mbox.IsGettter) ||
+ (rbox is CollectionResultBox);
+
+ if(isRead)
+ {
+ invocation.ReturnValue = _reader.Read(rbox, -1, invocation.Arguments);
+ return;
+ }
+ else
+ {
+ _writer.Write(rbox, invocation.Arguments);
+
+ TryNotifyInpc(invocation, rbox);
+ }
+ }
+
+ private bool TryInterceptInpc(IInvocation invocation)
+ {
+ if (!_isInpc) return false;
+
+ if (invocation.Method.Name == "add_PropertyChanged")
+ {
+ invocation.ReturnValue =
+ _inpcHandler =
+ (PropertyChangedEventHandler)Delegate.Combine(_inpcHandler, (Delegate)invocation.Arguments[0]);
+ return true;
+ }
+ else if(invocation.Method.Name == "remove_PropertyChanged")
+ {
+ invocation.ReturnValue =
+ _inpcHandler =
+ (PropertyChangedEventHandler?)Delegate.Remove(_inpcHandler, (Delegate)invocation.Arguments[0]);
+ return true;
+ }
+
+ return false;
+ }
+
+ private void TryNotifyInpc(IInvocation invocation, ResultBox rbox)
+ {
+ if (_inpcHandler == null || rbox is MethodResultBox) return;
+
+ _inpcHandler.Invoke(invocation.InvocationTarget, new PropertyChangedEventArgs(rbox.Name));
+ if(rbox.Name != rbox.StoreByName)
+ {
+ //notify on StoreByName as well
+ _inpcHandler.Invoke(invocation.InvocationTarget, new PropertyChangedEventArgs(rbox.StoreByName));
+ }
+ }
+ }
+}
diff --git a/Config.Net/Core/IoHandler.cs b/Config.Net/Core/IoHandler.cs
new file mode 100644
index 0000000..0d8e0e2
--- /dev/null
+++ b/Config.Net/Core/IoHandler.cs
@@ -0,0 +1,70 @@
+using System;
+using System.Collections.Concurrent;
+using System.Collections.Generic;
+using System.Linq;
+
+namespace Config.Net.Core
+{
+ class IoHandler
+ {
+ private readonly IEnumerable _stores;
+ private readonly ValueHandler _valueHandler;
+ private readonly TimeSpan _cacheInterval;
+ private readonly ConcurrentDictionary> _keyToValue = new ConcurrentDictionary>();
+
+ public IoHandler(IEnumerable stores, ValueHandler valueHandler, TimeSpan cacheInterval)
+ {
+ _stores = stores ?? throw new ArgumentNullException(nameof(stores));
+ _valueHandler = valueHandler ?? throw new ArgumentNullException(nameof(valueHandler));
+ _cacheInterval = cacheInterval;
+ }
+
+ public ValueHandler ValueHandler => _valueHandler;
+
+ public object? Read(Type baseType, string path, object? defaultValue)
+ {
+ if(!_keyToValue.TryGetValue(path, out _))
+ {
+ var v = new LazyVar