Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support of constructor parameters #281

Merged
merged 2 commits into from
Aug 31, 2021
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGES.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
# Changelog

3.3.0

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

3.2.0

* #162 - LoggingFilterSwitch support
Expand Down
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,22 @@ Static member access can be used for passing to the configuration argument via [

### 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}",
"formatProvider": "System.Globalization.CultureInfo::InvariantCulture"
skomis-mm marked this conversation as resolved.
Show resolved Hide resolved
}
}
```

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
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
Expand Up @@ -2,7 +2,7 @@

<PropertyGroup>
<Description>Microsoft.Extensions.Configuration (appsettings.json) support for Serilog.</Description>
<VersionPrefix>3.2.1</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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,12 @@
<PropertyGroup Condition="'$(TargetFramework)' == 'net452'">
<DefineConstants>$(DefineConstants);PRIVATE_BIN</DefineConstants>
</PropertyGroup>

<ItemGroup>
<None Include="*.json">
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
</None>
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\..\src\Serilog.Settings.Configuration\Serilog.Settings.Configuration.csproj" />
Expand All @@ -35,7 +41,7 @@
</ItemGroup>

<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.8.0" />
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.11.0" />
<PackageReference Include="Serilog.Filters.Expressions" Version="2.0.0" />
<PackageReference Include="xunit.runner.visualstudio" Version="2.2.0" />
<PackageReference Include="xunit" Version="2.2.0" />
Expand Down