Skip to content

Commit 651449d

Browse files
authored
Merge pull request #282 from serilog/dev
3.3.0 Release
2 parents 816581f + 10a2cbd commit 651449d

File tree

10 files changed

+301
-9
lines changed

10 files changed

+301
-9
lines changed

Diff for: CHANGES.md

+5-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
11
# Changelog
22

3-
3.2.0 (pre-release)
3+
3.3.0
4+
5+
* #276, #225, #167 - added support for constructors with arguments for complex types
6+
7+
3.2.0
48

59
* #162 - LoggingFilterSwitch support
610
* #202 - added support to AuditTo.Logger

Diff for: README.md

+29-2
Original file line numberDiff line numberDiff line change
@@ -269,9 +269,36 @@ Some Serilog packages require a reference to a logger configuration object. The
269269

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

272+
### Static member support
273+
274+
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:
275+
276+
```json
277+
{
278+
"Args": {
279+
"encoding": "System.Text.Encoding::UTF8",
280+
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console"
281+
}
282+
}
283+
```
284+
272285
### Complex parameter value binding
273286

274-
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.
287+
If the parameter value is not a discrete value, it will try to find a best matching public constructor for the argument:
288+
289+
```json
290+
{
291+
"Name": "Console",
292+
"Args": {
293+
"formatter": {
294+
// `type` (or $type) is optional, must be specified for abstract declared parameter types
295+
"type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions",
296+
"template": "[{@t:HH:mm:ss} {@l:u3} {Coalesce(SourceContext, '<none>')}] {@m}\n{@x}"
297+
}
298+
}
299+
```
300+
301+
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.
275302

276303
### Abstract parameter types
277304

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

362389
<Target Name="FunctionsPublishDepsCopy" AfterTargets="Publish">
363-
<Copy SourceFiles="$(PublishDir)$(AssemblyName).deps.json" DestinationFiles="$(PublishDir)bin\$(AssemblyName).deps.json" />
390+
<Copy SourceFiles="$(OutDir)$(AssemblyName).deps.json" DestinationFiles="$(PublishDir)bin\$(AssemblyName).deps.json" />
364391
</Target>
365392

366393
</Project>

Diff for: sample/Sample/Program.cs

+8-1
Original file line numberDiff line numberDiff line change
@@ -63,9 +63,16 @@ public static void Main(string[] args)
6363
// processed by the Serilog.Filters.Expressions package.
6464
public class CustomFilter : ILogEventFilter
6565
{
66+
readonly LogEventLevel _levelFilter;
67+
68+
public CustomFilter(LogEventLevel levelFilter = LogEventLevel.Information)
69+
{
70+
_levelFilter = levelFilter;
71+
}
72+
6673
public bool IsEnabled(LogEvent logEvent)
6774
{
68-
return true;
75+
return logEvent.Level >= _levelFilter;
6976
}
7077
}
7178

Diff for: sample/Sample/Sample.csproj

+1
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@
3838
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
3939
<PackageReference Include="Serilog.Sinks.File" Version="4.1.0" />
4040
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.1.3" />
41+
<PackageReference Include="Serilog.Formatting.Compact" Version="1.1.0" />
4142
<PackageReference Include="Serilog.Enrichers.Thread" Version="3.1.0" />
4243
</ItemGroup>
4344

Diff for: sample/Sample/appsettings.json

+11-2
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,13 @@
5151
{
5252
"Name": "File",
5353
"Args": {
54-
"path": "%TEMP%/Logs/serilog-configuration-sample-errors.txt"
54+
"path": "%TEMP%/Logs/serilog-configuration-sample-errors.txt",
55+
"formatter": {
56+
"type": "Serilog.Formatting.Compact.CompactJsonFormatter, Serilog.Formatting.Compact",
57+
"valueFormatter": {
58+
"typeTagName": "customTypeTag"
59+
}
60+
}
5561
}
5662
}
5763
]
@@ -106,7 +112,10 @@
106112
{
107113
"Name": "With",
108114
"Args": {
109-
"filter": "Sample.CustomFilter, Sample"
115+
"filter": {
116+
"type": "Sample.CustomFilter, Sample",
117+
"levelFilter": "Verbose"
118+
}
110119
}
111120
}
112121
]

Diff for: src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
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>
5-
<VersionPrefix>3.2.0</VersionPrefix>
5+
<VersionPrefix>3.3.0</VersionPrefix>
66
<LangVersion>latest</LangVersion>
77
<Authors>Serilog Contributors</Authors>
88
<TargetFrameworks>netstandard2.0;net451;net461</TargetFrameworks>

Diff for: src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs

+126
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
using System;
22
using System.Collections.Generic;
33
using System.Linq;
4+
using System.Linq.Expressions;
45
using System.Reflection;
56

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

54+
if (TryBuildCtorExpression(_section, toType, resolutionContext, out var ctorExpression))
55+
{
56+
return Expression.Lambda<Func<object>>(ctorExpression).Compile().Invoke();
57+
}
58+
5359
// MS Config binding can work with a limited set of primitive types and collections
5460
return _section.Get(toType);
5561

@@ -94,6 +100,126 @@ bool TryCreateContainer(out object result)
94100
}
95101
}
96102

103+
internal static bool TryBuildCtorExpression(
104+
IConfigurationSection section, Type parameterType, ResolutionContext resolutionContext, out NewExpression ctorExpression)
105+
{
106+
ctorExpression = null;
107+
108+
var typeDirective = section.GetValue<string>("$type") switch
109+
{
110+
not null => "$type",
111+
null => section.GetValue<string>("type") switch
112+
{
113+
not null => "type",
114+
null => null,
115+
},
116+
};
117+
118+
var type = typeDirective switch
119+
{
120+
not null => Type.GetType(section.GetValue<string>(typeDirective), throwOnError: false),
121+
null => parameterType,
122+
};
123+
124+
if (type is null or { IsAbstract: true })
125+
{
126+
return false;
127+
}
128+
129+
var suppliedArguments = section.GetChildren().Where(s => s.Key != typeDirective)
130+
.ToDictionary(s => s.Key, StringComparer.OrdinalIgnoreCase);
131+
132+
if (suppliedArguments.Count == 0 &&
133+
type.GetConstructor(Type.EmptyTypes) is ConstructorInfo parameterlessCtor)
134+
{
135+
ctorExpression = Expression.New(parameterlessCtor);
136+
return true;
137+
}
138+
139+
var ctor =
140+
(from c in type.GetConstructors()
141+
from p in c.GetParameters()
142+
let argumentBindResult = suppliedArguments.TryGetValue(p.Name, out var argValue) switch
143+
{
144+
true => new { success = true, hasMatch = true, value = (object)argValue },
145+
false => p.HasDefaultValue switch
146+
{
147+
true => new { success = true, hasMatch = false, value = p.DefaultValue },
148+
false => new { success = false, hasMatch = false, value = (object)null },
149+
},
150+
}
151+
group new { argumentBindResult, p.ParameterType } by c into gr
152+
where gr.All(z => z.argumentBindResult.success)
153+
let matchedArgs = gr.Where(z => z.argumentBindResult.hasMatch).ToList()
154+
orderby matchedArgs.Count descending,
155+
matchedArgs.Count(p => p.ParameterType == typeof(string)) descending
156+
select new
157+
{
158+
ConstructorInfo = gr.Key,
159+
ArgumentValues = gr.Select(z => new { Value = z.argumentBindResult.value, Type = z.ParameterType })
160+
.ToList()
161+
}).FirstOrDefault();
162+
163+
if (ctor is null)
164+
{
165+
return false;
166+
}
167+
168+
var ctorArguments = new List<Expression>();
169+
foreach (var argumentValue in ctor.ArgumentValues)
170+
{
171+
if (TryBindToCtorArgument(argumentValue.Value, argumentValue.Type, resolutionContext, out var argumentExpression))
172+
{
173+
ctorArguments.Add(argumentExpression);
174+
}
175+
else
176+
{
177+
return false;
178+
}
179+
}
180+
181+
ctorExpression = Expression.New(ctor.ConstructorInfo, ctorArguments);
182+
return true;
183+
184+
static bool TryBindToCtorArgument(object value, Type type, ResolutionContext resolutionContext, out Expression argumentExpression)
185+
{
186+
argumentExpression = null;
187+
188+
if (value is IConfigurationSection s)
189+
{
190+
if (s.Value is string argValue)
191+
{
192+
var stringArgumentValue = new StringArgumentValue(argValue);
193+
try
194+
{
195+
argumentExpression = Expression.Constant(
196+
stringArgumentValue.ConvertTo(type, resolutionContext),
197+
type);
198+
199+
return true;
200+
}
201+
catch (Exception)
202+
{
203+
return false;
204+
}
205+
}
206+
else if (s.GetChildren().Any())
207+
{
208+
if (TryBuildCtorExpression(s, type, resolutionContext, out var ctorExpression))
209+
{
210+
argumentExpression = ctorExpression;
211+
return true;
212+
}
213+
214+
return false;
215+
}
216+
}
217+
218+
argumentExpression = Expression.Constant(value, type);
219+
return true;
220+
}
221+
}
222+
97223
static bool IsContainer(Type type, out Type elementType)
98224
{
99225
elementType = null;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
using System;
2+
3+
using Microsoft.Extensions.Configuration;
4+
5+
using Xunit;
6+
7+
namespace Serilog.Settings.Configuration.Tests
8+
{
9+
public class ObjectArgumentValueTests
10+
{
11+
readonly IConfigurationRoot _config;
12+
13+
public ObjectArgumentValueTests()
14+
{
15+
_config = new ConfigurationBuilder()
16+
.AddJsonFile("ObjectArgumentValueTests.json")
17+
.Build();
18+
}
19+
20+
[Theory]
21+
[InlineData("case_1", typeof(A), "new A(1, 23:59:59, http://dot.com/, \"d\")")]
22+
[InlineData("case_2", typeof(B), "new B(2, new A(3, new D()), null)")]
23+
[InlineData("case_3", typeof(E), "new E(\"1\", \"2\", \"3\")")]
24+
[InlineData("case_4", typeof(F), "new F(\"paramType\", new E(1, 2, 3, 4))")]
25+
[InlineData("case_5", typeof(G), "new G()")]
26+
[InlineData("case_6", typeof(G), "new G(3, 4)")]
27+
public void ShouldBindToConstructorArguments(string caseSection, Type targetType, string expectedExpression)
28+
{
29+
var testSection = _config.GetSection(caseSection);
30+
31+
Assert.True(ObjectArgumentValue.TryBuildCtorExpression(testSection, targetType, new(), out var ctorExpression));
32+
Assert.Equal(expectedExpression, ctorExpression.ToString());
33+
}
34+
35+
class A
36+
{
37+
public A(int a, TimeSpan b, Uri c, string d = "d") { }
38+
public A(int a, C c) { }
39+
}
40+
41+
class B
42+
{
43+
public B(int b, A a, long? c = null) { }
44+
}
45+
46+
interface C { }
47+
48+
class D : C { }
49+
50+
class E
51+
{
52+
public E(int a, int b, int c, int d = 4) { }
53+
public E(int a, string b, string c) { }
54+
public E(string a, string b, string c) { }
55+
}
56+
57+
class F
58+
{
59+
public F(string type, E e) { }
60+
}
61+
62+
class G
63+
{
64+
public G() { }
65+
public G(int a = 1, int b = 2) { }
66+
}
67+
}
68+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
{
2+
"case_1": {
3+
"A": 1,
4+
"b": "23:59:59",
5+
"c": "http://dot.com/"
6+
},
7+
8+
"case_2": {
9+
"b": 2,
10+
"A": {
11+
"a": 3,
12+
"c": {
13+
"type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+D, Serilog.Settings.Configuration.Tests"
14+
}
15+
}
16+
},
17+
18+
"case_3": {
19+
"a": 1,
20+
"b": 2,
21+
"c": 3
22+
},
23+
24+
"case_4": {
25+
"$type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+F, Serilog.Settings.Configuration.Tests",
26+
"type": "paramType",
27+
"e": {
28+
"type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+E, Serilog.Settings.Configuration.Tests",
29+
"a": 1,
30+
"b": 2,
31+
"c": 3,
32+
"d": 4
33+
}
34+
},
35+
36+
"case_5": {
37+
38+
},
39+
40+
"case_6": {
41+
"a": 3,
42+
"b": 4
43+
}
44+
}

0 commit comments

Comments
 (0)