Skip to content

Commit 764b92b

Browse files
Add support for dictionaries and common collection interfaces
1 parent 0ad2ebe commit 764b92b

File tree

3 files changed

+502
-44
lines changed

3 files changed

+502
-44
lines changed

src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs

Lines changed: 190 additions & 40 deletions
Original file line numberDiff line numberDiff line change
@@ -46,13 +46,16 @@ public ObjectArgumentValue(IConfigurationSection section, IReadOnlyCollection<As
4646
if (toType.IsArray)
4747
return CreateArray();
4848

49+
// Only build ctor expression when type is explicitly specified in _section
50+
if (TryBuildCtorExpression(_section, resolutionContext, out var ctorExpression))
51+
return RunCtorExpression(ctorExpression);
52+
4953
if (IsContainer(toType, out var elementType) && TryCreateContainer(out var container))
5054
return container;
5155

52-
if (TryBuildCtorExpression(_section, toType, resolutionContext, out var ctorExpression))
53-
{
54-
return Expression.Lambda<Func<object>>(ctorExpression).Compile().Invoke();
55-
}
56+
// Without a type explicitly specified, attempt to create ctor expression of toType
57+
if (TryBuildCtorExpression(_section, toType, resolutionContext, out ctorExpression))
58+
return RunCtorExpression(ctorExpression);
5659

5760
// MS Config binding can work with a limited set of primitive types and collections
5861
return _section.Get(toType);
@@ -76,31 +79,46 @@ bool TryCreateContainer([NotNullWhen(true)] out object? result)
7679
{
7780
result = null;
7881

79-
if (toType.GetConstructor(Type.EmptyTypes) == null)
80-
return false;
81-
82-
if (!HasAddMethod(toType, elementType, out var addMethod))
83-
return false;
82+
if (IsConstructableDictionary(toType, elementType, out var concreteType, out var addMethod))
83+
{
84+
result = Activator.CreateInstance(concreteType) ?? throw new InvalidOperationException($"Activator.CreateInstance returned null for {concreteType}");
8485

85-
var configurationElements = _section.GetChildren().ToArray();
86-
result = Activator.CreateInstance(toType) ?? throw new InvalidOperationException($"Activator.CreateInstance returned null for {toType}");
86+
foreach (var section in _section.GetChildren())
87+
{
88+
var argumentValue = ConfigurationReader.GetArgumentValue(section, _configurationAssemblies);
89+
var value = argumentValue.ConvertTo(elementType, resolutionContext);
90+
addMethod.Invoke(result, new[] { section.Key, value });
91+
}
92+
return true;
93+
}
94+
else if (IsConstructableContainer(toType, elementType, out concreteType, out addMethod))
95+
{
96+
result = Activator.CreateInstance(concreteType) ?? throw new InvalidOperationException($"Activator.CreateInstance returned null for {concreteType}");
8797

88-
for (int i = 0; i < configurationElements.Length; ++i)
98+
foreach (var section in _section.GetChildren())
99+
{
100+
var argumentValue = ConfigurationReader.GetArgumentValue(section, _configurationAssemblies);
101+
var value = argumentValue.ConvertTo(elementType, resolutionContext);
102+
addMethod.Invoke(result, new[] { value });
103+
}
104+
return true;
105+
}
106+
else
89107
{
90-
var argumentValue = ConfigurationReader.GetArgumentValue(configurationElements[i], _configurationAssemblies);
91-
var value = argumentValue.ConvertTo(elementType, resolutionContext);
92-
addMethod.Invoke(result, new[] { value });
108+
return false;
93109
}
94-
95-
return true;
96110
}
97111
}
98112

99-
internal static bool TryBuildCtorExpression(
100-
IConfigurationSection section, Type parameterType, ResolutionContext resolutionContext, [NotNullWhen(true)] out NewExpression? ctorExpression)
113+
static object RunCtorExpression(NewExpression ctorExpression)
101114
{
102-
ctorExpression = null;
115+
Expression body = ctorExpression.Type.IsValueType ? Expression.Convert(ctorExpression, typeof(object)) : ctorExpression;
116+
return Expression.Lambda<Func<object>>(body).Compile().Invoke();
117+
}
103118

119+
internal static bool TryBuildCtorExpression(
120+
IConfigurationSection section, ResolutionContext resolutionContext, [NotNullWhen(true)] out NewExpression? ctorExpression)
121+
{
104122
var typeDirective = section.GetValue<string>("$type") switch
105123
{
106124
not null => "$type",
@@ -114,16 +132,35 @@ internal static bool TryBuildCtorExpression(
114132
var type = typeDirective switch
115133
{
116134
not null => Type.GetType(section.GetValue<string>(typeDirective)!, throwOnError: false),
117-
null => parameterType,
135+
null => null,
118136
};
119137

120138
if (type is null or { IsAbstract: true })
121139
{
140+
ctorExpression = null;
122141
return false;
123142
}
143+
else
144+
{
145+
var suppliedArguments = section.GetChildren().Where(s => s.Key != typeDirective)
146+
.ToDictionary(s => s.Key, StringComparer.OrdinalIgnoreCase);
147+
return TryBuildCtorExpression(type, suppliedArguments, resolutionContext, out ctorExpression);
148+
}
149+
150+
}
124151

125-
var suppliedArguments = section.GetChildren().Where(s => s.Key != typeDirective)
152+
internal static bool TryBuildCtorExpression(
153+
IConfigurationSection section, Type parameterType, ResolutionContext resolutionContext, [NotNullWhen(true)] out NewExpression? ctorExpression)
154+
{
155+
var suppliedArguments = section.GetChildren()
126156
.ToDictionary(s => s.Key, StringComparer.OrdinalIgnoreCase);
157+
return TryBuildCtorExpression(parameterType, suppliedArguments, resolutionContext, out ctorExpression);
158+
}
159+
160+
static bool TryBuildCtorExpression(
161+
Type type, Dictionary<string, IConfigurationSection> suppliedArguments, ResolutionContext resolutionContext, [NotNullWhen(true)] out NewExpression? ctorExpression)
162+
{
163+
ctorExpression = null;
127164

128165
if (suppliedArguments.Count == 0 &&
129166
type.GetConstructor(Type.EmptyTypes) is ConstructorInfo parameterlessCtor)
@@ -219,27 +256,53 @@ static bool TryBindToCtorArgument(object value, Type type, ResolutionContext res
219256
argumentExpression = Expression.NewArrayInit(elementType, elements);
220257
return true;
221258
}
222-
else if (IsContainer(type, out elementType) && type.GetConstructor(Type.EmptyTypes) is not null && HasAddMethod(type, elementType, out var addMethod))
259+
if (TryBuildCtorExpression(s, type, resolutionContext, out var ctorExpression))
223260
{
224-
var elements = new List<Expression>();
225-
foreach (var element in s.GetChildren())
261+
if (ctorExpression.Type.IsValueType && !type.IsValueType)
226262
{
227-
if (TryBindToCtorArgument(element, elementType, resolutionContext, out var elementExpression))
263+
argumentExpression = Expression.Convert(ctorExpression, type);
264+
}
265+
else {
266+
argumentExpression = ctorExpression;
267+
}
268+
return true;
269+
}
270+
if (IsContainer(type, out elementType))
271+
{
272+
if (IsConstructableDictionary(type, elementType, out var concreteType, out var addMethod))
273+
{
274+
var elements = new List<ElementInit>();
275+
foreach (var element in s.GetChildren())
228276
{
229-
elements.Add(elementExpression);
277+
if (TryBindToCtorArgument(element, elementType, resolutionContext, out var elementExpression))
278+
{
279+
elements.Add(Expression.ElementInit(addMethod, Expression.Constant(element.Key), elementExpression));
280+
}
281+
else
282+
{
283+
return false;
284+
}
230285
}
231-
else
286+
argumentExpression = Expression.ListInit(Expression.New(concreteType), elements);
287+
return true;
288+
}
289+
if (IsConstructableContainer(type, elementType, out concreteType, out addMethod))
290+
{
291+
var elements = new List<Expression>();
292+
foreach (var element in s.GetChildren())
232293
{
233-
return false;
294+
if (TryBindToCtorArgument(element, elementType, resolutionContext, out var elementExpression))
295+
{
296+
elements.Add(elementExpression);
297+
}
298+
else
299+
{
300+
return false;
301+
}
234302
}
303+
argumentExpression = Expression.ListInit(Expression.New(concreteType), addMethod, elements);
304+
return true;
235305
}
236-
argumentExpression = Expression.ListInit(Expression.New(type), addMethod, elements);
237-
return true;
238-
}
239-
else if (TryBuildCtorExpression(s, type, resolutionContext, out var ctorExpression))
240-
{
241-
argumentExpression = ctorExpression;
242-
return true;
243306
}
244307

245308
return false;
@@ -254,6 +317,11 @@ static bool TryBindToCtorArgument(object value, Type type, ResolutionContext res
254317
static bool IsContainer(Type type, [NotNullWhen(true)] out Type? elementType)
255318
{
256319
elementType = null;
320+
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>))
321+
{
322+
elementType = type.GetGenericArguments()[0];
323+
return true;
324+
}
257325
foreach (var iface in type.GetInterfaces())
258326
{
259327
if (iface.IsGenericType)
@@ -269,10 +337,92 @@ static bool IsContainer(Type type, [NotNullWhen(true)] out Type? elementType)
269337
return false;
270338
}
271339

272-
static bool HasAddMethod(Type type, Type elementType, [NotNullWhen(true)] out MethodInfo? addMethod)
340+
static bool IsConstructableDictionary(Type type, Type elementType, [NotNullWhen(true)] out Type? concreteType, [NotNullWhen(true)] out MethodInfo? addMethod)
341+
{
342+
concreteType = null;
343+
addMethod = null;
344+
if (!elementType.IsGenericType || elementType.GetGenericTypeDefinition() != typeof(KeyValuePair<,>))
345+
{
346+
return false;
347+
}
348+
var argumentTypes = elementType.GetGenericArguments();
349+
if (argumentTypes[0] != typeof(string))
350+
{
351+
return false;
352+
}
353+
if (!typeof(IDictionary<,>).MakeGenericType(argumentTypes).IsAssignableFrom(type)
354+
&& !typeof(IReadOnlyDictionary<,>).MakeGenericType(argumentTypes).IsAssignableFrom(type))
355+
{
356+
return false;
357+
}
358+
if (type.IsAbstract)
359+
{
360+
concreteType = typeof(Dictionary<,>).MakeGenericType(argumentTypes);
361+
if (!type.IsAssignableFrom(concreteType))
362+
{
363+
return false;
364+
}
365+
}
366+
else
367+
{
368+
concreteType = type;
369+
}
370+
if (concreteType.GetConstructor(Type.EmptyTypes) == null)
371+
{
372+
return false;
373+
}
374+
var valueType = argumentTypes[1];
375+
foreach (var method in concreteType.GetMethods())
376+
{
377+
if (!method.IsStatic && method.Name == "Add")
378+
{
379+
var parameters = method.GetParameters();
380+
if (parameters.Length == 2 && parameters[0].ParameterType == typeof(string) && parameters[1].ParameterType == valueType)
381+
{
382+
addMethod = method;
383+
return true;
384+
}
385+
}
386+
}
387+
return false;
388+
}
389+
390+
static bool IsConstructableContainer(Type type, Type elementType, [NotNullWhen(true)] out Type? concreteType, [NotNullWhen(true)] out MethodInfo? addMethod)
273391
{
274-
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers#collection-initializers
275-
addMethod = type.GetMethods().FirstOrDefault(m => !m.IsStatic && m.Name == "Add" && m.GetParameters().Length == 1 && m.GetParameters()[0].ParameterType == elementType);
276-
return addMethod is not null;
392+
addMethod = null;
393+
if (type.IsAbstract)
394+
{
395+
concreteType = typeof(List<>).MakeGenericType(elementType);
396+
if (!type.IsAssignableFrom(concreteType))
397+
{
398+
concreteType = typeof(HashSet<>).MakeGenericType(elementType);
399+
if (!type.IsAssignableFrom(concreteType))
400+
{
401+
concreteType = null;
402+
return false;
403+
}
404+
}
405+
}
406+
else
407+
{
408+
concreteType = type;
409+
}
410+
if (concreteType.GetConstructor(Type.EmptyTypes) == null)
411+
{
412+
return false;
413+
}
414+
foreach (var method in concreteType.GetMethods())
415+
{
416+
if (!method.IsStatic && method.Name == "Add")
417+
{
418+
var parameters = method.GetParameters();
419+
if (parameters.Length == 1 && parameters[0].ParameterType == elementType)
420+
{
421+
addMethod = method;
422+
return true;
423+
}
424+
}
425+
}
426+
return false;
277427
}
278428
}

0 commit comments

Comments
 (0)