Skip to content

Commit 04d694d

Browse files
committed
Use consistent binding logic for nested properties in object argument values
1 parent 76f8d5e commit 04d694d

File tree

6 files changed

+170
-104
lines changed

6 files changed

+170
-104
lines changed
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System.Reflection;
2+
using Microsoft.Extensions.Configuration;
3+
4+
namespace Serilog.Settings.Configuration;
5+
6+
abstract class ConfigurationArgumentValue
7+
{
8+
public abstract object? ConvertTo(Type toType, ResolutionContext resolutionContext);
9+
10+
public static ConfigurationArgumentValue FromSection(IConfigurationSection argumentSection, IReadOnlyCollection<Assembly> configurationAssemblies)
11+
{
12+
ConfigurationArgumentValue argumentValue;
13+
14+
// Reject configurations where an element has both scalar and complex
15+
// values as a result of reading multiple configuration sources.
16+
if (argumentSection.Value != null && argumentSection.GetChildren().Any())
17+
throw new InvalidOperationException(
18+
$"The value for the argument '{argumentSection.Path}' is assigned different value " +
19+
"types in more than one configuration source. Ensure all configurations consistently " +
20+
"use either a scalar (int, string, boolean) or a complex (array, section, list, " +
21+
"POCO, etc.) type for this argument value.");
22+
23+
if (argumentSection.Value != null)
24+
{
25+
argumentValue = new StringArgumentValue(argumentSection.Value);
26+
}
27+
else
28+
{
29+
argumentValue = new ObjectArgumentValue(argumentSection, configurationAssemblies);
30+
}
31+
32+
return argumentValue;
33+
}
34+
}

src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs

Lines changed: 4 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -303,14 +303,14 @@ void ApplyEnrichment(LoggerConfiguration loggerConfiguration)
303303
}
304304
}
305305

306-
internal ILookup<string, Dictionary<string, IConfigurationArgumentValue>> GetMethodCalls(IConfiguration directive)
306+
internal ILookup<string, Dictionary<string, ConfigurationArgumentValue>> GetMethodCalls(IConfiguration directive)
307307
{
308308
var children = directive.GetChildren().ToList();
309309

310310
var result =
311311
(from child in children
312312
where child.Value != null // Plain string
313-
select new { Name = child.Value, Args = new Dictionary<string, IConfigurationArgumentValue>() })
313+
select new { Name = child.Value, Args = new Dictionary<string, ConfigurationArgumentValue>() })
314314
.Concat(
315315
(from child in children
316316
where child.Value == null
@@ -319,7 +319,7 @@ internal ILookup<string, Dictionary<string, IConfigurationArgumentValue>> GetMet
319319
select new
320320
{
321321
Name = argument.Key,
322-
Value = GetArgumentValue(argument, _configurationAssemblies)
322+
Value = ConfigurationArgumentValue.FromSection(argument, _configurationAssemblies)
323323
}).ToDictionary(p => p.Name, p => p.Value)
324324
select new { Name = name, Args = callArgs }))
325325
.ToLookup(p => p.Name, p => p.Args);
@@ -336,31 +336,6 @@ static string GetSectionName(IConfigurationSection s)
336336
}
337337
}
338338

339-
internal static IConfigurationArgumentValue GetArgumentValue(IConfigurationSection argumentSection, IReadOnlyCollection<Assembly> configurationAssemblies)
340-
{
341-
IConfigurationArgumentValue argumentValue;
342-
343-
// Reject configurations where an element has both scalar and complex
344-
// values as a result of reading multiple configuration sources.
345-
if (argumentSection.Value != null && argumentSection.GetChildren().Any())
346-
throw new InvalidOperationException(
347-
$"The value for the argument '{argumentSection.Path}' is assigned different value " +
348-
"types in more than one configuration source. Ensure all configurations consistently " +
349-
"use either a scalar (int, string, boolean) or a complex (array, section, list, " +
350-
"POCO, etc.) type for this argument value.");
351-
352-
if (argumentSection.Value != null)
353-
{
354-
argumentValue = new StringArgumentValue(argumentSection.Value);
355-
}
356-
else
357-
{
358-
argumentValue = new ObjectArgumentValue(argumentSection, configurationAssemblies);
359-
}
360-
361-
return argumentValue;
362-
}
363-
364339
static IReadOnlyCollection<Assembly> LoadConfigurationAssemblies(IConfiguration section, AssemblyFinder assemblyFinder)
365340
{
366341
var serilogAssembly = typeof(ILogger).Assembly;
@@ -404,7 +379,7 @@ This is most likely because the application is published as single-file.
404379
return assemblies;
405380
}
406381

407-
void CallConfigurationMethods(ILookup<string, Dictionary<string, IConfigurationArgumentValue>> methods, IReadOnlyCollection<MethodInfo> configurationMethods, object receiver)
382+
void CallConfigurationMethods(ILookup<string, Dictionary<string, ConfigurationArgumentValue>> methods, IReadOnlyCollection<MethodInfo> configurationMethods, object receiver)
408383
{
409384
foreach (var method in methods.SelectMany(g => g.Select(x => new { g.Key, Value = x })))
410385
{

src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationArgumentValue.cs

Lines changed: 0 additions & 6 deletions
This file was deleted.

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

Lines changed: 81 additions & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -4,10 +4,11 @@
44
using Microsoft.Extensions.Configuration;
55

66
using Serilog.Configuration;
7+
using Serilog.Debugging;
78

89
namespace Serilog.Settings.Configuration;
910

10-
class ObjectArgumentValue : IConfigurationArgumentValue
11+
class ObjectArgumentValue : ConfigurationArgumentValue
1112
{
1213
readonly IConfigurationSection _section;
1314
readonly IReadOnlyCollection<Assembly> _configurationAssemblies;
@@ -20,15 +21,15 @@ public ObjectArgumentValue(IConfigurationSection section, IReadOnlyCollection<As
2021
_configurationAssemblies = configurationAssemblies ?? throw new ArgumentNullException(nameof(configurationAssemblies));
2122
}
2223

23-
public object? ConvertTo(Type toType, ResolutionContext resolutionContext)
24+
public override object? ConvertTo(Type toType, ResolutionContext resolutionContext)
2425
{
2526
// return the entire section for internal processing
2627
if (toType == typeof(IConfigurationSection)) return _section;
2728

2829
// process a nested configuration to populate an Action<> logger/sink config parameter?
2930
var typeInfo = toType.GetTypeInfo();
3031
if (typeInfo.IsGenericType &&
31-
typeInfo.GetGenericTypeDefinition() is Type genericType && genericType == typeof(Action<>))
32+
typeInfo.GetGenericTypeDefinition() is {} genericType && genericType == typeof(Action<>))
3233
{
3334
var configType = typeInfo.GenericTypeArguments[0];
3435
IConfigurationReader configReader = new ConfigurationReader(_section, _configurationAssemblies, resolutionContext);
@@ -64,9 +65,9 @@ object CreateArray()
6465
var arrayElementType = toType.GetElementType()!;
6566
var configurationElements = _section.GetChildren().ToArray();
6667
var array = Array.CreateInstance(arrayElementType, configurationElements.Length);
67-
for (int i = 0; i < configurationElements.Length; ++i)
68+
for (var i = 0; i < configurationElements.Length; ++i)
6869
{
69-
var argumentValue = ConfigurationReader.GetArgumentValue(configurationElements[i], _configurationAssemblies);
70+
var argumentValue = FromSection(configurationElements[i], _configurationAssemblies);
7071
var value = argumentValue.ConvertTo(arrayElementType, resolutionContext);
7172
array.SetValue(value, i);
7273
}
@@ -84,29 +85,28 @@ bool TryCreateContainer([NotNullWhen(true)] out object? result)
8485

8586
foreach (var section in _section.GetChildren())
8687
{
87-
var argumentValue = ConfigurationReader.GetArgumentValue(section, _configurationAssemblies);
88+
var argumentValue = FromSection(section, _configurationAssemblies);
8889
var key = new StringArgumentValue(section.Key).ConvertTo(keyType, resolutionContext);
8990
var value = argumentValue.ConvertTo(valueType, resolutionContext);
90-
addMethod.Invoke(result, new[] { key, value });
91+
addMethod.Invoke(result, [key, value]);
9192
}
9293
return true;
9394
}
94-
else if (IsConstructableContainer(toType, elementType, out concreteType, out addMethod))
95+
96+
if (IsConstructableContainer(toType, elementType, out concreteType, out addMethod))
9597
{
9698
result = Activator.CreateInstance(concreteType) ?? throw new InvalidOperationException($"Activator.CreateInstance returned null for {concreteType}");
9799

98100
foreach (var section in _section.GetChildren())
99101
{
100-
var argumentValue = ConfigurationReader.GetArgumentValue(section, _configurationAssemblies);
102+
var argumentValue = FromSection(section, _configurationAssemblies);
101103
var value = argumentValue.ConvertTo(elementType, resolutionContext);
102104
addMethod.Invoke(result, new[] { value });
103105
}
104106
return true;
105107
}
106-
else
107-
{
108-
return false;
109-
}
108+
109+
return false;
110110
}
111111
}
112112

@@ -153,58 +153,84 @@ bool TryCallCtorImplicit(
153153

154154
bool TryCallCtor(Type type, Dictionary<string, IConfigurationSection> suppliedArguments, ResolutionContext resolutionContext, [NotNullWhen(true)] out object? value)
155155
{
156-
value = null;
156+
var binding = type.GetConstructors()
157+
.Select(ci =>
158+
{
159+
var args = new List<object?>();
160+
var matches = 0;
161+
var stringMatches = 0;
162+
var suppliedNames = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
163+
foreach (var p in ci.GetParameters())
164+
{
165+
if (suppliedArguments.TryGetValue(p.Name ?? "", out var argValue))
166+
{
167+
args.Add(argValue);
168+
matches += 1;
169+
170+
if (p.ParameterType == typeof(string))
171+
{
172+
stringMatches += 1;
173+
}
174+
175+
if (p.Name != null)
176+
{
177+
suppliedNames.Add(p.Name);
178+
}
179+
}
180+
else
181+
{
182+
if (p.HasDefaultValue)
183+
{
184+
args.Add(p.DefaultValue);
185+
}
186+
else
187+
{
188+
return new { ci, args, isCallable = false, matches, stringMatches, suppliedNames };
189+
}
190+
}
191+
}
157192

158-
if (suppliedArguments.Count == 0 &&
159-
type.GetConstructor(Type.EmptyTypes) is ConstructorInfo parameterlessCtor)
193+
return new { ci, args, isCallable = true, matches, stringMatches, suppliedNames };
194+
})
195+
.Where(binding => binding.isCallable)
196+
.OrderByDescending(binding => binding.matches)
197+
.ThenByDescending(binding => binding.stringMatches)
198+
.ThenBy(binding => binding.args.Count)
199+
.FirstOrDefault();
200+
201+
if (binding == null)
160202
{
161-
value = parameterlessCtor.Invoke([]);
162-
return true;
203+
value = null;
204+
return false;
163205
}
164206

165-
var ctor =
166-
(from c in type.GetConstructors()
167-
from p in c.GetParameters()
168-
let argumentBindResult = suppliedArguments.TryGetValue(p.Name ?? "", out var argValue) switch
169-
{
170-
true => new { success = true, hasMatch = true, value = (object?)argValue },
171-
false => p.HasDefaultValue switch
172-
{
173-
true => new { success = true, hasMatch = false, value = (object?)p.DefaultValue },
174-
false => new { success = false, hasMatch = false, value = (object?)null },
175-
},
176-
}
177-
group new { argumentBindResult, p.ParameterType } by c into gr
178-
where gr.All(z => z.argumentBindResult.success)
179-
let matchedArgs = gr.Where(z => z.argumentBindResult.hasMatch).ToList()
180-
orderby matchedArgs.Count descending,
181-
matchedArgs.Count(p => p.ParameterType == typeof(string)) descending
182-
select new
183-
{
184-
ConstructorInfo = gr.Key,
185-
ArgumentValues = gr.Select(z => new { Value = z.argumentBindResult.value, Type = z.ParameterType })
186-
.ToList()
187-
}).FirstOrDefault();
188-
189-
if (ctor is null)
207+
for (var i = 0; i < binding.ci.GetParameters().Length; ++i)
190208
{
191-
return false;
209+
if (binding.args[i] is IConfigurationSection section)
210+
{
211+
var argumentValue = FromSection(section, _configurationAssemblies);
212+
binding.args[i] = argumentValue.ConvertTo(binding.ci.GetParameters()[i].ParameterType, resolutionContext);
213+
}
192214
}
193215

194-
var ctorArguments = new object?[ctor.ArgumentValues.Count];
195-
for (var i = 0; i < ctor.ArgumentValues.Count; i++)
216+
value = binding.ci.Invoke(binding.args.ToArray());
217+
218+
foreach (var pi in type.GetProperties(BindingFlags.Instance | BindingFlags.Public))
196219
{
197-
var argument = ctor.ArgumentValues[i];
198-
var valueValue = argument.Value;
199-
if (valueValue is IConfigurationSection s)
220+
if (!binding.suppliedNames.Contains(pi.Name) && suppliedArguments.TryGetValue(pi.Name, out var section) && pi.CanWrite)
200221
{
201-
var argumentValue = ConfigurationReader.GetArgumentValue(s, _configurationAssemblies);
202-
valueValue = argumentValue.ConvertTo(argument.Type, resolutionContext);
222+
var propertyValue = FromSection(section, _configurationAssemblies);
223+
try
224+
{
225+
pi.SetValue(value, propertyValue.ConvertTo(pi.PropertyType, resolutionContext));
226+
}
227+
catch (Exception ex)
228+
{
229+
SelfLog.WriteLine($"Serilog.Settings.Configuration: Property setter on {type} failed: {ex}");
230+
}
203231
}
204-
ctorArguments[i] = valueValue;
205232
}
206233

207-
value = ctor.ConstructorInfo.Invoke(ctorArguments);
208234
return true;
209235
}
210236

@@ -262,7 +288,7 @@ static bool IsConstructableDictionary(Type type, Type elementType, [NotNullWhen(
262288
}
263289
foreach (var method in concreteType.GetMethods())
264290
{
265-
if (!method.IsStatic && method.Name == "Add")
291+
if (method is { IsStatic: false, Name: "Add" })
266292
{
267293
var parameters = method.GetParameters();
268294
if (parameters.Length == 2 && parameters[0].ParameterType == keyType && parameters[1].ParameterType == valueType)
@@ -301,7 +327,7 @@ static bool IsConstructableContainer(Type type, Type elementType, [NotNullWhen(t
301327
}
302328
foreach (var method in concreteType.GetMethods())
303329
{
304-
if (!method.IsStatic && method.Name == "Add")
330+
if (method is { IsStatic: false, Name: "Add" })
305331
{
306332
var parameters = method.GetParameters();
307333
if (parameters.Length == 1 && parameters[0].ParameterType == elementType)

src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
namespace Serilog.Settings.Configuration;
88

9-
class StringArgumentValue : IConfigurationArgumentValue
9+
class StringArgumentValue : ConfigurationArgumentValue
1010
{
1111
readonly string _providedValue;
1212

@@ -24,7 +24,7 @@ public StringArgumentValue(string providedValue)
2424
{ typeof(Type), s => Type.GetType(s, throwOnError:true)! },
2525
};
2626

27-
public object? ConvertTo(Type toType, ResolutionContext resolutionContext)
27+
public override object? ConvertTo(Type toType, ResolutionContext resolutionContext)
2828
{
2929
var argumentValue = Environment.ExpandEnvironmentVariables(_providedValue);
3030

0 commit comments

Comments
 (0)