Skip to content

Commit b0f97c9

Browse files
committed
Add support for IFormatProvider used to convert string to other types
Also introduce a new `Options` class to avoid `ReadFrom.Configuration()` exponential number of methods explosion when adding new options. Fixes serilog#325
1 parent ab87641 commit b0f97c9

10 files changed

+192
-27
lines changed

src/Serilog.Settings.Configuration/ConfigurationLoggerConfigurationExtensions.cs

Lines changed: 52 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -52,15 +52,8 @@ public static LoggerConfiguration Configuration(
5252
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
5353
if (sectionName == null) throw new ArgumentNullException(nameof(sectionName));
5454

55-
var assemblyFinder = dependencyContext == null
56-
? AssemblyFinder.Auto()
57-
: AssemblyFinder.ForDependencyContext(dependencyContext);
58-
59-
return settingConfiguration.Settings(
60-
new ConfigurationReader(
61-
configuration.GetSection(sectionName),
62-
assemblyFinder,
63-
configuration));
55+
var options = new Options(dependencyContext) { SectionName = sectionName, FormatProvider = null };
56+
return Configuration(settingConfiguration, configuration, options);
6457
}
6558

6659
/// <summary>
@@ -76,7 +69,7 @@ public static LoggerConfiguration Configuration(
7669
public static LoggerConfiguration Configuration(
7770
this LoggerSettingsConfiguration settingConfiguration,
7871
IConfiguration configuration,
79-
DependencyContext dependencyContext = null)
72+
DependencyContext dependencyContext)
8073
=> Configuration(settingConfiguration, configuration, DefaultSectionName, dependencyContext);
8174

8275
/// <summary>
@@ -105,7 +98,8 @@ public static LoggerConfiguration ConfigurationSection(
10598
new ConfigurationReader(
10699
configSection,
107100
assemblyFinder,
108-
configuration: null));
101+
configuration: null,
102+
formatProvider: null));
109103
}
110104

111105
/// <summary>
@@ -128,9 +122,8 @@ public static LoggerConfiguration Configuration(
128122
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
129123
if (sectionName == null) throw new ArgumentNullException(nameof(sectionName));
130124

131-
var assemblyFinder = AssemblyFinder.ForSource(configurationAssemblySource);
132-
133-
return settingConfiguration.Settings(new ConfigurationReader(configuration.GetSection(sectionName), assemblyFinder, configuration));
125+
var options = new Options(configurationAssemblySource) { SectionName = sectionName, FormatProvider = null };
126+
return Configuration(settingConfiguration, configuration, options);
134127
}
135128

136129
/// <summary>
@@ -167,7 +160,7 @@ public static LoggerConfiguration ConfigurationSection(
167160

168161
var assemblyFinder = AssemblyFinder.ForSource(configurationAssemblySource);
169162

170-
return settingConfiguration.Settings(new ConfigurationReader(configSection, assemblyFinder, configuration: null));
163+
return settingConfiguration.Settings(new ConfigurationReader(configSection, assemblyFinder, configuration: null, formatProvider: null));
171164
}
172165

173166
/// <summary>
@@ -188,7 +181,8 @@ public static LoggerConfiguration Configuration(
188181
if (configuration == null) throw new ArgumentNullException(nameof(configuration));
189182
if (sectionName == null) throw new ArgumentNullException(nameof(sectionName));
190183

191-
return settingConfiguration.Settings(new ConfigurationReader(configuration.GetSection(sectionName), assemblies, new ResolutionContext(configuration)));
184+
var options = new Options(assemblies) { SectionName = sectionName, FormatProvider = null };
185+
return Configuration(settingConfiguration, configuration, options);
192186
}
193187

194188
/// <summary>
@@ -203,4 +197,46 @@ public static LoggerConfiguration Configuration(
203197
IConfiguration configuration,
204198
params Assembly[] assemblies)
205199
=> Configuration(settingConfiguration, configuration, DefaultSectionName, assemblies);
200+
201+
/// <summary>
202+
/// Reads logger settings from the provided configuration object using the specified options.
203+
/// </summary>
204+
/// <param name="settingConfiguration">Logger setting configuration.</param>
205+
/// <param name="configuration">A configuration object which contains a Serilog section.</param>
206+
/// <param name="options">The options to adjust how the configuration object is processed.</param>
207+
/// <returns>An object allowing configuration to continue.</returns>
208+
public static LoggerConfiguration Configuration(
209+
this LoggerSettingsConfiguration settingConfiguration,
210+
IConfiguration configuration,
211+
Options options = null)
212+
{
213+
options ??= new Options(dependencyContext: null);
214+
var configurationReader = options switch
215+
{
216+
{ ConfigurationAssemblySource: {} } => GetConfigurationReader(configuration, options, options.ConfigurationAssemblySource.Value),
217+
{ Assemblies: {} } => GetConfigurationReader(configuration, options, options.Assemblies),
218+
_ => GetConfigurationReader(configuration, options, options.DependencyContext),
219+
};
220+
return settingConfiguration.Settings(configurationReader);
221+
}
222+
223+
static ConfigurationReader GetConfigurationReader(IConfiguration configuration, Options options, DependencyContext dependencyContext)
224+
{
225+
var assemblyFinder = dependencyContext == null ? AssemblyFinder.Auto() : AssemblyFinder.ForDependencyContext(dependencyContext);
226+
var section = configuration.GetSection(options.SectionName);
227+
return new ConfigurationReader(section, assemblyFinder, options.FormatProvider, configuration);
228+
}
229+
230+
static ConfigurationReader GetConfigurationReader(IConfiguration configuration, Options options, ConfigurationAssemblySource source)
231+
{
232+
var assemblyFinder = AssemblyFinder.ForSource(source);
233+
var section = configuration.GetSection(options.SectionName);
234+
return new ConfigurationReader(section, assemblyFinder, options.FormatProvider, configuration);
235+
}
236+
237+
static ConfigurationReader GetConfigurationReader(IConfiguration configuration, Options options, IReadOnlyCollection<Assembly> assemblies)
238+
{
239+
var section = configuration.GetSection(options.SectionName);
240+
return new ConfigurationReader(section, assemblies, new ResolutionContext(configuration, options.FormatProvider));
241+
}
206242
}

src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
<ItemGroup>
2424
<PackageReference Include="Microsoft.SourceLink.GitHub" Version="1.1.1" PrivateAssets="All" />
2525
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="3.0.0" />
26+
<PackageReference Include="PolySharp" Version="1.12.1" PrivateAssets="All" />
2627
<PackageReference Include="Serilog" Version="2.10.0" />
2728
<None Include="..\..\assets\icon.png" Pack="true" PackagePath="" Visible="false" />
2829
</ItemGroup>

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -24,11 +24,11 @@ class ConfigurationReader : IConfigurationReader
2424
readonly IConfigurationRoot _configurationRoot;
2525
#endif
2626

27-
public ConfigurationReader(IConfigurationSection configSection, AssemblyFinder assemblyFinder, IConfiguration configuration = null)
27+
public ConfigurationReader(IConfigurationSection configSection, AssemblyFinder assemblyFinder, IFormatProvider formatProvider, IConfiguration configuration = null)
2828
{
2929
_section = configSection ?? throw new ArgumentNullException(nameof(configSection));
3030
_configurationAssemblies = LoadConfigurationAssemblies(_section, assemblyFinder);
31-
_resolutionContext = new ResolutionContext(configuration);
31+
_resolutionContext = new ResolutionContext(configuration, formatProvider);
3232
#if NETSTANDARD || NET461
3333
_configurationRoot = configuration as IConfigurationRoot;
3434
#endif
Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
using System.Globalization;
2+
using System.Reflection;
3+
using Microsoft.Extensions.DependencyModel;
4+
5+
namespace Serilog.Settings.Configuration;
6+
7+
/// <summary>
8+
/// Options to adjust how the configuration object is processed.
9+
/// </summary>
10+
public class Options
11+
{
12+
/// <summary>
13+
/// Initialize a new instance of the <see cref="Options"/> class.
14+
/// </summary>
15+
/// <param name="assemblies">A collection of assemblies that contains sinks and other types.</param>
16+
/// <exception cref="ArgumentNullException">The <paramref name="assemblies"/> argument is null.</exception>
17+
/// <exception cref="ArgumentException">The <paramref name="assemblies"/> argument is empty.</exception>
18+
public Options(params Assembly[] assemblies)
19+
{
20+
Assemblies = assemblies ?? throw new ArgumentNullException(nameof(assemblies));
21+
if (assemblies.Length == 0)
22+
throw new ArgumentException("The assemblies array must not be empty.", nameof(assemblies));
23+
}
24+
25+
/// <summary>
26+
/// Initialize a new instance of the <see cref="Options"/> class.
27+
/// </summary>
28+
public Options() : this(dependencyContext: null)
29+
{
30+
}
31+
32+
/// <summary>
33+
/// Initialize a new instance of the <see cref="Options"/> class.
34+
/// </summary>
35+
/// <param name="dependencyContext">
36+
/// The dependency context from which sink/enricher packages can be located. If <see langword="null"/>, the platform default will be used.
37+
/// </param>
38+
public Options(DependencyContext dependencyContext) => DependencyContext = dependencyContext;
39+
40+
/// <summary>
41+
/// Initialize a new instance of the <see cref="Options"/> class.
42+
/// </summary>
43+
/// <param name="configurationAssemblySource">Defines how the package identifies assemblies to scan for sinks and other types.</param>
44+
public Options(ConfigurationAssemblySource configurationAssemblySource) => ConfigurationAssemblySource = configurationAssemblySource;
45+
46+
/// <summary>
47+
/// The section name for section which contains a Serilog section. Defaults to <c>Serilog</c>.
48+
/// </summary>
49+
public string SectionName { get; init; } = ConfigurationLoggerConfigurationExtensions.DefaultSectionName;
50+
51+
/// <summary>
52+
/// The <see cref="IFormatProvider"/> used when converting strings to other object types. Defaults to the invariant culture.
53+
/// </summary>
54+
public IFormatProvider FormatProvider { get; init; } = CultureInfo.InvariantCulture;
55+
56+
internal Assembly[] Assemblies { get; }
57+
internal DependencyContext DependencyContext { get; }
58+
internal ConfigurationAssemblySource? ConfigurationAssemblySource { get; }
59+
}

src/Serilog.Settings.Configuration/Settings/Configuration/ResolutionContext.cs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@ sealed class ResolutionContext
1313
readonly IDictionary<string, LoggingFilterSwitchProxy> _declaredFilterSwitches;
1414
readonly IConfiguration _appConfiguration;
1515

16-
public ResolutionContext(IConfiguration appConfiguration = null)
16+
public ResolutionContext(IConfiguration appConfiguration = null, IFormatProvider formatProvider = null)
1717
{
1818
_declaredLevelSwitches = new Dictionary<string, LoggingLevelSwitch>();
1919
_declaredFilterSwitches = new Dictionary<string, LoggingFilterSwitchProxy>();
2020
_appConfiguration = appConfiguration;
21+
FormatProvider = formatProvider;
2122
}
2223

24+
public IFormatProvider FormatProvider { get; }
25+
2326
/// <summary>
2427
/// Looks up a switch in the declared LoggingLevelSwitches
2528
/// </summary>

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ public object ConvertTo(Type toType, ResolutionContext resolutionContext)
155155
}
156156
}
157157

158-
return Convert.ChangeType(argumentValue, toType);
158+
return Convert.ChangeType(argumentValue, toType, resolutionContext.FormatProvider);
159159
}
160160

161161
internal static Type FindType(string typeName)

test/Serilog.Settings.Configuration.Tests/ConfigurationReaderTests.cs

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using System.Reflection;
1+
using System.Globalization;
2+
using System.Reflection;
23
using Microsoft.Extensions.Configuration;
34
using Serilog.Events;
45
using Serilog.Formatting;
@@ -16,7 +17,8 @@ public ConfigurationReaderTests()
1617
{
1718
_configurationReader = new ConfigurationReader(
1819
JsonStringConfigSource.LoadSection(@"{ 'Serilog': { } }", "Serilog"),
19-
AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies));
20+
AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies),
21+
CultureInfo.InvariantCulture);
2022
}
2123

2224
[Fact]
@@ -190,7 +192,7 @@ public void MethodsAreSelectedBasedOnCountOfMatchedArgumentsAndThenStringType()
190192
[MemberData(nameof(FlatMinimumLevel))]
191193
public void FlatMinimumLevelCorrectOneIsEnabledOnLogger(IConfigurationRoot root, LogEventLevel expectedMinimumLevel)
192194
{
193-
var reader = new ConfigurationReader(root.GetSection("Serilog"), AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies), root);
195+
var reader = new ConfigurationReader(root.GetSection("Serilog"), AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies), CultureInfo.InvariantCulture, root);
194196
var loggerConfig = new LoggerConfiguration();
195197

196198
reader.Configure(loggerConfig);
@@ -214,7 +216,7 @@ public void FlatMinimumLevelCorrectOneIsEnabledOnLogger(IConfigurationRoot root,
214216
[MemberData(nameof(ObjectMinimumLevel))]
215217
public void ObjectMinimumLevelCorrectOneIsEnabledOnLogger(IConfigurationRoot root, LogEventLevel expectedMinimumLevel)
216218
{
217-
var reader = new ConfigurationReader(root.GetSection("Serilog"), AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies), root);
219+
var reader = new ConfigurationReader(root.GetSection("Serilog"), AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies), CultureInfo.InvariantCulture, root);
218220
var loggerConfig = new LoggerConfiguration();
219221

220222
reader.Configure(loggerConfig);
@@ -258,7 +260,7 @@ public void ObjectMinimumLevelCorrectOneIsEnabledOnLogger(IConfigurationRoot roo
258260
[MemberData(nameof(MixedMinimumLevel))]
259261
public void MixedMinimumLevelCorrectOneIsEnabledOnLogger(IConfigurationRoot root, LogEventLevel expectedMinimumLevel)
260262
{
261-
var reader = new ConfigurationReader(root.GetSection("Serilog"), AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies), root);
263+
var reader = new ConfigurationReader(root.GetSection("Serilog"), AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies), CultureInfo.InvariantCulture, root);
262264
var loggerConfig = new LoggerConfiguration();
263265

264266
reader.Configure(loggerConfig);
@@ -272,7 +274,7 @@ public void MixedMinimumLevelCorrectOneIsEnabledOnLogger(IConfigurationRoot root
272274
public void NoConfigurationRootUsedStillValid()
273275
{
274276
var section = JsonStringConfigSource.LoadSection(@"{ 'Nest': { 'Serilog': { 'MinimumLevel': 'Error' } } }", "Nest");
275-
var reader = new ConfigurationReader(section.GetSection("Serilog"), AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies), section);
277+
var reader = new ConfigurationReader(section.GetSection("Serilog"), AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies), CultureInfo.InvariantCulture, section);
276278
var loggerConfig = new LoggerConfiguration();
277279

278280
reader.Configure(loggerConfig);

test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
1-
using Microsoft.Extensions.Configuration;
1+
using System.Globalization;
2+
using Microsoft.Extensions.Configuration;
23
using Serilog.Core;
34
using Serilog.Events;
45
using Serilog.Settings.Configuration.Tests.Support;
@@ -648,6 +649,50 @@ public void SinkWithStringArrayArgument()
648649
Assert.Single(DummyRollingFileSink.Emitted);
649650
}
650651

652+
[Theory]
653+
[InlineData(".")]
654+
[InlineData(",")]
655+
[Trait("BugFix", "https://github.com/serilog/serilog-settings-configuration/issues/325")]
656+
public void DestructureNumericNumbers(string numberDecimalSeparator)
657+
{
658+
var originalCulture = Thread.CurrentThread.CurrentCulture;
659+
660+
var culture = (CultureInfo)CultureInfo.InvariantCulture.Clone();
661+
culture.NumberFormat.NumberDecimalSeparator = numberDecimalSeparator;
662+
663+
Thread.CurrentThread.CurrentCulture = culture;
664+
665+
try
666+
{
667+
var json = @"{
668+
""Serilog"": {
669+
""Using"": [ ""TestDummies"" ],
670+
""Destructure"": [{
671+
""Name"": ""DummyNumbers"",
672+
""Args"": {
673+
""floatValue"": 0.1,
674+
""doubleValue"": 0.2,
675+
""decimalValue"": 0.3
676+
}
677+
}]
678+
}
679+
}";
680+
681+
DummyPolicy.Current = null;
682+
683+
ConfigFromJson(json);
684+
685+
Assert.NotNull(DummyPolicy.Current);
686+
Assert.Equal(0.1f, DummyPolicy.Current.Float);
687+
Assert.Equal(0.2d, DummyPolicy.Current.Double);
688+
Assert.Equal(0.3m, DummyPolicy.Current.Decimal);
689+
}
690+
finally
691+
{
692+
Thread.CurrentThread.CurrentCulture = originalCulture;
693+
}
694+
}
695+
651696
[Fact]
652697
public void DestructureWithCollectionsOfTypeArgument()
653698
{

test/TestDummies/DummyLoggerConfigurationExtensions.cs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -156,4 +156,17 @@ public static LoggerConfiguration DummyArrayOfType(this LoggerDestructuringConfi
156156
CustomStrings = customString,
157157
});
158158
}
159+
160+
public static LoggerConfiguration DummyNumbers(this LoggerDestructuringConfiguration loggerSinkConfiguration,
161+
float floatValue,
162+
double doubleValue,
163+
decimal decimalValue)
164+
{
165+
return loggerSinkConfiguration.With(DummyPolicy.Current = new DummyPolicy
166+
{
167+
Float = floatValue,
168+
Double = doubleValue,
169+
Decimal = decimalValue,
170+
});
171+
}
159172
}

test/TestDummies/DummyPolicy.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,12 @@ public class DummyPolicy : IDestructuringPolicy
1818

1919
public Type Type { get; set; }
2020

21+
public float Float { get; set; }
22+
23+
public double Double { get; set; }
24+
25+
public decimal Decimal { get; set; }
26+
2127
public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventPropertyValue result)
2228
{
2329
result = null;

0 commit comments

Comments
 (0)