Skip to content

Commit

Permalink
Merge pull request #203 from sungam3r/collections-of-custom-types
Browse files Browse the repository at this point in the history
Add support for custom types in arrays and custom collections
  • Loading branch information
Sergey Komisarchik authored Mar 25, 2020
2 parents 02d559c + 33f20bb commit addaa41
Show file tree
Hide file tree
Showing 19 changed files with 250 additions and 60 deletions.
8 changes: 4 additions & 4 deletions Build.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ echo "build: Build started"

Push-Location $PSScriptRoot

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

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

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

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

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

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

Pop-Location
}
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -126,7 +126,7 @@ If a Serilog package requires additional external configuration information (for

### Complex parameter value binding

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.
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.

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.

Expand Down
2 changes: 1 addition & 1 deletion appveyor.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
version: '{build}'
skip_tags: true
image: Visual Studio 2017
image: Visual Studio 2019
configuration: Release
build_script:
- ps: ./Build.ps1
Expand Down
Binary file added assets/icon.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
5 changes: 3 additions & 2 deletions serilog-settings-configuration.sln
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 15
VisualStudioVersion = 15.0.27130.0
# Visual Studio Version 16
VisualStudioVersion = 16.0.29709.97
MinimumVisualStudioVersion = 10.0.40219.1
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{4E41FD57-5FAB-4E3C-B16E-463DE98338BC}"
EndProject
Expand All @@ -11,6 +11,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{62D0B9
appveyor.yml = appveyor.yml
Build.ps1 = Build.ps1
CHANGES.md = CHANGES.md
assets\icon.png = assets\icon.png
LICENSE = LICENSE
README.md = README.md
serilog-settings-configuration.sln.DotSettings = serilog-settings-configuration.sln.DotSettings
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>Microsoft.Extensions.Configuration (appsettings.json) support for Serilog.</Description>
<VersionPrefix>3.1.1</VersionPrefix>
<LangVersion>latest</LangVersion>
<Authors>Serilog Contributors</Authors>
<TargetFrameworks>netstandard2.0;net451;net461</TargetFrameworks>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
Expand All @@ -13,7 +14,7 @@
<PublicSign Condition=" '$(OS)' != 'Windows_NT' ">true</PublicSign>
<PackageId>Serilog.Settings.Configuration</PackageId>
<PackageTags>serilog;json</PackageTags>
<PackageIconUrl>https://serilog.net/images/serilog-configuration-nuget.png</PackageIconUrl>
<PackageIcon>icon.png</PackageIcon>
<PackageProjectUrl>https://github.com/serilog/serilog-settings-configuration</PackageProjectUrl>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<RepositoryUrl>https://github.com/serilog/serilog-settings-configuration</RepositoryUrl>
Expand All @@ -28,6 +29,7 @@
<ItemGroup>
<PackageReference Include="Microsoft.Extensions.DependencyModel" Version="2.0.4" />
<PackageReference Include="Serilog" Version="2.6.0" />
<None Include="..\..\assets\icon.png" Pack="true" PackagePath=""/>
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'net451'">
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,15 +27,12 @@ public static AssemblyFinder Auto()

public static AssemblyFinder ForSource(ConfigurationAssemblySource configurationAssemblySource)
{
switch (configurationAssemblySource)
return configurationAssemblySource switch
{
case ConfigurationAssemblySource.UseLoadedAssemblies:
return Auto();
case ConfigurationAssemblySource.AlwaysScanDllFiles:
return new DllScanningAssemblyFinder();
default:
throw new ArgumentOutOfRangeException(nameof(configurationAssemblySource), configurationAssemblySource, null);
}
ConfigurationAssemblySource.UseLoadedAssemblies => Auto(),
ConfigurationAssemblySource.AlwaysScanDllFiles => new DllScanningAssemblyFinder(),
_ => throw new ArgumentOutOfRangeException(nameof(configurationAssemblySource), configurationAssemblySource, null),
};
}

public static AssemblyFinder ForDependencyContext(DependencyContext dependencyContext)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ where IsCaseInsensitiveMatch(assemblyFileName, nameToFind)

return query.ToList().AsReadOnly();

AssemblyName TryGetAssemblyNameFrom(string path)
static AssemblyName TryGetAssemblyNameFrom(string path)
{
try
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -220,39 +220,14 @@ internal ILookup<string, Dictionary<string, IConfigurationArgumentValue>> GetMet
select new
{
Name = argument.Key,
Value = GetArgumentValue(argument)
Value = GetArgumentValue(argument, _configurationAssemblies)
}).ToDictionary(p => p.Name, p => p.Value)
select new { Name = name, Args = callArgs }))
.ToLookup(p => p.Name, p => p.Args);

return result;

IConfigurationArgumentValue GetArgumentValue(IConfigurationSection argumentSection)
{
IConfigurationArgumentValue argumentValue;

// Reject configurations where an element has both scalar and complex
// values as a result of reading multiple configuration sources.
if (argumentSection.Value != null && argumentSection.GetChildren().Any())
throw new InvalidOperationException(
$"The value for the argument '{argumentSection.Path}' is assigned different value " +
"types in more than one configuration source. Ensure all configurations consistently " +
"use either a scalar (int, string, boolean) or a complex (array, section, list, " +
"POCO, etc.) type for this argument value.");

if (argumentSection.Value != null)
{
argumentValue = new StringArgumentValue(argumentSection.Value);
}
else
{
argumentValue = new ObjectArgumentValue(argumentSection, _configurationAssemblies);
}

return argumentValue;
}

string GetSectionName(IConfigurationSection s)
static string GetSectionName(IConfigurationSection s)
{
var name = s.GetSection("Name");
if (name.Value == null)
Expand All @@ -262,6 +237,31 @@ string GetSectionName(IConfigurationSection s)
}
}

internal static IConfigurationArgumentValue GetArgumentValue(IConfigurationSection argumentSection, IReadOnlyCollection<Assembly> configurationAssemblies)
{
IConfigurationArgumentValue argumentValue;

// Reject configurations where an element has both scalar and complex
// values as a result of reading multiple configuration sources.
if (argumentSection.Value != null && argumentSection.GetChildren().Any())
throw new InvalidOperationException(
$"The value for the argument '{argumentSection.Path}' is assigned different value " +
"types in more than one configuration source. Ensure all configurations consistently " +
"use either a scalar (int, string, boolean) or a complex (array, section, list, " +
"POCO, etc.) type for this argument value.");

if (argumentSection.Value != null)
{
argumentValue = new StringArgumentValue(argumentSection.Value);
}
else
{
argumentValue = new ObjectArgumentValue(argumentSection, configurationAssemblies);
}

return argumentValue;
}

static IReadOnlyCollection<Assembly> LoadConfigurationAssemblies(IConfigurationSection section, AssemblyFinder assemblyFinder)
{
var assemblies = new Dictionary<string, Assembly>();
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
using Microsoft.Extensions.Configuration;
using Serilog.Configuration;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

using Serilog.Configuration;

namespace Serilog.Settings.Configuration
{
class ObjectArgumentValue : IConfigurationArgumentValue
Expand Down Expand Up @@ -47,8 +47,72 @@ public object ConvertTo(Type toType, ResolutionContext resolutionContext)
}
}

// MS Config binding
if (toType.IsArray)
return CreateArray();

if (IsContainer(toType, out var elementType) && TryCreateContainer(out var result))
return result;

// MS Config binding can work with a limited set of primitive types and collections
return _section.Get(toType);

object CreateArray()
{
var elementType = toType.GetElementType();
var configurationElements = _section.GetChildren().ToArray();
var result = Array.CreateInstance(elementType, configurationElements.Length);
for (int i = 0; i < configurationElements.Length; ++i)
{
var argumentValue = ConfigurationReader.GetArgumentValue(configurationElements[i], _configurationAssemblies);
var value = argumentValue.ConvertTo(elementType, resolutionContext);
result.SetValue(value, i);
}

return result;
}

bool TryCreateContainer(out object result)
{
result = null;

if (toType.GetConstructor(Type.EmptyTypes) == null)
return false;

// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/object-and-collection-initializers#collection-initializers
var addMethod = toType.GetMethods().FirstOrDefault(m => !m.IsStatic && m.Name == "Add" && m.GetParameters()?.Length == 1 && m.GetParameters()[0].ParameterType == elementType);
if (addMethod == null)
return false;

var configurationElements = _section.GetChildren().ToArray();
result = Activator.CreateInstance(toType);

for (int i = 0; i < configurationElements.Length; ++i)
{
var argumentValue = ConfigurationReader.GetArgumentValue(configurationElements[i], _configurationAssemblies);
var value = argumentValue.ConvertTo(elementType, resolutionContext);
addMethod.Invoke(result, new object[] { value });
}

return true;
}
}

private static bool IsContainer(Type type, out Type elementType)
{
elementType = null;
foreach (var iface in type.GetInterfaces())
{
if (iface.IsGenericType)
{
if (iface.GetGenericTypeDefinition() == typeof(IEnumerable<>))
{
elementType = iface.GetGenericArguments()[0];
return true;
}
}
}

return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,46 @@ public void SinkWithStringArrayArgument()
Assert.Equal(1, DummyRollingFileSink.Emitted.Count);
}

[Fact]
public void DestructureWithCollectionsOfTypeArgument()
{
var json = @"{
""Serilog"": {
""Using"": [ ""TestDummies"" ],
""Destructure"": [{
""Name"": ""DummyArrayOfType"",
""Args"": {
""list"": [
""System.Byte"",
""System.Int16""
],
""array"" : [
""System.Int32"",
""System.String""
],
""type"" : ""System.TimeSpan"",
""custom"" : [
""System.Int64""
],
""customString"" : [
""System.UInt32""
]
}
}]
}
}";

DummyPolicy.Current = null;

ConfigFromJson(json);

Assert.Equal(typeof(TimeSpan), DummyPolicy.Current.Type);
Assert.Equal(new[] { typeof(int), typeof(string) }, DummyPolicy.Current.Array);
Assert.Equal(new[] { typeof(byte), typeof(short) }, DummyPolicy.Current.List);
Assert.Equal(typeof(long), DummyPolicy.Current.Custom.First);
Assert.Equal("System.UInt32", DummyPolicy.Current.CustomStrings.First);
}

[Fact]
public void SinkWithIntArrayArgument()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ public void ShouldProbePrivateBinPath()
AppDomain.Unload(ad);
}

void DoTestInner()
static void DoTestInner()
{
var assemblyNames = new DllScanningAssemblyFinder().FindAssembliesContainingName("customSink");
Assert.Equal(2, assemblyNames.Count);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@
-->

<PropertyGroup>
<TargetFrameworks>net452;netcoreapp2.0</TargetFrameworks>
<TargetFrameworks>net452;netcoreapp2.0;netcoreapp3.1</TargetFrameworks>
<LangVersion>latest</LangVersion>
<AssemblyName>Serilog.Settings.Configuration.Tests</AssemblyName>
<AssemblyOriginatorKeyFile>../../assets/Serilog.snk</AssemblyOriginatorKeyFile>
<SignAssembly>true</SignAssembly>
Expand All @@ -38,6 +39,10 @@
<PackageReference Include="Microsoft.Extensions.Configuration.Abstractions" Version="2.0.1" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp3.1'">
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="3.1.0" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="15.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,7 @@ public class DelegatingSink : ILogEventSink

public DelegatingSink(Action<LogEvent> write)
{
if (write == null) throw new ArgumentNullException(nameof(write));
_write = write;
_write = write ?? throw new ArgumentNullException(nameof(write));
}

public void Emit(LogEvent logEvent)
Expand Down
10 changes: 10 additions & 0 deletions test/Serilog.Settings.Configuration.Tests/Support/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,5 +8,15 @@ public static object LiteralValue(this LogEventPropertyValue @this)
{
return ((ScalarValue)@this).Value;
}

// netcore3.0 error:
// Could not parse the JSON file. System.Text.Json.JsonReaderException : ''' is an invalid start of a property name. Expected a '"'
public static string ToValidJson(this string str)
{
#if NETCOREAPP3_1
str = str.Replace('\'', '"');
#endif
return str;
}
}
}
Loading

0 comments on commit addaa41

Please sign in to comment.