Skip to content

Commit addaa41

Browse files
author
Sergey Komisarchik
authored
Merge pull request #203 from sungam3r/collections-of-custom-types
Add support for custom types in arrays and custom collections
2 parents 02d559c + 33f20bb commit addaa41

19 files changed

+250
-60
lines changed

Build.ps1

+4-4
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ echo "build: Build started"
22

33
Push-Location $PSScriptRoot
44

5-
if(Test-Path .\artifacts) {
5+
if (Test-Path .\artifacts) {
66
echo "build: Cleaning .\artifacts"
77
Remove-Item .\artifacts -Force -Recurse
88
}
@@ -21,7 +21,7 @@ foreach ($src in ls src/*) {
2121
echo "build: Packaging project in $src"
2222

2323
& dotnet pack -c Release -o ..\..\artifacts --version-suffix=$suffix --include-source
24-
if($LASTEXITCODE -ne 0) { exit 1 }
24+
if ($LASTEXITCODE -ne 0) { exit 1 }
2525

2626
Pop-Location
2727
}
@@ -32,7 +32,7 @@ foreach ($test in ls test/*.PerformanceTests) {
3232
echo "build: Building performance test project in $test"
3333

3434
& dotnet build -c Release
35-
if($LASTEXITCODE -ne 0) { exit 2 }
35+
if ($LASTEXITCODE -ne 0) { exit 2 }
3636

3737
Pop-Location
3838
}
@@ -43,7 +43,7 @@ foreach ($test in ls test/*.Tests) {
4343
echo "build: Testing project in $test"
4444

4545
& dotnet test -c Release
46-
if($LASTEXITCODE -ne 0) { exit 3 }
46+
if ($LASTEXITCODE -ne 0) { exit 3 }
4747

4848
Pop-Location
4949
}

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,7 @@ If a Serilog package requires additional external configuration information (for
126126

127127
### Complex parameter value binding
128128

129-
When the configuration specifies a discrete value for a parameter (such as a string literal), the package will attempt to convert that value to the target method's declared CLR type of the parameter. Additional explicit handling is provided for parsing strings to `Uri` and `TimeSpan` objects and `enum` elements.
129+
When the configuration specifies a discrete value for a parameter (such as a string literal), the package will attempt to convert that value to the target method's declared CLR type of the parameter. Additional explicit handling is provided for parsing strings to `Uri`, `TimeSpan`, `enum` and arrays.
130130

131131
If the parameter value is not a discrete value, the package will use the configuration binding system provided by _Microsoft.Extensions.Options.ConfigurationExtensions_ to attempt to populate the parameter. Almost anything that can be bound by `IConfiguration.Get<T>` should work with this package. An example of this is the optional `List<Column>` parameter used to configure the .NET Standard version of the _Serilog.Sinks.MSSqlServer_ package.
132132

appveyor.yml

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
version: '{build}'
22
skip_tags: true
3-
image: Visual Studio 2017
3+
image: Visual Studio 2019
44
configuration: Release
55
build_script:
66
- ps: ./Build.ps1

assets/icon.png

22.5 KB
Loading

serilog-settings-configuration.sln

+3-2
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11

22
Microsoft Visual Studio Solution File, Format Version 12.00
3-
# Visual Studio 15
4-
VisualStudioVersion = 15.0.27130.0
3+
# Visual Studio Version 16
4+
VisualStudioVersion = 16.0.29709.97
55
MinimumVisualStudioVersion = 10.0.40219.1
66
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4E41FD57-5FAB-4E3C-B16E-463DE98338BC}"
77
EndProject
@@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{62D0B9
1111
appveyor.yml = appveyor.yml
1212
Build.ps1 = Build.ps1
1313
CHANGES.md = CHANGES.md
14+
assets\icon.png = assets\icon.png
1415
LICENSE = LICENSE
1516
README.md = README.md
1617
serilog-settings-configuration.sln.DotSettings = serilog-settings-configuration.sln.DotSettings

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

+4-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
1-
<Project Sdk="Microsoft.NET.Sdk">
1+
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
44
<Description>Microsoft.Extensions.Configuration (appsettings.json) support for Serilog.</Description>
55
<VersionPrefix>3.1.1</VersionPrefix>
6+
<LangVersion>latest</LangVersion>
67
<Authors>Serilog Contributors</Authors>
78
<TargetFrameworks>netstandard2.0;net451;net461</TargetFrameworks>
89
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
@@ -13,7 +14,7 @@
1314
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
1415
<PackageId>Serilog.Settings.Configuration</PackageId>
1516
<PackageTags>serilog;json</PackageTags>
16-
<PackageIconUrl>https://serilog.net/images/serilog-configuration-nuget.png</PackageIconUrl>
17+
<PackageIcon>icon.png</PackageIcon>
1718
<PackageProjectUrl>https://github.com/serilog/serilog-settings-configuration</PackageProjectUrl>
1819
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
1920
<RepositoryUrl>https://github.com/serilog/serilog-settings-configuration</RepositoryUrl>
@@ -28,6 +29,7 @@
2829
<ItemGroup>
2930
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="2.0.4" />
3031
<PackageReference Include="Serilog" Version="2.6.0" />
32+
<None Include="..\..\assets\icon.png" Pack="true" PackagePath=""/>
3133
</ItemGroup>
3234

3335
<ItemGroup Condition="'$(TargetFramework)' == 'net451'">

src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/AssemblyFinder.cs

+5-8
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,12 @@ public static AssemblyFinder Auto()
2727

2828
public static AssemblyFinder ForSource(ConfigurationAssemblySource configurationAssemblySource)
2929
{
30-
switch (configurationAssemblySource)
30+
return configurationAssemblySource switch
3131
{
32-
case ConfigurationAssemblySource.UseLoadedAssemblies:
33-
return Auto();
34-
case ConfigurationAssemblySource.AlwaysScanDllFiles:
35-
return new DllScanningAssemblyFinder();
36-
default:
37-
throw new ArgumentOutOfRangeException(nameof(configurationAssemblySource), configurationAssemblySource, null);
38-
}
32+
ConfigurationAssemblySource.UseLoadedAssemblies => Auto(),
33+
ConfigurationAssemblySource.AlwaysScanDllFiles => new DllScanningAssemblyFinder(),
34+
_ => throw new ArgumentOutOfRangeException(nameof(configurationAssemblySource), configurationAssemblySource, null),
35+
};
3936
}
4037

4138
public static AssemblyFinder ForDependencyContext(DependencyContext dependencyContext)

src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DllScanningAssemblyFinder.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,7 @@ where IsCaseInsensitiveMatch(assemblyFileName, nameToFind)
5050

5151
return query.ToList().AsReadOnly();
5252

53-
AssemblyName TryGetAssemblyNameFrom(string path)
53+
static AssemblyName TryGetAssemblyNameFrom(string path)
5454
{
5555
try
5656
{

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

+27-27
Original file line numberDiff line numberDiff line change
@@ -220,39 +220,14 @@ internal ILookup<string, Dictionary<string, IConfigurationArgumentValue>> GetMet
220220
select new
221221
{
222222
Name = argument.Key,
223-
Value = GetArgumentValue(argument)
223+
Value = GetArgumentValue(argument, _configurationAssemblies)
224224
}).ToDictionary(p => p.Name, p => p.Value)
225225
select new { Name = name, Args = callArgs }))
226226
.ToLookup(p => p.Name, p => p.Args);
227227

228228
return result;
229229

230-
IConfigurationArgumentValue GetArgumentValue(IConfigurationSection argumentSection)
231-
{
232-
IConfigurationArgumentValue argumentValue;
233-
234-
// Reject configurations where an element has both scalar and complex
235-
// values as a result of reading multiple configuration sources.
236-
if (argumentSection.Value != null && argumentSection.GetChildren().Any())
237-
throw new InvalidOperationException(
238-
$"The value for the argument '{argumentSection.Path}' is assigned different value " +
239-
"types in more than one configuration source. Ensure all configurations consistently " +
240-
"use either a scalar (int, string, boolean) or a complex (array, section, list, " +
241-
"POCO, etc.) type for this argument value.");
242-
243-
if (argumentSection.Value != null)
244-
{
245-
argumentValue = new StringArgumentValue(argumentSection.Value);
246-
}
247-
else
248-
{
249-
argumentValue = new ObjectArgumentValue(argumentSection, _configurationAssemblies);
250-
}
251-
252-
return argumentValue;
253-
}
254-
255-
string GetSectionName(IConfigurationSection s)
230+
static string GetSectionName(IConfigurationSection s)
256231
{
257232
var name = s.GetSection("Name");
258233
if (name.Value == null)
@@ -262,6 +237,31 @@ string GetSectionName(IConfigurationSection s)
262237
}
263238
}
264239

240+
internal static IConfigurationArgumentValue GetArgumentValue(IConfigurationSection argumentSection, IReadOnlyCollection<Assembly> configurationAssemblies)
241+
{
242+
IConfigurationArgumentValue argumentValue;
243+
244+
// Reject configurations where an element has both scalar and complex
245+
// values as a result of reading multiple configuration sources.
246+
if (argumentSection.Value != null && argumentSection.GetChildren().Any())
247+
throw new InvalidOperationException(
248+
$"The value for the argument '{argumentSection.Path}' is assigned different value " +
249+
"types in more than one configuration source. Ensure all configurations consistently " +
250+
"use either a scalar (int, string, boolean) or a complex (array, section, list, " +
251+
"POCO, etc.) type for this argument value.");
252+
253+
if (argumentSection.Value != null)
254+
{
255+
argumentValue = new StringArgumentValue(argumentSection.Value);
256+
}
257+
else
258+
{
259+
argumentValue = new ObjectArgumentValue(argumentSection, configurationAssemblies);
260+
}
261+
262+
return argumentValue;
263+
}
264+
265265
static IReadOnlyCollection<Assembly> LoadConfigurationAssemblies(IConfigurationSection section, AssemblyFinder assemblyFinder)
266266
{
267267
var assemblies = new Dictionary<string, Assembly>();

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

+67-3
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
using Microsoft.Extensions.Configuration;
2+
using Serilog.Configuration;
23
using System;
34
using System.Collections.Generic;
5+
using System.Linq;
46
using System.Reflection;
57

6-
using Serilog.Configuration;
7-
88
namespace Serilog.Settings.Configuration
99
{
1010
class ObjectArgumentValue : IConfigurationArgumentValue
@@ -47,8 +47,72 @@ public object ConvertTo(Type toType, ResolutionContext resolutionContext)
4747
}
4848
}
4949

50-
// MS Config binding
50+
if (toType.IsArray)
51+
return CreateArray();
52+
53+
if (IsContainer(toType, out var elementType) && TryCreateContainer(out var result))
54+
return result;
55+
56+
// MS Config binding can work with a limited set of primitive types and collections
5157
return _section.Get(toType);
58+
59+
object CreateArray()
60+
{
61+
var elementType = toType.GetElementType();
62+
var configurationElements = _section.GetChildren().ToArray();
63+
var result = Array.CreateInstance(elementType, configurationElements.Length);
64+
for (int i = 0; i < configurationElements.Length; ++i)
65+
{
66+
var argumentValue = ConfigurationReader.GetArgumentValue(configurationElements[i], _configurationAssemblies);
67+
var value = argumentValue.ConvertTo(elementType, resolutionContext);
68+
result.SetValue(value, i);
69+
}
70+
71+
return result;
72+
}
73+
74+
bool TryCreateContainer(out object result)
75+
{
76+
result = null;
77+
78+
if (toType.GetConstructor(Type.EmptyTypes) == null)
79+
return false;
80+
81+
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers#collection-initializers
82+
var addMethod = toType.GetMethods().FirstOrDefault(m => !m.IsStatic && m.Name == "Add" && m.GetParameters()?.Length == 1 && m.GetParameters()[0].ParameterType == elementType);
83+
if (addMethod == null)
84+
return false;
85+
86+
var configurationElements = _section.GetChildren().ToArray();
87+
result = Activator.CreateInstance(toType);
88+
89+
for (int i = 0; i < configurationElements.Length; ++i)
90+
{
91+
var argumentValue = ConfigurationReader.GetArgumentValue(configurationElements[i], _configurationAssemblies);
92+
var value = argumentValue.ConvertTo(elementType, resolutionContext);
93+
addMethod.Invoke(result, new object[] { value });
94+
}
95+
96+
return true;
97+
}
98+
}
99+
100+
private static bool IsContainer(Type type, out Type elementType)
101+
{
102+
elementType = null;
103+
foreach (var iface in type.GetInterfaces())
104+
{
105+
if (iface.IsGenericType)
106+
{
107+
if (iface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
108+
{
109+
elementType = iface.GetGenericArguments()[0];
110+
return true;
111+
}
112+
}
113+
}
114+
115+
return false;
52116
}
53117
}
54118
}

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

+40
Original file line numberDiff line numberDiff line change
@@ -615,6 +615,46 @@ public void SinkWithStringArrayArgument()
615615
Assert.Equal(1, DummyRollingFileSink.Emitted.Count);
616616
}
617617

618+
[Fact]
619+
public void DestructureWithCollectionsOfTypeArgument()
620+
{
621+
var json = @"{
622+
""Serilog"": {
623+
""Using"": [ ""TestDummies"" ],
624+
""Destructure"": [{
625+
""Name"": ""DummyArrayOfType"",
626+
""Args"": {
627+
""list"": [
628+
""System.Byte"",
629+
""System.Int16""
630+
],
631+
""array"" : [
632+
""System.Int32"",
633+
""System.String""
634+
],
635+
""type"" : ""System.TimeSpan"",
636+
""custom"" : [
637+
""System.Int64""
638+
],
639+
""customString"" : [
640+
""System.UInt32""
641+
]
642+
}
643+
}]
644+
}
645+
}";
646+
647+
DummyPolicy.Current = null;
648+
649+
ConfigFromJson(json);
650+
651+
Assert.Equal(typeof(TimeSpan), DummyPolicy.Current.Type);
652+
Assert.Equal(new[] { typeof(int), typeof(string) }, DummyPolicy.Current.Array);
653+
Assert.Equal(new[] { typeof(byte), typeof(short) }, DummyPolicy.Current.List);
654+
Assert.Equal(typeof(long), DummyPolicy.Current.Custom.First);
655+
Assert.Equal("System.UInt32", DummyPolicy.Current.CustomStrings.First);
656+
}
657+
618658
[Fact]
619659
public void SinkWithIntArrayArgument()
620660
{

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -65,7 +65,7 @@ public void ShouldProbePrivateBinPath()
6565
AppDomain.Unload(ad);
6666
}
6767

68-
void DoTestInner()
68+
static void DoTestInner()
6969
{
7070
var assemblyNames = new DllScanningAssemblyFinder().FindAssembliesContainingName("customSink");
7171
Assert.Equal(2, assemblyNames.Count);

test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj

+6-1
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@
1212
-->
1313

1414
<PropertyGroup>
15-
<TargetFrameworks>net452;netcoreapp2.0</TargetFrameworks>
15+
<TargetFrameworks>net452;netcoreapp2.0;netcoreapp3.1</TargetFrameworks>
16+
<LangVersion>latest</LangVersion>
1617
<AssemblyName>Serilog.Settings.Configuration.Tests</AssemblyName>
1718
<AssemblyOriginatorKeyFile>../../assets/Serilog.snk</AssemblyOriginatorKeyFile>
1819
<SignAssembly>true</SignAssembly>
@@ -38,6 +39,10 @@
3839
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.0.1" />
3940
</ItemGroup>
4041

42+
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
43+
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.0" />
44+
</ItemGroup>
45+
4146
<ItemGroup>
4247
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
4348
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />

test/Serilog.Settings.Configuration.Tests/Support/DelegatingSink.cs

+1-2
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@ public class DelegatingSink : ILogEventSink
1010

1111
public DelegatingSink(Action<LogEvent> write)
1212
{
13-
if (write == null) throw new ArgumentNullException(nameof(write));
14-
_write = write;
13+
_write = write ?? throw new ArgumentNullException(nameof(write));
1514
}
1615

1716
public void Emit(LogEvent logEvent)

test/Serilog.Settings.Configuration.Tests/Support/Extensions.cs

+10
Original file line numberDiff line numberDiff line change
@@ -8,5 +8,15 @@ public static object LiteralValue(this LogEventPropertyValue @this)
88
{
99
return ((ScalarValue)@this).Value;
1010
}
11+
12+
// netcore3.0 error:
13+
// Could not parse the JSON file. System.Text.Json.JsonReaderException : ''' is an invalid start of a property name. Expected a '"'
14+
public static string ToValidJson(this string str)
15+
{
16+
#if NETCOREAPP3_1
17+
str = str.Replace('\'', '"');
18+
#endif
19+
return str;
20+
}
1121
}
1222
}

0 commit comments

Comments
 (0)