Skip to content

Commit

Permalink
Merge pull request #282 from serilog/dev
Browse files Browse the repository at this point in the history
3.3.0 Release
  • Loading branch information
nblumhardt authored Oct 7, 2021
2 parents 816581f + 10a2cbd commit 651449d
Show file tree
Hide file tree
Showing 10 changed files with 301 additions and 9 deletions.
6 changes: 5 additions & 1 deletion CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
# Changelog

3.2.0 (pre-release)
3.3.0

* #276, #225, #167 - added support for constructors with arguments for complex types

3.2.0

* #162 - LoggingFilterSwitch support
* #202 - added support to AuditTo.Logger
Expand Down
31 changes: 29 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -269,9 +269,36 @@ Some Serilog packages require a reference to a logger configuration object. The

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`, arrays and custom collections.

### Static member support

Static member access can be used for passing to the configuration argument via [special](https://github.com/serilog/serilog-settings-configuration/blob/dev/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs#L35) syntax:

```json
{
"Args": {
"encoding": "System.Text.Encoding::UTF8",
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console"
}
}
```

### Complex parameter value binding

If the parameter value is not a discrete value, the package will use the configuration binding system provided by _[Microsoft.Extensions.Options.ConfigurationExtensions](https://www.nuget.org/packages/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](https://github.com/serilog/serilog-sinks-mssqlserver)_ package.
If the parameter value is not a discrete value, it will try to find a best matching public constructor for the argument:

```json
{
"Name": "Console",
"Args": {
"formatter": {
// `type` (or $type) is optional, must be specified for abstract declared parameter types
"type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions",
"template": "[{@t:HH:mm:ss} {@l:u3} {Coalesce(SourceContext, '<none>')}] {@m}\n{@x}"
}
}
```

For other cases the package will use the configuration binding system provided by _[Microsoft.Extensions.Options.ConfigurationExtensions](https://www.nuget.org/packages/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](https://github.com/serilog/serilog-sinks-mssqlserver)_ package.

### Abstract parameter types

Expand Down Expand Up @@ -360,7 +387,7 @@ In order to make auto-discovery of configuration assemblies work, modify Functio
</Target>

<Target Name="FunctionsPublishDepsCopy" AfterTargets="Publish">
<Copy SourceFiles="$(PublishDir)$(AssemblyName).deps.json" DestinationFiles="$(PublishDir)bin\$(AssemblyName).deps.json" />
<Copy SourceFiles="$(OutDir)$(AssemblyName).deps.json" DestinationFiles="$(PublishDir)bin\$(AssemblyName).deps.json" />
</Target>

</Project>
Expand Down
9 changes: 8 additions & 1 deletion sample/Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,16 @@ public static void Main(string[] args)
// processed by the Serilog.Filters.Expressions package.
public class CustomFilter : ILogEventFilter
{
readonly LogEventLevel _levelFilter;

public CustomFilter(LogEventLevel levelFilter = LogEventLevel.Information)
{
_levelFilter = levelFilter;
}

public bool IsEnabled(LogEvent logEvent)
{
return true;
return logEvent.Level >= _levelFilter;
}
}

Expand Down
1 change: 1 addition & 0 deletions sample/Sample/Sample.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.1.3" />
<PackageReference Include="Serilog.Formatting.Compact" Version="1.1.0" />
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
</ItemGroup>

Expand Down
13 changes: 11 additions & 2 deletions sample/Sample/appsettings.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,13 @@
{
"Name": "File",
"Args": {
"path": "%TEMP%/Logs/serilog-configuration-sample-errors.txt"
"path": "%TEMP%/Logs/serilog-configuration-sample-errors.txt",
"formatter": {
"type": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact",
"valueFormatter": {
"typeTagName": "customTypeTag"
}
}
}
}
]
Expand Down Expand Up @@ -106,7 +112,10 @@
{
"Name": "With",
"Args": {
"filter": "Sample.CustomFilter, Sample"
"filter": {
"type": "Sample.CustomFilter, Sample",
"levelFilter": "Verbose"
}
}
}
]
Expand Down
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<Description>Microsoft.Extensions.Configuration (appsettings.json) support for Serilog.</Description>
<VersionPrefix>3.2.0</VersionPrefix>
<VersionPrefix>3.3.0</VersionPrefix>
<LangVersion>latest</LangVersion>
<Authors>Serilog Contributors</Authors>
<TargetFrameworks>netstandard2.0;net451;net461</TargetFrameworks>
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;

using Microsoft.Extensions.Configuration;
Expand Down Expand Up @@ -50,6 +51,11 @@ public object ConvertTo(Type toType, ResolutionContext resolutionContext)
if (IsContainer(toType, out var elementType) && TryCreateContainer(out var result))
return result;

if (TryBuildCtorExpression(_section, toType, resolutionContext, out var ctorExpression))
{
return Expression.Lambda<Func<object>>(ctorExpression).Compile().Invoke();
}

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

Expand Down Expand Up @@ -94,6 +100,126 @@ bool TryCreateContainer(out object result)
}
}

internal static bool TryBuildCtorExpression(
IConfigurationSection section, Type parameterType, ResolutionContext resolutionContext, out NewExpression ctorExpression)
{
ctorExpression = null;

var typeDirective = section.GetValue<string>("$type") switch
{
not null => "$type",
null => section.GetValue<string>("type") switch
{
not null => "type",
null => null,
},
};

var type = typeDirective switch
{
not null => Type.GetType(section.GetValue<string>(typeDirective), throwOnError: false),
null => parameterType,
};

if (type is null or { IsAbstract: true })
{
return false;
}

var suppliedArguments = section.GetChildren().Where(s => s.Key != typeDirective)
.ToDictionary(s => s.Key, StringComparer.OrdinalIgnoreCase);

if (suppliedArguments.Count == 0 &&
type.GetConstructor(Type.EmptyTypes) is ConstructorInfo parameterlessCtor)
{
ctorExpression = Expression.New(parameterlessCtor);
return true;
}

var ctor =
(from c in type.GetConstructors()
from p in c.GetParameters()
let argumentBindResult = suppliedArguments.TryGetValue(p.Name, out var argValue) switch
{
true => new { success = true, hasMatch = true, value = (object)argValue },
false => p.HasDefaultValue switch
{
true => new { success = true, hasMatch = false, value = p.DefaultValue },
false => new { success = false, hasMatch = false, value = (object)null },
},
}
group new { argumentBindResult, p.ParameterType } by c into gr
where gr.All(z => z.argumentBindResult.success)
let matchedArgs = gr.Where(z => z.argumentBindResult.hasMatch).ToList()
orderby matchedArgs.Count descending,
matchedArgs.Count(p => p.ParameterType == typeof(string)) descending
select new
{
ConstructorInfo = gr.Key,
ArgumentValues = gr.Select(z => new { Value = z.argumentBindResult.value, Type = z.ParameterType })
.ToList()
}).FirstOrDefault();

if (ctor is null)
{
return false;
}

var ctorArguments = new List<Expression>();
foreach (var argumentValue in ctor.ArgumentValues)
{
if (TryBindToCtorArgument(argumentValue.Value, argumentValue.Type, resolutionContext, out var argumentExpression))
{
ctorArguments.Add(argumentExpression);
}
else
{
return false;
}
}

ctorExpression = Expression.New(ctor.ConstructorInfo, ctorArguments);
return true;

static bool TryBindToCtorArgument(object value, Type type, ResolutionContext resolutionContext, out Expression argumentExpression)
{
argumentExpression = null;

if (value is IConfigurationSection s)
{
if (s.Value is string argValue)
{
var stringArgumentValue = new StringArgumentValue(argValue);
try
{
argumentExpression = Expression.Constant(
stringArgumentValue.ConvertTo(type, resolutionContext),
type);

return true;
}
catch (Exception)
{
return false;
}
}
else if (s.GetChildren().Any())
{
if (TryBuildCtorExpression(s, type, resolutionContext, out var ctorExpression))
{
argumentExpression = ctorExpression;
return true;
}

return false;
}
}

argumentExpression = Expression.Constant(value, type);
return true;
}
}

static bool IsContainer(Type type, out Type elementType)
{
elementType = null;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
using System;

using Microsoft.Extensions.Configuration;

using Xunit;

namespace Serilog.Settings.Configuration.Tests
{
public class ObjectArgumentValueTests
{
readonly IConfigurationRoot _config;

public ObjectArgumentValueTests()
{
_config = new ConfigurationBuilder()
.AddJsonFile("ObjectArgumentValueTests.json")
.Build();
}

[Theory]
[InlineData("case_1", typeof(A), "new A(1, 23:59:59, http://dot.com/, \"d\")")]
[InlineData("case_2", typeof(B), "new B(2, new A(3, new D()), null)")]
[InlineData("case_3", typeof(E), "new E(\"1\", \"2\", \"3\")")]
[InlineData("case_4", typeof(F), "new F(\"paramType\", new E(1, 2, 3, 4))")]
[InlineData("case_5", typeof(G), "new G()")]
[InlineData("case_6", typeof(G), "new G(3, 4)")]
public void ShouldBindToConstructorArguments(string caseSection, Type targetType, string expectedExpression)
{
var testSection = _config.GetSection(caseSection);

Assert.True(ObjectArgumentValue.TryBuildCtorExpression(testSection, targetType, new(), out var ctorExpression));
Assert.Equal(expectedExpression, ctorExpression.ToString());
}

class A
{
public A(int a, TimeSpan b, Uri c, string d = "d") { }
public A(int a, C c) { }
}

class B
{
public B(int b, A a, long? c = null) { }
}

interface C { }

class D : C { }

class E
{
public E(int a, int b, int c, int d = 4) { }
public E(int a, string b, string c) { }
public E(string a, string b, string c) { }
}

class F
{
public F(string type, E e) { }
}

class G
{
public G() { }
public G(int a = 1, int b = 2) { }
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
{
"case_1": {
"A": 1,
"b": "23:59:59",
"c": "http://dot.com/"
},

"case_2": {
"b": 2,
"A": {
"a": 3,
"c": {
"type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+D, Serilog.Settings.Configuration.Tests"
}
}
},

"case_3": {
"a": 1,
"b": 2,
"c": 3
},

"case_4": {
"$type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+F, Serilog.Settings.Configuration.Tests",
"type": "paramType",
"e": {
"type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+E, Serilog.Settings.Configuration.Tests",
"a": 1,
"b": 2,
"c": 3,
"d": 4
}
},

"case_5": {

},

"case_6": {
"a": 3,
"b": 4
}
}
Loading

0 comments on commit 651449d

Please sign in to comment.