Sicherung
This commit is contained in:
204
Config.Net/Core/Box/BoxFactory.cs
Normal file
204
Config.Net/Core/Box/BoxFactory.cs
Normal file
@@ -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<string, ResultBox> Discover(Type t, ValueHandler valueHandler, string? basePath)
|
||||
{
|
||||
var result = new Dictionary<string, ResultBox>();
|
||||
|
||||
DiscoverProperties(t, valueHandler, result, basePath);
|
||||
|
||||
DiscoverMethods(t, valueHandler, result);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void DiscoverProperties(Type t, ValueHandler valueHandler, Dictionary<string, ResultBox> result, string? basePath)
|
||||
{
|
||||
IEnumerable<PropertyInfo> 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<string, ResultBox> result)
|
||||
{
|
||||
TypeInfo ti = t.GetTypeInfo();
|
||||
|
||||
IEnumerable<MethodInfo> 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<OptionAttribute>(),
|
||||
pi.GetCustomAttribute<DefaultValueAttribute>());
|
||||
}
|
||||
|
||||
private static void AddAttributes(ResultBox box, MethodInfo mi, ValueHandler valueHandler)
|
||||
{
|
||||
AddAttributes(box, valueHandler, mi.GetCustomAttribute<OptionAttribute>(), mi.GetCustomAttribute<DefaultValueAttribute>());
|
||||
}
|
||||
|
||||
|
||||
private static void AddAttributes(ResultBox box, ValueHandler valueHandler, params Attribute?[] attributes)
|
||||
{
|
||||
OptionAttribute? optionAttribute = attributes.OfType<OptionAttribute>().FirstOrDefault();
|
||||
DefaultValueAttribute? defaultValueAttribute = attributes.OfType<DefaultValueAttribute>().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<PropertyInfo>();
|
||||
|
||||
var considered = new List<TypeInfo>();
|
||||
var queue = new Queue<TypeInfo>();
|
||||
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<PropertyInfo> newProperties = typeInfo.DeclaredProperties.Where(p => !propertyInfos.Contains(p));
|
||||
propertyInfos.InsertRange(0, newProperties);
|
||||
}
|
||||
|
||||
return propertyInfos.ToArray();
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
109
Config.Net/Core/Box/CollectionResultBox.cs
Normal file
109
Config.Net/Core/Box/CollectionResultBox.cs
Normal file
@@ -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<T> : IEnumerable<T>
|
||||
{
|
||||
private readonly int _count;
|
||||
private readonly CollectionResultBox _parent;
|
||||
|
||||
public DynamicEnumerable(int count, CollectionResultBox parent)
|
||||
{
|
||||
_count = count;
|
||||
_parent = parent;
|
||||
}
|
||||
|
||||
public IEnumerator<T> GetEnumerator()
|
||||
{
|
||||
return new DynamicEnumerator<T>(_count, _parent);
|
||||
}
|
||||
|
||||
IEnumerator IEnumerable.GetEnumerator()
|
||||
{
|
||||
return new DynamicEnumerator<T>(_count, _parent);
|
||||
}
|
||||
}
|
||||
|
||||
private class DynamicEnumerator<T> : IEnumerator<T>
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
87
Config.Net/Core/Box/MethodResultBox.cs
Normal file
87
Config.Net/Core/Box/MethodResultBox.cs
Normal file
@@ -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; }
|
||||
|
||||
/// <summary>
|
||||
/// Composes a uniqueue method name using method name itself and parameter type names, separated by underscore
|
||||
/// </summary>
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
51
Config.Net/Core/Box/PropertyResultBox.cs
Normal file
51
Config.Net/Core/Box/PropertyResultBox.cs
Normal file
@@ -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_");
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
34
Config.Net/Core/Box/ProxyResultBox.cs
Normal file
34
Config.Net/Core/Box/ProxyResultBox.cs
Normal file
@@ -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<int, object> _indexToProxyInstance = new Dictionary<int, object>();
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
73
Config.Net/Core/Box/ResultBox.cs
Normal file
73
Config.Net/Core/Box/ResultBox.cs
Normal file
@@ -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
|
||||
}
|
||||
}
|
||||
74
Config.Net/Core/DynamicReader.cs
Normal file
74
Config.Net/Core/DynamicReader.cs
Normal file
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
48
Config.Net/Core/DynamicWriter.cs
Normal file
48
Config.Net/Core/DynamicWriter.cs
Normal file
@@ -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");
|
||||
}
|
||||
}
|
||||
}
|
||||
16
Config.Net/Core/Extensions.cs
Normal file
16
Config.Net/Core/Extensions.cs
Normal file
@@ -0,0 +1,16 @@
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace Config.Net.Core
|
||||
{
|
||||
static class Extensions
|
||||
{
|
||||
public static TValue? GetValueOrDefaultInternal<TKey, TValue>(this IDictionary<TKey, TValue> dictionary, TKey key)
|
||||
where TKey: notnull
|
||||
where TValue: class
|
||||
{
|
||||
if (!dictionary.TryGetValue(key, out TValue? value)) return default(TValue);
|
||||
|
||||
return value;
|
||||
}
|
||||
}
|
||||
}
|
||||
58
Config.Net/Core/FlatArrays.cs
Normal file
58
Config.Net/Core/FlatArrays.cs
Normal file
@@ -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
|
||||
{
|
||||
/// <summary>
|
||||
/// Helper class to implement flat arrays
|
||||
/// </summary>
|
||||
public static class FlatArrays
|
||||
{
|
||||
public static bool IsArrayLength(string? key, Func<string, string?> 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<string, string?> 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
103
Config.Net/Core/InterfaceInterceptor.cs
Normal file
103
Config.Net/Core/InterfaceInterceptor.cs
Normal file
@@ -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<string, ResultBox> _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));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
70
Config.Net/Core/IoHandler.cs
Normal file
70
Config.Net/Core/IoHandler.cs
Normal file
@@ -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<IConfigStore> _stores;
|
||||
private readonly ValueHandler _valueHandler;
|
||||
private readonly TimeSpan _cacheInterval;
|
||||
private readonly ConcurrentDictionary<string, LazyVar<object>> _keyToValue = new ConcurrentDictionary<string, LazyVar<object>>();
|
||||
|
||||
public IoHandler(IEnumerable<IConfigStore> 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<object>(_cacheInterval, () => ReadNonCached(baseType, path, defaultValue));
|
||||
_keyToValue[path] = v;
|
||||
return v.GetValue();
|
||||
}
|
||||
|
||||
return _keyToValue[path].GetValue();
|
||||
}
|
||||
|
||||
public void Write(Type baseType, string path, object? value)
|
||||
{
|
||||
string? valueToWrite = _valueHandler.ConvertValue(baseType, value);
|
||||
|
||||
foreach (IConfigStore store in _stores.Where(s => s.CanWrite))
|
||||
{
|
||||
store.Write(path, valueToWrite);
|
||||
}
|
||||
}
|
||||
|
||||
private object? ReadNonCached(Type baseType, string path, object? defaultValue)
|
||||
{
|
||||
string? rawValue = ReadFirstValue(path);
|
||||
|
||||
return _valueHandler.ParseValue(baseType, rawValue, defaultValue);
|
||||
}
|
||||
|
||||
private string? ReadFirstValue(string key)
|
||||
{
|
||||
foreach (IConfigStore store in _stores)
|
||||
{
|
||||
if (store.CanRead)
|
||||
{
|
||||
string? value = store.Read(key);
|
||||
|
||||
if (value != null) return value;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
96
Config.Net/Core/LazyVar.cs
Normal file
96
Config.Net/Core/LazyVar.cs
Normal file
@@ -0,0 +1,96 @@
|
||||
using System;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace Config.Net.Core
|
||||
{
|
||||
/// <summary>
|
||||
/// Implements a lazy value i.e. that can expire in future
|
||||
/// </summary>
|
||||
/// <typeparam name="T"></typeparam>
|
||||
class LazyVar<T> where T : class
|
||||
{
|
||||
private readonly Func<Task<T?>>? _renewFuncAsync;
|
||||
private readonly Func<T?>? _renewFunc;
|
||||
private DateTime _lastRenewed = DateTime.MinValue;
|
||||
private readonly TimeSpan _timeToLive;
|
||||
private T? _value;
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of a lazy variable with time-to-live value
|
||||
/// </summary>
|
||||
/// <param name="timeToLive">Time to live. Setting to <see cref="TimeSpan.Zero"/> disables caching completely</param>
|
||||
/// <param name="renewFunc"></param>
|
||||
public LazyVar(TimeSpan timeToLive, Func<Task<T?>> renewFunc)
|
||||
{
|
||||
_timeToLive = timeToLive;
|
||||
_renewFuncAsync = renewFunc ?? throw new ArgumentNullException(nameof(renewFunc));
|
||||
_renewFunc = null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Creates an instance of a lazy variable with time-to-live value
|
||||
/// </summary>
|
||||
/// <param name="timeToLive">Time to live. Setting to <see cref="TimeSpan.Zero"/> disables caching completely</param>
|
||||
/// <param name="renewFunc"></param>
|
||||
public LazyVar(TimeSpan timeToLive, Func<T?> renewFunc)
|
||||
{
|
||||
_timeToLive = timeToLive;
|
||||
_renewFuncAsync = null;
|
||||
_renewFunc = renewFunc ?? throw new ArgumentNullException(nameof(renewFunc));
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the values, renewing it if necessary
|
||||
/// </summary>
|
||||
/// <returns>Value</returns>
|
||||
public async Task<T?> GetValueAsync()
|
||||
{
|
||||
if (_renewFuncAsync == null)
|
||||
{
|
||||
throw new InvalidOperationException("cannot renew value, async delegate is not specified");
|
||||
}
|
||||
|
||||
if (_timeToLive == TimeSpan.Zero)
|
||||
{
|
||||
return await _renewFuncAsync();
|
||||
}
|
||||
|
||||
bool expired = (DateTime.UtcNow - _lastRenewed) > _timeToLive;
|
||||
|
||||
if (expired)
|
||||
{
|
||||
_value = await _renewFuncAsync();
|
||||
_lastRenewed = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
return _value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the values, renewing it if necessary
|
||||
/// </summary>
|
||||
/// <returns>Value</returns>
|
||||
public T? GetValue()
|
||||
{
|
||||
if (_renewFunc == null)
|
||||
{
|
||||
throw new InvalidOperationException("cannot renew value, synchronous delegate is not specified");
|
||||
}
|
||||
|
||||
if (_timeToLive == TimeSpan.Zero)
|
||||
{
|
||||
return _renewFunc();
|
||||
}
|
||||
|
||||
bool expired = (DateTime.UtcNow - _lastRenewed) > _timeToLive;
|
||||
|
||||
if (expired)
|
||||
{
|
||||
_value = _renewFunc();
|
||||
_lastRenewed = DateTime.UtcNow;
|
||||
}
|
||||
|
||||
return _value;
|
||||
}
|
||||
}
|
||||
}
|
||||
88
Config.Net/Core/OptionPath.cs
Normal file
88
Config.Net/Core/OptionPath.cs
Normal file
@@ -0,0 +1,88 @@
|
||||
namespace Config.Net.Core
|
||||
{
|
||||
public static class OptionPath
|
||||
{
|
||||
public const string Separator = ".";
|
||||
private const string IndexOpen = "[";
|
||||
private const string IndexClose = "]";
|
||||
public const string LengthFunction = ".$l";
|
||||
|
||||
public static string Combine(params string?[] parts)
|
||||
{
|
||||
return Combine(-1, parts);
|
||||
}
|
||||
|
||||
public static string AddLength(string path)
|
||||
{
|
||||
return path + LengthFunction;
|
||||
}
|
||||
|
||||
public static bool TryStripLength(string? path, out string? noLengthPath)
|
||||
{
|
||||
if (path == null)
|
||||
{
|
||||
noLengthPath = path;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!path.EndsWith(LengthFunction))
|
||||
{
|
||||
noLengthPath = path;
|
||||
return false;
|
||||
}
|
||||
|
||||
noLengthPath = path.Substring(0, path.Length - LengthFunction.Length);
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// For indexed paths like "creds[1]" strips index part so it becomes:
|
||||
/// - noIndexPath: "creds"
|
||||
/// - index: 1
|
||||
///
|
||||
/// If path is not indexed returns false and noIndexPath is equal to path itself
|
||||
/// </summary>
|
||||
public static bool TryStripIndex(string? path, out string? noIndexPath, out int index)
|
||||
{
|
||||
if (path == null)
|
||||
{
|
||||
index = 0;
|
||||
noIndexPath = path;
|
||||
return false;
|
||||
}
|
||||
|
||||
int openIdx = path.IndexOf(IndexOpen);
|
||||
int closeIdx = path.IndexOf(IndexClose);
|
||||
|
||||
if (openIdx == -1 || closeIdx == -1 || openIdx > closeIdx || closeIdx != path.Length - 1)
|
||||
{
|
||||
noIndexPath = path;
|
||||
index = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
noIndexPath = path.Substring(0, openIdx);
|
||||
int.TryParse(path.Substring(openIdx + 1, closeIdx - openIdx - 1), out index);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static string Combine(int index, params string?[] parts)
|
||||
{
|
||||
string s = string.Empty;
|
||||
|
||||
for (int i = 0; i < parts.Length; i++)
|
||||
{
|
||||
if (s.Length > 0) s += Separator;
|
||||
|
||||
if (!string.IsNullOrEmpty(parts[i])) s += parts[i];
|
||||
}
|
||||
|
||||
if (index != -1)
|
||||
{
|
||||
s = $"{s}{IndexOpen}{index}{IndexClose}";
|
||||
}
|
||||
|
||||
return s;
|
||||
}
|
||||
}
|
||||
}
|
||||
157
Config.Net/Core/ValueHandler.cs
Normal file
157
Config.Net/Core/ValueHandler.cs
Normal file
@@ -0,0 +1,157 @@
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using Config.Net.TypeParsers;
|
||||
|
||||
namespace Config.Net.Core
|
||||
{
|
||||
class ValueHandler
|
||||
{
|
||||
private readonly DefaultParser _defaultParser = new DefaultParser();
|
||||
private readonly ConcurrentDictionary<Type, ITypeParser> _allParsers = new ConcurrentDictionary<Type, ITypeParser>();
|
||||
private readonly HashSet<Type> _supportedTypes = new HashSet<Type>();
|
||||
|
||||
private static readonly ValueHandler _default = new ValueHandler();
|
||||
|
||||
public ValueHandler(IEnumerable<ITypeParser>? customParsers = null)
|
||||
{
|
||||
foreach (ITypeParser pc in GetBuiltInParsers())
|
||||
{
|
||||
AddParser(pc);
|
||||
}
|
||||
|
||||
if(customParsers != null)
|
||||
{
|
||||
foreach(ITypeParser pc in customParsers)
|
||||
{
|
||||
AddParser(pc);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void AddParser(ITypeParser parser)
|
||||
{
|
||||
if (parser == null)
|
||||
{
|
||||
throw new ArgumentNullException(nameof(parser));
|
||||
}
|
||||
|
||||
foreach (Type t in parser.SupportedTypes)
|
||||
{
|
||||
_allParsers[t] = parser;
|
||||
_supportedTypes.Add(t);
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsSupported(Type t)
|
||||
{
|
||||
return _supportedTypes.Contains(t) || _defaultParser.IsSupported(t);
|
||||
}
|
||||
|
||||
public object? ParseValue(Type baseType, string? rawValue, object? defaultValue)
|
||||
{
|
||||
object? result;
|
||||
|
||||
if (rawValue == null)
|
||||
{
|
||||
result = defaultValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
if(!TryParse(baseType, rawValue, out result))
|
||||
{
|
||||
result = defaultValue;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
public bool TryParse(Type propertyType, string? rawValue, out object? result)
|
||||
{
|
||||
if (_defaultParser.IsSupported(propertyType)) //type here must be a non-nullable one
|
||||
{
|
||||
if (!_defaultParser.TryParse(rawValue, propertyType, out result))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
ITypeParser? typeParser = GetParser(propertyType);
|
||||
if (typeParser == null)
|
||||
{
|
||||
result = null;
|
||||
return false;
|
||||
}
|
||||
if (!typeParser.TryParse(rawValue, propertyType, out result))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public string? ConvertValue(Type baseType, object? value)
|
||||
{
|
||||
string? str;
|
||||
|
||||
if (value == null)
|
||||
{
|
||||
str = null;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_defaultParser.IsSupported(baseType))
|
||||
{
|
||||
str = _defaultParser.ToRawString(value);
|
||||
}
|
||||
else
|
||||
{
|
||||
ITypeParser? parser = GetParser(value.GetType());
|
||||
str = parser?.ToRawString(value);
|
||||
}
|
||||
}
|
||||
|
||||
return str;
|
||||
}
|
||||
|
||||
private ITypeParser? GetParser(Type t)
|
||||
{
|
||||
ITypeParser? result;
|
||||
_allParsers.TryGetValue(t, out result);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Scans assembly for types implementing <see cref="ITypeParser"/> and builds Type => instance dictionary.
|
||||
/// Not sure if I should use reflection here, however the assembly is small and this shouldn't cause any
|
||||
/// performance issues
|
||||
/// </summary>
|
||||
/// <returns></returns>
|
||||
private static IEnumerable<ITypeParser> GetBuiltInParsers()
|
||||
{
|
||||
return new ITypeParser[]
|
||||
{
|
||||
new FloatParser(),
|
||||
new DoubleParser(),
|
||||
new DecimalParser(),
|
||||
new SByteParser(),
|
||||
new ShortParser(),
|
||||
new IntParser(),
|
||||
new LongParser(),
|
||||
new ByteParser(),
|
||||
new UShortParser(),
|
||||
new UIntParser(),
|
||||
new ULongParser(),
|
||||
new StringArrayParser(),
|
||||
new StringParser(),
|
||||
new TimeSpanParser(),
|
||||
new CoreParsers(),
|
||||
new NetworkCredentialParser()
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user