From 52a37d2d3ff945b98fef3255b3c05b5b1a2a3aaa Mon Sep 17 00:00:00 2001 From: Sergey Komisarchik Date: Thu, 26 Aug 2021 13:13:40 +0300 Subject: [PATCH 1/2] constructors with arguments support for complex types --- CHANGES.md | 4 + README.md | 17 ++- sample/Sample/Program.cs | 9 +- sample/Sample/Sample.csproj | 1 + sample/Sample/appsettings.json | 13 +- .../Serilog.Settings.Configuration.csproj | 2 +- .../Configuration/ObjectArgumentValue.cs | 126 ++++++++++++++++++ .../ObjectArgumentValueTests.cs | 68 ++++++++++ .../ObjectArgumentValueTests.json | 44 ++++++ ...erilog.Settings.Configuration.Tests.csproj | 8 +- 10 files changed, 286 insertions(+), 6 deletions(-) create mode 100644 test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs create mode 100644 test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.json diff --git a/CHANGES.md b/CHANGES.md index da060cf..813058c 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -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 diff --git a/README.md b/README.md index 9d4b943..a026219 100644 --- a/README.md +++ b/README.md @@ -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` should work with this package. An example of this is the optional `List` 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, '')}] {@m}\n{@x}", + "formatProvider": "System.Globalization.CultureInfo::InvariantCulture" + } +} +``` + +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` should work with this package. An example of this is the optional `List` parameter used to configure the .NET Standard version of the _[Serilog.Sinks.MSSqlServer](https://github.com/serilog/serilog-sinks-mssqlserver)_ package. ### Abstract parameter types diff --git a/sample/Sample/Program.cs b/sample/Sample/Program.cs index 726df35..d3506f8 100644 --- a/sample/Sample/Program.cs +++ b/sample/Sample/Program.cs @@ -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; } } diff --git a/sample/Sample/Sample.csproj b/sample/Sample/Sample.csproj index d64b702..e52d441 100644 --- a/sample/Sample/Sample.csproj +++ b/sample/Sample/Sample.csproj @@ -38,6 +38,7 @@ + diff --git a/sample/Sample/appsettings.json b/sample/Sample/appsettings.json index 0d89e71..98aa712 100644 --- a/sample/Sample/appsettings.json +++ b/sample/Sample/appsettings.json @@ -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" + } + } } } ] @@ -106,7 +112,10 @@ { "Name": "With", "Args": { - "filter": "Sample.CustomFilter, Sample" + "filter": { + "type": "Sample.CustomFilter, Sample", + "levelFilter": "Verbose" + } } } ] diff --git a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj index 7dcf225..b170e46 100644 --- a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj +++ b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj @@ -2,7 +2,7 @@ Microsoft.Extensions.Configuration (appsettings.json) support for Serilog. - 3.2.1 + 3.3.0 latest Serilog Contributors netstandard2.0;net451;net461 diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs index 83f7443..c99598f 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Linq.Expressions; using System.Reflection; using Microsoft.Extensions.Configuration; @@ -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>(ctorExpression).Compile().Invoke(); + } + // MS Config binding can work with a limited set of primitive types and collections return _section.Get(toType); @@ -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("$type") switch + { + not null => "$type", + null => section.GetValue("type") switch + { + not null => "type", + null => null, + }, + }; + + var type = typeDirective switch + { + not null => Type.GetType(section.GetValue(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(); + 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; diff --git a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs new file mode 100644 index 0000000..f30d864 --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs @@ -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) { } + } + } +} diff --git a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.json b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.json new file mode 100644 index 0000000..f11c7ad --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.json @@ -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 + } +} diff --git a/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj b/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj index 9a02189..f25b9f0 100644 --- a/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj +++ b/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj @@ -12,6 +12,12 @@ $(DefineConstants);PRIVATE_BIN + + + + Always + + @@ -35,7 +41,7 @@ - + From d30a69894e5a9477fe17defc332400be6f1e1ad5 Mon Sep 17 00:00:00 2001 From: Sergey Komisarchik Date: Tue, 31 Aug 2021 08:16:56 +0300 Subject: [PATCH 2/2] review fix: simplify readme sample --- README.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/README.md b/README.md index a026219..64af6ac 100644 --- a/README.md +++ b/README.md @@ -293,8 +293,7 @@ If the parameter value is not a discrete value, it will try to find a best match "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, '')}] {@m}\n{@x}", - "formatProvider": "System.Globalization.CultureInfo::InvariantCulture" + "template": "[{@t:HH:mm:ss} {@l:u3} {Coalesce(SourceContext, '')}] {@m}\n{@x}" } } ```