From 15abae91dd1cb3576067aabb1cafb7828c2ff676 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Wed, 15 Nov 2023 11:30:09 +1000 Subject: [PATCH 01/13] Dev version bump [skip ci] --- .../Serilog.Settings.Configuration.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj index 8ccfbb3..8e5a021 100644 --- a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj +++ b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj @@ -3,7 +3,7 @@ Microsoft.Extensions.Configuration (appsettings.json) support for Serilog. - 8.0.0 + 8.0.1 Serilog Contributors From e3088a1691662465c5c3c66befcafb0118888e65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Wed, 15 Nov 2023 10:28:20 +0100 Subject: [PATCH 02/13] Disable deterministic source paths in the tests So that the [CallerFilePath] attribute works as expected with absolute paths on the current machine instead of being substituted with /_/ --- .../Serilog.Settings.Configuration.Tests.csproj | 1 + 1 file changed, 1 insertion(+) 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 b04410e..e79a4d5 100644 --- a/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj +++ b/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj @@ -3,6 +3,7 @@ net48 $(TargetFrameworks);net6.0;net7.0;net8.0 + false From 18db0739af9efb7e1d4b1c1123a724ba646bc4d1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Wed, 15 Nov 2023 10:28:52 +0100 Subject: [PATCH 03/13] Revert "Use the regular dotnet test command invocation" This reverts commit 41bc33d4bc7b03c920102ddaba195e96d152fb5f. --- .gitignore | 1 - Build.ps1 | 7 +++++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/.gitignore b/.gitignore index db9e81a..c90bd95 100644 --- a/.gitignore +++ b/.gitignore @@ -201,4 +201,3 @@ FakesAssemblies/ project.lock.json artifacts/ -/test/TestApp-* diff --git a/Build.ps1 b/Build.ps1 index 18d5bfb..1513382 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -30,6 +30,9 @@ if($LASTEXITCODE -ne 0) { throw 'pack failed' } Write-Output "build: Testing" -& dotnet test test\Serilog.Settings.Configuration.Tests\Serilog.Settings.Configuration.Tests.csproj +# Dotnet test doesn't run separate TargetFrameworks in parallel: https://github.com/dotnet/sdk/issues/19147 +# Workaround: use `dotnet test` on dlls directly in order to pass the `--parallel` option to vstest. +# The _reported_ runtime is wrong but the _actual_ used runtime is correct, see https://github.com/microsoft/vstest/issues/2037#issuecomment-720549173 +& dotnet test test\Serilog.Settings.Configuration.Tests\bin\Release\*\Serilog.Settings.Configuration.Tests.dll --parallel -if($LASTEXITCODE -ne 0) { throw 'unit tests failed' } +if($LASTEXITCODE -ne 0) { throw 'unit tests failed' } \ No newline at end of file From 75cbe35cec85ebe14dffb838dc2cc741d561b61a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?C=C3=A9dric=20Luthi?= Date: Wed, 15 Nov 2023 10:47:25 +0100 Subject: [PATCH 04/13] Restore allowPrerelease=false and rollForward=latestFeature in global.json Was removed in 725b6b24885a0b150b53734fb8eebabfb33b84cd but will be useful when the next versions of the .NET 8 SDK will be published. --- global.json | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/global.json b/global.json index 5ce8495..b7e3357 100644 --- a/global.json +++ b/global.json @@ -1,5 +1,7 @@ { "sdk": { - "version": "8.0.100" + "version": "8.0.100", + "allowPrerelease": false, + "rollForward": "latestFeature" } } From 9cd610f213771fcbf13e436fb788e5db921df1f0 Mon Sep 17 00:00:00 2001 From: Christoffer Gersen Date: Sun, 14 Jan 2024 17:09:24 +0100 Subject: [PATCH 05/13] Improve test cases for ObjectArgumentValue Also includes some improvements for StringArgumentValueTests --- .../ObjectArgumentValueTests.cs | 901 +++++++++++++++++- .../ObjectArgumentValueTests.json | 44 - .../StringArgumentValueTests.cs | 144 ++- .../Support/GenericClass.cs | 11 + .../Support/StaticAccessorClasses.cs | 1 + 5 files changed, 1025 insertions(+), 76 deletions(-) delete mode 100644 test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.json create mode 100644 test/Serilog.Settings.Configuration.Tests/Support/GenericClass.cs diff --git a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs index ee775e4..7c354c4 100644 --- a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs @@ -1,4 +1,13 @@ using Microsoft.Extensions.Configuration; +using Serilog.Configuration; +using Serilog.Events; +using Serilog.Settings.Configuration.Tests.Support; +using System.Collections; +using System.Diagnostics.CodeAnalysis; +using System.Globalization; +using System.Reflection; +using TestDummies; +using TestDummies.Console; // ReSharper disable UnusedMember.Local // ReSharper disable UnusedParameter.Local @@ -8,60 +17,890 @@ namespace Serilog.Settings.Configuration.Tests; public class ObjectArgumentValueTests { - readonly IConfigurationRoot _config; + private static T ConvertToReturnsType(ObjectArgumentValue value, ResolutionContext? resolutionContext = null) + { + return Assert.IsType(value.ConvertTo(typeof(T), resolutionContext ?? new())); + } + + [Fact] + public void ConvertToIConfigurationSection() + { + var section = JsonStringConfigSource.LoadSection("{ \"Serilog\": { } }", "Serilog"); + var value = new ObjectArgumentValue(section, []); + + var actual = value.ConvertTo(typeof(IConfigurationSection), new()); + + Assert.Same(section, actual); + } + + [Fact] + public void ConvertToLoggerConfigurationCallback() + { + var section = JsonStringConfigSource.LoadSection(""" + { + "Serilog": { + "WriteTo": [{ + "Name": "DummyRollingFile", + "Args": {"pathFormat" : "C:\\"} + }], + "Enrich": ["WithDummyThreadId"] + } + } + """, "Serilog"); + var value = new ObjectArgumentValue(section, [typeof(DummyRollingFileSink).GetTypeInfo().Assembly]); + + var configure = ConvertToReturnsType>(value); + + var config = new LoggerConfiguration(); + configure(config); + var log = config.CreateLogger(); + DummyRollingFileSink.Emitted.Clear(); + + log.Write(Some.InformationEvent()); + + var evt = Assert.Single(DummyRollingFileSink.Emitted); + Assert.True(evt.Properties.ContainsKey("ThreadId"), "Event should have enriched property ThreadId"); + } + + [Fact] + public void ConvertToLoggerSinkConfigurationCallback() + { + var section = JsonStringConfigSource.LoadSection(""" + { + "WriteTo": [{ + "Name": "Dummy", + "Args": { + "wrappedSinkAction" : [{ "Name": "DummyConsole", "Args": {} }] + } + }] + } + """, "WriteTo"); + var value = new ObjectArgumentValue(section, [typeof(DummyConfigurationSink).GetTypeInfo().Assembly]); + + var configureSinks = ConvertToReturnsType>(value); + + var config = new LoggerConfiguration(); + configureSinks(config.WriteTo); + var log = config.CreateLogger(); + DummyConsoleSink.Emitted.Clear(); + DummyWrappingSink.Emitted.Clear(); + + log.Write(Some.InformationEvent()); + + Assert.Single(DummyWrappingSink.Emitted); + Assert.Single(DummyConsoleSink.Emitted); + } + + [Fact] + public void ConvertToLoggerEnrichmentConfiguration() + { + var section = JsonStringConfigSource.LoadSection(""" + { + "Enrich": [{ + "Name": "AtLevel", + "Args": { + "enrichFromLevel": "Warning", + "configureEnricher": [ "WithDummyThreadId" ] + } + }] + } + """, "Enrich"); + var value = new ObjectArgumentValue( + section, + [ + typeof(LoggerEnrichmentConfiguration).GetTypeInfo().Assembly, + typeof(DummyThreadIdEnricher).GetTypeInfo().Assembly + ]); + + var configureEnrichment = ConvertToReturnsType>(value); + + var config = new LoggerConfiguration(); + config.WriteTo.DummyRollingFile(""); + configureEnrichment(config.Enrich); + var log = config.CreateLogger(); + + DummyRollingFileSink.Emitted.Clear(); + + log.Write(Some.InformationEvent()); + log.Write(Some.WarningEvent()); + + Assert.Collection(DummyRollingFileSink.Emitted, + info => Assert.False(info.Properties.ContainsKey("ThreadId"), "Information event or lower should not have enriched property ThreadId"), + warn => Assert.True(warn.Properties.ContainsKey("ThreadId"), "Warning event or higher should have enriched property ThreadId")); + } + + [Fact] + public void ConvertToConfigurationCallbackThrows() + { + var section = JsonStringConfigSource.LoadSection(""" + { + "Configure": {} + } + """, "Configure"); + var value = new ObjectArgumentValue(section, []); + + var ex = Assert.Throws(() => value.ConvertTo(typeof(Action), new())); + + Assert.Equal("Configuration resolution for Action parameter type at the path Configure is not implemented.", ex.Message); + } + + [Fact] + public void ConvertToArrayUsingStringArgumentValueForElements() + { + var section = JsonStringConfigSource.LoadSection("{ \"Array\": [ \"Information\", 3, null ] }", "Array"); + var value = new ObjectArgumentValue(section, []); + + var array = ConvertToReturnsType(value); + + Assert.Equal([LogEventLevel.Information, LogEventLevel.Warning, null], array); + } + + [Fact] + public void ConvertToArrayOfArraysPassingContext() + { + var formatProvider = new NumberFormatInfo() + { + NumberDecimalSeparator = ",", + NumberGroupSeparator = ".", + NumberGroupSizes = [3], + }; + + var section = JsonStringConfigSource.LoadSection("{ \"Array\": [ [ 1, 2 ], [ 3, 4 ], [ \"1.234,56\" ] ] }", "Array"); + var value = new ObjectArgumentValue(section, []); + + var array = ConvertToReturnsType(value, new(readerOptions: new() { FormatProvider = formatProvider })); + + Assert.Equal([[1, 2], [3, 4], [1_234.56M]], array); + } - public ObjectArgumentValueTests() + [Fact] + public void ConvertToArrayRecursingObjectArgumentValuePassingAssemblies() { - _config = new ConfigurationBuilder() - .AddJsonFile("ObjectArgumentValueTests.json") - .Build(); + var section = JsonStringConfigSource.LoadSection(""" + { + "Array": [{ "WriteTo": [{ "Name": "DummyConsole", "Args": {} }] }] + } + """, "Array"); + var value = new ObjectArgumentValue(section, [typeof(DummyConsoleSink).GetTypeInfo().Assembly]); + + var configureCalls = ConvertToReturnsType[]>(value); + + var configure = Assert.Single(configureCalls); + var config = new LoggerConfiguration(); + configure(config); + var log = config.CreateLogger(); + DummyConsoleSink.Emitted.Clear(); + + log.Write(Some.InformationEvent()); + + Assert.Single(DummyConsoleSink.Emitted); + } + + [Fact] + public void ConvertToArrayWithDifferentImplementations() + { + var section = JsonStringConfigSource.LoadSection(""" + { + "Array": [ + "Serilog.Settings.Configuration.Tests.Support.ConcreteImpl::Instance, Serilog.Settings.Configuration.Tests", + "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+PrivateImplWithPublicCtor, Serilog.Settings.Configuration.Tests" + ] + } + """, "Array"); + var value = new ObjectArgumentValue(section, []); + + var array = ConvertToReturnsType(value); + + Assert.Collection(array, + first => Assert.IsType(first), + second => Assert.IsType(second)); + } + + [Fact] + public void ConvertToContainerUsingStringArgumentValueForElements() + { + var section = JsonStringConfigSource.LoadSection("{ \"List\": [ \"Information\", 3, null ] }", "List"); + var value = new ObjectArgumentValue(section, []); + + var list = ConvertToReturnsType>(value); + + Assert.Equal([LogEventLevel.Information, LogEventLevel.Warning, null], list); + } + + [Fact] + public void ConvertToNestedContainerPassingContext() + { + var formatProvider = new NumberFormatInfo() + { + NumberDecimalSeparator = ",", + NumberGroupSeparator = ".", + NumberGroupSizes = [3], + }; + + var section = JsonStringConfigSource.LoadSection("{ \"List\": [ [ 1, 2 ], [ 3, 4 ], [ \"1.234,56\" ] ] }", "List"); + var value = new ObjectArgumentValue(section, []); + + var array = ConvertToReturnsType>>(value, new(readerOptions: new() { FormatProvider = formatProvider })); + + Assert.Equal([[1, 2], [3, 4], [1_234.56M]], array); + } + + [Fact] + public void ConvertToContainerRecursingObjectArgumentValuePassingAssemblies() + { + var section = JsonStringConfigSource.LoadSection(""" + { + "List": [{ "WriteTo": [{ "Name": "DummyConsole", "Args": {} }] }] + } + """, "List"); + var value = new ObjectArgumentValue(section, [typeof(DummyConsoleSink).GetTypeInfo().Assembly]); + + var configureCalls = ConvertToReturnsType>>(value); + + var configure = Assert.Single(configureCalls); + var config = new LoggerConfiguration(); + configure(config); + var log = config.CreateLogger(); + DummyConsoleSink.Emitted.Clear(); + + log.Write(Some.InformationEvent()); + + Assert.Single(DummyConsoleSink.Emitted); + } + + [Fact] + public void ConvertToListWithDifferentImplementations() + { + var section = JsonStringConfigSource.LoadSection(""" + { + "List": [ + "Serilog.Settings.Configuration.Tests.Support.ConcreteImpl::Instance, Serilog.Settings.Configuration.Tests", + "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+PrivateImplWithPublicCtor, Serilog.Settings.Configuration.Tests" + ] + } + """, "List"); + var value = new ObjectArgumentValue(section, []); + + var list = ConvertToReturnsType>(value); + + Assert.Collection(list, + first => Assert.IsType(first), + second => Assert.IsType(second)); + } + + class UnsupportedContainer : IEnumerable + { + public IEnumerator GetEnumerator() + { + return Enumerable.Empty().GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + + [Fact] + public void ConvertToUnsupportedContainerWillBeCreatedButWillRemainEmpty() + { + var section = JsonStringConfigSource.LoadSection(""" + { + "List": ["a", "b"] + } + """, "List"); + var value = new ObjectArgumentValue(section, []); + + var unsupported = ConvertToReturnsType(value); + + Assert.Empty(unsupported); } [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) + [InlineData(typeof(ICollection))] + [InlineData(typeof(IList))] + [InlineData(typeof(List))] + public void ConvertToContainerUsingList(Type containerType) { - var testSection = _config.GetSection(caseSection); + var section = JsonStringConfigSource.LoadSection("{ \"Container\": [ 1, 1 ] }", "Container"); + var value = new ObjectArgumentValue(section, []); + + var container = value.ConvertTo(containerType, new()); - Assert.True(ObjectArgumentValue.TryBuildCtorExpression(testSection, targetType, new(), out var ctorExpression)); - Assert.Equal(expectedExpression, ctorExpression.ToString()); + var list = Assert.IsType>(container); + Assert.Equal([1, 1], list); } - class A + [Fact] + public void ConvertToContainerUsingHashSet() { - public A(int a, TimeSpan b, Uri c, string d = "d") { } - public A(int a, C c) { } + var containerType = typeof(HashSet); + var section = JsonStringConfigSource.LoadSection("{ \"Container\": [ 1, 1, 2, 2 ] }", "Container"); + var value = new ObjectArgumentValue(section, []); + + var container = value.ConvertTo(containerType, new()); + + var set = Assert.IsType>(container); + Assert.Equal([1, 2], set); } - class B + [Theory] + [InlineData(typeof(IDictionary))] + [InlineData(typeof(IReadOnlyDictionary))] + [InlineData(typeof(Dictionary))] + public void ConvertToContainerUsingDictionary(Type containerType) { - public B(int b, A a, long? c = null) { } + var section = JsonStringConfigSource.LoadSection(""" + { + "Container": { + "a": 1, + "b": 2 + } + } + """, "Container"); + var value = new ObjectArgumentValue(section, []); + + var container = value.ConvertTo(containerType, new()); + + var dictionary = Assert.IsType>(container); + Assert.Equal(new Dictionary() { { "a", 1 }, { "b", 2 } }, dictionary); } - interface C { } + [Theory] + [InlineData(typeof(IDictionary))] + [InlineData(typeof(IReadOnlyDictionary))] + [InlineData(typeof(Dictionary))] + public void ConvertToContainerUsingDictionaryUsingStringArgumentValueToConvertKey(Type containerType) + { + var section = JsonStringConfigSource.LoadSection(""" + { + "Container": { + "1": 2, + "3": 4 + } + } + """, "Container"); + var value = new ObjectArgumentValue(section, []); + + var container = value.ConvertTo(containerType, new()); - class D : C { } + var dictionary = Assert.IsType>(container); + Assert.Equal(new Dictionary() { { 1, 2 }, { 3, 4 } }, dictionary); + } - class E + class DictionaryWithoutPublicDefaultConstructor : IDictionary { - 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) { } + private readonly IDictionary backing; + + public int this[string key] { get => backing[key]; set => backing[key] = value; } + + public ICollection Keys => backing.Keys; + + public ICollection Values => backing.Values; + + public int Count => backing.Count; + + public bool IsReadOnly => backing.IsReadOnly; + + // Normally there would be a default constructor here, like: public DictionaryWithoutPublicDefaultConstructor() {} + public DictionaryWithoutPublicDefaultConstructor(IDictionary values) { backing = values; } + + public void Add(string key, int value) + { + backing.Add(key, value); + } + + public void Add(KeyValuePair item) + { + backing.Add(item); + } + + public void Clear() + { + backing.Clear(); + } + + public bool Contains(KeyValuePair item) + { + return backing.Contains(item); + } + + public bool ContainsKey(string key) + { + return backing.ContainsKey(key); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + backing.CopyTo(array, arrayIndex); + } + + public IEnumerator> GetEnumerator() + { + return backing.GetEnumerator(); + } + + public bool Remove(string key) + { + return backing.Remove(key); + } + + public bool Remove(KeyValuePair item) + { + return backing.Remove(item); + } + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out int value) + { + return backing.TryGetValue(key, out value); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return ((IEnumerable)backing).GetEnumerator(); + } } - class F + abstract class CustomAbstractDictionary : IDictionary { - public F(string type, E e) { } + public abstract int this[string key] { get; set; } + + public abstract ICollection Keys { get; } + public abstract ICollection Values { get; } + public abstract int Count { get; } + public abstract bool IsReadOnly { get; } + + public abstract void Add(string key, int value); + public abstract void Add(KeyValuePair item); + public abstract void Clear(); + public abstract bool Contains(KeyValuePair item); + public abstract bool ContainsKey(string key); + public abstract void CopyTo(KeyValuePair[] array, int arrayIndex); + public abstract IEnumerator> GetEnumerator(); + public abstract bool Remove(string key); + public abstract bool Remove(KeyValuePair item); + public abstract bool TryGetValue(string key, [MaybeNullWhen(false)] out int value); + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } } - class G + [Fact] + public void ConvertToCustomAbstractDictionaryThrows() { - public G() { } - public G(int a = 1, int b = 2) { } + var section = JsonStringConfigSource.LoadSection(""" + { + "Container": { + "a": 1, + "b": 2 + } + } + """, "Container"); + var value = new ObjectArgumentValue(section, []); + + Assert.Throws(() => value.ConvertTo(typeof(CustomAbstractDictionary), new())); + } + + class CustomReadOnlyDictionary : IReadOnlyDictionary + { + public int this[string key] => throw new NotImplementedException(); + + public IEnumerable Keys => throw new NotImplementedException(); + + public IEnumerable Values => throw new NotImplementedException(); + + public int Count => throw new NotImplementedException(); + + public bool ContainsKey(string key) + { + throw new NotImplementedException(); + } + + public IEnumerator> GetEnumerator() + { + throw new NotImplementedException(); + } + + public bool TryGetValue(string key, [MaybeNullWhen(false)] out int value) + { + throw new NotImplementedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + throw new NotImplementedException(); + } + } + + [Fact] + public void ConvertToCustomReadOnlyDictionaryCreatesEmpty() + { + var section = JsonStringConfigSource.LoadSection(""" + { + "Container": { + "a": 1, + "b": 2 + } + } + """, "Container"); + var value = new ObjectArgumentValue(section, []); + + ConvertToReturnsType(value); + } + + class PrivateImplWithPublicCtor : AnAbstractClass, IAmAnInterface + { + } + + [Theory] + [InlineData(typeof(AbstractClass), typeof(ConcreteClass))] + [InlineData(typeof(IAmAnInterface), typeof(PrivateImplWithPublicCtor))] + [InlineData(typeof(AnAbstractClass), typeof(PrivateImplWithPublicCtor))] + [InlineData(typeof(AConcreteClass), typeof(ConcreteImplOfConcreteClass))] + public void ConvertToExplicitType(Type targetType, Type expectedType) + { + var section = JsonStringConfigSource.LoadSection($$""" + { + "Ctor": { "type": "{{expectedType.AssemblyQualifiedName}}"} + } + """, "Ctor"); + var value = new ObjectArgumentValue(section, []); + + var result = value.ConvertTo(targetType, new()); + + Assert.IsType(expectedType, result); + } + + class WithTypeArgumentClassCtor : AnAbstractClass + { + public string Type { get; } + + public WithTypeArgumentClassCtor(string type) { Type = type; } + } + + [Fact] + public void ConvertToExplicitTypeUsingTypeAsConstructorArgument() + { + var section = JsonStringConfigSource.LoadSection(""" + { + "Ctor": { + "$type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+WithTypeArgumentClassCtor, Serilog.Settings.Configuration.Tests", + "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+PrivateImplWithPublicCtor, Serilog.Settings.Configuration.Tests", + } + } + """, "Ctor"); + var value = new ObjectArgumentValue(section, []); + + var result = value.ConvertTo(typeof(AnAbstractClass), new()); + + var actual = Assert.IsType(result); + Assert.Equal("Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+PrivateImplWithPublicCtor, Serilog.Settings.Configuration.Tests", actual.Type); + } + + class WithOverloads : IAmAnInterface + { + public int A { get; } + public TimeSpan B { get; } + public Uri? C { get; } + public string? D { get; } + + public WithOverloads(int a, TimeSpan b, Uri c) + { + A = a; + B = b; + C = c; + } + + public WithOverloads(int a, TimeSpan b, Uri c, string d = "d") + { + A = a; + B = b; + C = c; + D = d; + } + } + + [Theory] + [InlineData("", null)] + [InlineData(",\"d\": \"DValue\"", "DValue")] + public void ConvertToExplicitTypePickingConstructorOverloadWithMostMatchingArguments(string dJson, string? d) + { + var section = JsonStringConfigSource.LoadSection($$""" + { + "Ctor": { + "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+WithOverloads, Serilog.Settings.Configuration.Tests", + "a": 1, + "b": "23:59:59", + "c": "http://dot.com/" + {{dJson}} + } + } + """, "Ctor"); + var value = new ObjectArgumentValue(section, []); + + var result = value.ConvertTo(typeof(IAmAnInterface), new()); + + var actual = Assert.IsType(result); + Assert.Equal(1, actual.A); + Assert.Equal(new TimeSpan(23, 59, 59), actual.B); + Assert.Equal(new Uri("http://dot.com/"), actual.C); + Assert.Equal(d, actual.D); + } + + [Fact] + public void ConvertToExplicitTypeMatchingArgumentsCaseInsensitively() + { + var section = JsonStringConfigSource.LoadSection($$""" + { + "Ctor": { + "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+WithOverloads, Serilog.Settings.Configuration.Tests", + "A": 1, + "B": "23:59:59", + "C": "http://dot.com/" + } + } + """, "Ctor"); + var value = new ObjectArgumentValue(section, []); + + var result = value.ConvertTo(typeof(IAmAnInterface), new()); + + var actual = Assert.IsType(result); + Assert.Equal(1, actual.A); + Assert.Equal(new TimeSpan(23, 59, 59), actual.B); + Assert.Equal(new Uri("http://dot.com/"), actual.C); + } + + class WithSimilarOverloads : IAmAnInterface + { + public object A { get; } + public object B { get; } + public object C { get; } + public int D { get; } + + public WithSimilarOverloads(int a, int b, int c, int d = 1) { A = a; B = b; C = c; D = d; } + public WithSimilarOverloads(int a, string b, string c) { A = a; B = b; C = c; D = 2; } + public WithSimilarOverloads(string a, string b, string c) { A = a; B = b; C = c; D = 3; } + } + + [Fact] + public void ConvertToExplicitTypePickingConstructorOverloadWithMostStrings() + { + var section = JsonStringConfigSource.LoadSection(""" + { + "Ctor": { + "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+WithSimilarOverloads, Serilog.Settings.Configuration.Tests", + "a": 1, + "b": 2, + "c": 3 + } + } + """, "Ctor"); + var value = new ObjectArgumentValue(section, []); + + var result = value.ConvertTo(typeof(IAmAnInterface), new()); + var actual = Assert.IsType(result); + + Assert.Equal("1", actual.A); + Assert.Equal("2", actual.B); + Assert.Equal("3", actual.C); + Assert.Equal(3, actual.D); + } + +#if NET7_0_OR_GREATER + class OnlyDifferentTypeOverloads : IAmAnInterface + { + public object Value { get; } + + public OnlyDifferentTypeOverloads(int value) { Value = value; } + public OnlyDifferentTypeOverloads(long value) { Value = value; } + } + + // Is only guaranteed to work when Type.GetConstructors returns constructors in a deterministic order + // This is only the case since .Net 7 + [Fact] + public void ConvertToExplicitTypePickingFirstMatchWhenOtherwiseAmbiguous() + { + var section = JsonStringConfigSource.LoadSection(""" + { + "Ctor": { + "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+OnlyDifferentTypeOverloads, Serilog.Settings.Configuration.Tests", + "value": 123, + } + } + """, "Ctor"); + var value = new ObjectArgumentValue(section, []); + + var result = value.ConvertTo(typeof(IAmAnInterface), new()); + var actual = Assert.IsType(result); + + Assert.Equal(123, actual.Value); + } +#endif + + class WithDefaults : IAmAnInterface + { + public int A { get; } + public int B { get; } + public int C { get; } + + public WithDefaults(int a, int b = 2, int c = 3) + { + A = a; + B = b; + C = c; + } + } + + [Theory] + [InlineData("", 2, 3)] + [InlineData(",\"b\": 5", 5, 3)] + [InlineData(",\"c\": 6", 2, 6)] + [InlineData(",\"b\": 7, \"c\": 8", 7, 8)] + public void ConvertToExplicitTypeFillingInDefaultsInConstructor(string json, int b, int c) + { + var section = JsonStringConfigSource.LoadSection($$""" + { + "Ctor": { + "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+WithDefaults, Serilog.Settings.Configuration.Tests", + "a": 1 + {{json}} + } + } + """, "Ctor"); + var value = new ObjectArgumentValue(section, []); + + var result = value.ConvertTo(typeof(IAmAnInterface), new()); + + var actual = Assert.IsType(result); + Assert.Equal(1, actual.A); + Assert.Equal(b, actual.B); + Assert.Equal(c, actual.C); + } + + class WithParamsArray : IAmAnInterface + { + public IReadOnlyList Values { get; } + + public WithParamsArray(params int[] values) { Values = values; } + } + + [Fact] + public void ConvertToExplicitTypeWithExplicitTypeConstructorArgument() + { + var section = JsonStringConfigSource.LoadSection(""" + { + "Ctor": { + "type": "Serilog.Settings.Configuration.Tests.Support.GenericClass`1[[Serilog.Settings.Configuration.Tests.Support.IAmAnInterface, Serilog.Settings.Configuration.Tests]], Serilog.Settings.Configuration.Tests", + "value": { + "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+PrivateImplWithPublicCtor, Serilog.Settings.Configuration.Tests" + } + } + } + """, "Ctor"); + var value = new ObjectArgumentValue(section, []); + + var result = value.ConvertTo(typeof(IAmAnInterface), new()); + + var actual = Assert.IsType>(result); + Assert.IsType(actual.Value); + } + + // While ObjectArgumentValue supports converting to primitives, this is normally handled by StringArgumentValue + // ObjectArgumentValue will not honor ConfigurationReaderOptions.FormatProvider, it will use InvariantCulture + [Theory] + [InlineData(typeof(bool), false, "false")] + [InlineData(typeof(bool), true, "true")] + [InlineData(typeof(sbyte), (sbyte)-1, "-1")] + [InlineData(typeof(byte), (byte)2, "2")] + [InlineData(typeof(short), (short)-3, "-3")] + [InlineData(typeof(ushort), (ushort)4, "4")] + [InlineData(typeof(int), -5, "-5")] + [InlineData(typeof(uint), 6U, "6")] + [InlineData(typeof(long), -7L, "-7")] + [InlineData(typeof(ulong), 8UL, "8")] + [InlineData(typeof(float), -9.1F, "-9.1")] + [InlineData(typeof(double), 10.2D, "10.2")] + public void ConvertToPrimitives(Type type, object expected, string sectionValue) + { + var section = JsonStringConfigSource.LoadSection($"{{ \"Serilog\": {sectionValue} }}", "Serilog"); + var value = new ObjectArgumentValue(section, []); + + var actual = value.ConvertTo(type, new()); + + Assert.Equal(expected, actual); + } + + // While ObjectArgumentValue supports converting to a nullable primitive, this is normally handled by StringArgumentValue + // ObjectArgumentValue will not honor ConfigurationReaderOptions.FormatProvider, it will use InvariantCulture + [Fact] + public void ConvertToNullablePrimitive() + { + var section = JsonStringConfigSource.LoadSection("{ \"Serilog\": 123 }", "Serilog"); + var value = new ObjectArgumentValue(section, []); + + var actual = value.ConvertTo(typeof(int?), new()); + + Assert.Equal(123, actual); + } + + // While ObjectArgumentValue supports converting to a nullable primitive, this is normally handled by StringArgumentValue + [Fact] + public void ConvertToNullWhenEmptyNullable() + { + var section = JsonStringConfigSource.LoadSection("{ \"Serilog\": null }", "Serilog"); + var value = new ObjectArgumentValue(section, []); + + var actual = value.ConvertTo(typeof(int?), new()); + + Assert.Null(actual); + } + + [Fact] + public void ConvertToPlainClass() + { + var section = JsonStringConfigSource.LoadSection("{ \"Serilog\": { \"foo\" : \"bar\" } }", "Serilog"); + var value = new ObjectArgumentValue(section, []); + + var actual = value.ConvertTo(typeof(TestDummies.DummyLoggerConfigurationExtensions.Binding), new()); + + var binding = Assert.IsType(actual); + Assert.Equal("bar", binding.Foo); + Assert.Null(binding.Abc); + } + + private struct PlainStruct + { + public string? A { get; set; } + public string? B { get; set; } + } + + [Fact] + public void ConvertToPlainStruct() + { + var section = JsonStringConfigSource.LoadSection("{ \"Serilog\": { \"A\" : \"1\" } }", "Serilog"); + var value = new ObjectArgumentValue(section, []); + + var actual = value.ConvertTo(typeof(PlainStruct), new()); + + var plain = Assert.IsType(actual); + Assert.Equal("1", plain.A); + Assert.Null(plain.B); + } + + // While ObjectArgumentValue supports this, a null value is normally handled by StringArgumentValue + // This is because IConfigurationSection will resolve null to an empty string + // This behavior is under review, see https://github.com/dotnet/runtime/issues/36510 + [Fact] + public void ConvertToNullWhenStructIsNull() + { + + var section = JsonStringConfigSource.LoadSection("{ \"Serilog\": null }", "Serilog"); + var value = new ObjectArgumentValue(section, []); + + var actual = value.ConvertTo(typeof(PlainStruct), new()); + + Assert.Null(actual); } } diff --git a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.json b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.json deleted file mode 100644 index f11c7ad..0000000 --- a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.json +++ /dev/null @@ -1,44 +0,0 @@ -{ - "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/StringArgumentValueTests.cs b/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs index c28b1bd..b97d4ca 100644 --- a/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs @@ -2,8 +2,9 @@ using Serilog.Events; using Serilog.Formatting; using Serilog.Formatting.Json; - using Serilog.Settings.Configuration.Tests.Support; +using System.Globalization; +using System.Reflection; namespace Serilog.Settings.Configuration.Tests; @@ -96,6 +97,17 @@ public void StaticMembersAccessorsCanBeUsedForAbstractTypes(string input, Type t Assert.Equal(ConcreteImpl.Instance, actual); } + [Fact] + public void StaticMembersAccessorsCanBeUsedForMethodInfoWhenThereAreNoOverloads() + { + var stringArgumentValue = new StringArgumentValue("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::IntParseMethodNoOverloads, Serilog.Settings.Configuration.Tests"); + + var actual = stringArgumentValue.ConvertTo(typeof(MethodInfo), new ResolutionContext()); + + var parser = Assert.IsAssignableFrom(actual); + Assert.Equal(100, parser.Invoke(null, ["100"])); + } + [Theory] [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::FuncIntParseField, Serilog.Settings.Configuration.Tests", typeof(Func))] [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::NamedIntParseField, Serilog.Settings.Configuration.Tests", typeof(NamedIntParse))] @@ -210,6 +222,66 @@ public void ReferencingUndeclaredLevelSwitchThrows() Assert.Contains("\"LevelSwitches\":{\"$mySwitch\":", ex.Message); } + [Fact] + public void StringValuesConvertToEnumByName() + { + var value = new StringArgumentValue(nameof(LogEventLevel.Information)); + + var actual = value.ConvertTo(typeof(LogEventLevel), new()); + + Assert.Equal(LogEventLevel.Information, actual); + } + + [Fact] + public void StringValuesConvertToEnumByValue() + { + var value = new StringArgumentValue("2"); + + var actual = value.ConvertTo(typeof(LogEventLevel), new()); + + Assert.Equal(LogEventLevel.Information, actual); + } + + [Fact] + public void StringValuesConvertToUnwrappedNullable() + { + var value = new StringArgumentValue("123"); + + var actual = value.ConvertTo(typeof(int?), new()); + + Assert.Equal(123, actual); + } + + [Fact] + public void StringValuesConvertToNullWhenEmptyNullable() + { + var value = new StringArgumentValue(""); + + var actual = value.ConvertTo(typeof(int?), new()); + + Assert.Null(actual); + } + + [Fact] + public void StringValuesConvertToUri() + { + var stringArgumentValue = new StringArgumentValue("https://test.local"); + + var actual = stringArgumentValue.ConvertTo(typeof(Uri), new ResolutionContext()); + + Assert.Equal(new Uri("https://test.local"), actual as Uri); + } + + [Fact] + public void StringValuesConvertToTimespan() + { + var stringArgumentValue = new StringArgumentValue("1.23:45:30.1234567"); + + var actual = stringArgumentValue.ConvertTo(typeof(TimeSpan), new ResolutionContext()); + + Assert.Equal(TimeSpan.Parse("1.23:45:30.1234567"), actual); + } + [Fact] public void StringValuesConvertToTypeFromShortTypeName() { @@ -231,4 +303,74 @@ public void StringValuesConvertToTypeFromAssemblyQualifiedName() Assert.Equal(typeof(Version), actual); } + + [Theory] + [InlineData(typeof(bool), false, "False")] + [InlineData(typeof(bool), true, "True")] + [InlineData(typeof(sbyte), (sbyte)-1, "-1")] + [InlineData(typeof(byte), (byte)2, "2")] + [InlineData(typeof(short), (short)-3, "-3")] + [InlineData(typeof(ushort), (ushort)4, "4")] + [InlineData(typeof(int), -5, "-5")] + [InlineData(typeof(uint), 6U, "6")] + [InlineData(typeof(long), -7L, "-7")] + [InlineData(typeof(ulong), 8UL, "8")] + [InlineData(typeof(float), -9.1F, "-9.1")] + [InlineData(typeof(double), 10.2D, "10.2")] + public void StringValuesConvertToPrimitives(Type type, object expected, string sectionValue) + { + var value = new StringArgumentValue(sectionValue); + + var actual = value.ConvertTo(type, new()); + + Assert.Equal(expected, actual); + } + + [Fact] + public void StringValuesConvertToPrimitivesUsingAlternativeFormatProvider() + { + var value = new StringArgumentValue("1.234,56"); + + var formatProvider = new NumberFormatInfo() + { + NumberDecimalSeparator = ",", + NumberGroupSeparator = ".", + NumberGroupSizes = [3], + }; + + var actual = value.ConvertTo(typeof(decimal), new(readerOptions: new() { FormatProvider = formatProvider })); + + Assert.Equal(1234.56M, actual); + } + + [Theory] + [InlineData("")] + [InlineData("Just some string that is hard to misinterpret")] + [InlineData("True")] + [InlineData("10")] + [InlineData("Information")] + [InlineData("1.23:45:30.1234567")] + [InlineData("https://test.local")] + [InlineData("Serilog.Formatting.Json.JsonFormatter")] + [InlineData("Serilog.Formatting.Json.JsonFormatter, Serilog")] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::IntProperty, Serilog.Settings.Configuration.Tests")] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::StringProperty, Serilog.Settings.Configuration.Tests")] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InterfaceProperty, Serilog.Settings.Configuration.Tests")] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::AbstractProperty, Serilog.Settings.Configuration.Tests")] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InterfaceField, Serilog.Settings.Configuration.Tests")] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::AbstractField, Serilog.Settings.Configuration.Tests")] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::FuncIntParseField, Serilog.Settings.Configuration.Tests")] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::NamedIntParseField, Serilog.Settings.Configuration.Tests")] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::FuncIntParseProperty, Serilog.Settings.Configuration.Tests")] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::NamedIntParseProperty, Serilog.Settings.Configuration.Tests")] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::IntParseMethod, Serilog.Settings.Configuration.Tests")] + public void StringValuesConvertToString(string expected) + { + var value = new StringArgumentValue(expected); + + var actual = value.ConvertTo(typeof(string), new()); + + Assert.Equal(expected, actual); + } + } diff --git a/test/Serilog.Settings.Configuration.Tests/Support/GenericClass.cs b/test/Serilog.Settings.Configuration.Tests/Support/GenericClass.cs new file mode 100644 index 0000000..5d007e2 --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/Support/GenericClass.cs @@ -0,0 +1,11 @@ +namespace Serilog.Settings.Configuration.Tests.Support; + +public class GenericClass : IAmAnInterface +{ + public T Value { get; } + + public GenericClass(T value) + { + Value = value; + } +} diff --git a/test/Serilog.Settings.Configuration.Tests/Support/StaticAccessorClasses.cs b/test/Serilog.Settings.Configuration.Tests/Support/StaticAccessorClasses.cs index babd2d6..b9728f2 100644 --- a/test/Serilog.Settings.Configuration.Tests/Support/StaticAccessorClasses.cs +++ b/test/Serilog.Settings.Configuration.Tests/Support/StaticAccessorClasses.cs @@ -56,4 +56,5 @@ public class ClassWithStaticAccessors public static int IntParseMethod(string value) => int.Parse(value); public static int IntParseMethod(string value, string otherValue) => throw new NotImplementedException(); // will not be chosen, extra parameter public static int IntParseMethod(object value) => throw new NotImplementedException(); // will not be chosen, wrong parameter type + public static int IntParseMethodNoOverloads(string value) => int.Parse(value); } From 1e5f6cbf458e2d5032db9cd3abfe37b5c4af5a72 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81dric=20Luthi?= Date: Mon, 19 Feb 2024 09:24:28 +0100 Subject: [PATCH 06/13] Reformat all JSON with raw literal strings for better readability Also some minor code cleanup --- .../ObjectArgumentValueTests.cs | 149 ++++++++++++++---- .../StringArgumentValueTests.cs | 3 +- 2 files changed, 115 insertions(+), 37 deletions(-) diff --git a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs index 7c354c4..8175077 100644 --- a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs @@ -5,7 +5,6 @@ using System.Collections; using System.Diagnostics.CodeAnalysis; using System.Globalization; -using System.Reflection; using TestDummies; using TestDummies.Console; @@ -17,7 +16,7 @@ namespace Serilog.Settings.Configuration.Tests; public class ObjectArgumentValueTests { - private static T ConvertToReturnsType(ObjectArgumentValue value, ResolutionContext? resolutionContext = null) + static T ConvertToReturnsType(ObjectArgumentValue value, ResolutionContext? resolutionContext = null) { return Assert.IsType(value.ConvertTo(typeof(T), resolutionContext ?? new())); } @@ -25,7 +24,12 @@ private static T ConvertToReturnsType(ObjectArgumentValue value, ResolutionCo [Fact] public void ConvertToIConfigurationSection() { - var section = JsonStringConfigSource.LoadSection("{ \"Serilog\": { } }", "Serilog"); + // language=json + var section = JsonStringConfigSource.LoadSection(""" + { + "Serilog": {} + } + """, "Serilog"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(IConfigurationSection), new()); @@ -36,6 +40,7 @@ public void ConvertToIConfigurationSection() [Fact] public void ConvertToLoggerConfigurationCallback() { + // language=json var section = JsonStringConfigSource.LoadSection(""" { "Serilog": { @@ -47,7 +52,7 @@ public void ConvertToLoggerConfigurationCallback() } } """, "Serilog"); - var value = new ObjectArgumentValue(section, [typeof(DummyRollingFileSink).GetTypeInfo().Assembly]); + var value = new ObjectArgumentValue(section, [typeof(DummyRollingFileSink).Assembly]); var configure = ConvertToReturnsType>(value); @@ -65,6 +70,7 @@ public void ConvertToLoggerConfigurationCallback() [Fact] public void ConvertToLoggerSinkConfigurationCallback() { + // language=json var section = JsonStringConfigSource.LoadSection(""" { "WriteTo": [{ @@ -75,7 +81,7 @@ public void ConvertToLoggerSinkConfigurationCallback() }] } """, "WriteTo"); - var value = new ObjectArgumentValue(section, [typeof(DummyConfigurationSink).GetTypeInfo().Assembly]); + var value = new ObjectArgumentValue(section, [typeof(DummyConfigurationSink).Assembly]); var configureSinks = ConvertToReturnsType>(value); @@ -94,6 +100,7 @@ public void ConvertToLoggerSinkConfigurationCallback() [Fact] public void ConvertToLoggerEnrichmentConfiguration() { + // language=json var section = JsonStringConfigSource.LoadSection(""" { "Enrich": [{ @@ -105,12 +112,7 @@ public void ConvertToLoggerEnrichmentConfiguration() }] } """, "Enrich"); - var value = new ObjectArgumentValue( - section, - [ - typeof(LoggerEnrichmentConfiguration).GetTypeInfo().Assembly, - typeof(DummyThreadIdEnricher).GetTypeInfo().Assembly - ]); + var value = new ObjectArgumentValue(section, [typeof(LoggerEnrichmentConfiguration).Assembly, typeof(DummyThreadIdEnricher).Assembly]); var configureEnrichment = ConvertToReturnsType>(value); @@ -132,6 +134,7 @@ public void ConvertToLoggerEnrichmentConfiguration() [Fact] public void ConvertToConfigurationCallbackThrows() { + // language=json var section = JsonStringConfigSource.LoadSection(""" { "Configure": {} @@ -147,7 +150,12 @@ public void ConvertToConfigurationCallbackThrows() [Fact] public void ConvertToArrayUsingStringArgumentValueForElements() { - var section = JsonStringConfigSource.LoadSection("{ \"Array\": [ \"Information\", 3, null ] }", "Array"); + // language=json + var section = JsonStringConfigSource.LoadSection(""" + { + "Array": [ "Information", 3, null ] + } + """, "Array"); var value = new ObjectArgumentValue(section, []); var array = ConvertToReturnsType(value); @@ -158,14 +166,20 @@ public void ConvertToArrayUsingStringArgumentValueForElements() [Fact] public void ConvertToArrayOfArraysPassingContext() { - var formatProvider = new NumberFormatInfo() + var formatProvider = new NumberFormatInfo { NumberDecimalSeparator = ",", NumberGroupSeparator = ".", NumberGroupSizes = [3], }; - var section = JsonStringConfigSource.LoadSection("{ \"Array\": [ [ 1, 2 ], [ 3, 4 ], [ \"1.234,56\" ] ] }", "Array"); + // language=json + var json = """ + { + "Array": [ [ 1, 2 ], [ 3, 4 ], [ "1.234,56" ] ] + } + """; + var section = JsonStringConfigSource.LoadSection(json, "Array"); var value = new ObjectArgumentValue(section, []); var array = ConvertToReturnsType(value, new(readerOptions: new() { FormatProvider = formatProvider })); @@ -176,12 +190,13 @@ public void ConvertToArrayOfArraysPassingContext() [Fact] public void ConvertToArrayRecursingObjectArgumentValuePassingAssemblies() { + // language=json var section = JsonStringConfigSource.LoadSection(""" { "Array": [{ "WriteTo": [{ "Name": "DummyConsole", "Args": {} }] }] } """, "Array"); - var value = new ObjectArgumentValue(section, [typeof(DummyConsoleSink).GetTypeInfo().Assembly]); + var value = new ObjectArgumentValue(section, [typeof(DummyConsoleSink).Assembly]); var configureCalls = ConvertToReturnsType[]>(value); @@ -199,6 +214,7 @@ public void ConvertToArrayRecursingObjectArgumentValuePassingAssemblies() [Fact] public void ConvertToArrayWithDifferentImplementations() { + // language=json var section = JsonStringConfigSource.LoadSection(""" { "Array": [ @@ -219,7 +235,12 @@ public void ConvertToArrayWithDifferentImplementations() [Fact] public void ConvertToContainerUsingStringArgumentValueForElements() { - var section = JsonStringConfigSource.LoadSection("{ \"List\": [ \"Information\", 3, null ] }", "List"); + // language=json + var section = JsonStringConfigSource.LoadSection(""" + { + "List": [ "Information", 3, null ] + } + """, "List"); var value = new ObjectArgumentValue(section, []); var list = ConvertToReturnsType>(value); @@ -230,14 +251,19 @@ public void ConvertToContainerUsingStringArgumentValueForElements() [Fact] public void ConvertToNestedContainerPassingContext() { - var formatProvider = new NumberFormatInfo() + var formatProvider = new NumberFormatInfo { NumberDecimalSeparator = ",", NumberGroupSeparator = ".", NumberGroupSizes = [3], }; - var section = JsonStringConfigSource.LoadSection("{ \"List\": [ [ 1, 2 ], [ 3, 4 ], [ \"1.234,56\" ] ] }", "List"); + // language=json + var section = JsonStringConfigSource.LoadSection(""" + { + "List": [ [ 1, 2 ], [ 3, 4 ], [ "1.234,56" ] ] + } + """, "List"); var value = new ObjectArgumentValue(section, []); var array = ConvertToReturnsType>>(value, new(readerOptions: new() { FormatProvider = formatProvider })); @@ -248,12 +274,13 @@ public void ConvertToNestedContainerPassingContext() [Fact] public void ConvertToContainerRecursingObjectArgumentValuePassingAssemblies() { + // language=json var section = JsonStringConfigSource.LoadSection(""" { "List": [{ "WriteTo": [{ "Name": "DummyConsole", "Args": {} }] }] } """, "List"); - var value = new ObjectArgumentValue(section, [typeof(DummyConsoleSink).GetTypeInfo().Assembly]); + var value = new ObjectArgumentValue(section, [typeof(DummyConsoleSink).Assembly]); var configureCalls = ConvertToReturnsType>>(value); @@ -271,6 +298,7 @@ public void ConvertToContainerRecursingObjectArgumentValuePassingAssemblies() [Fact] public void ConvertToListWithDifferentImplementations() { + // language=json var section = JsonStringConfigSource.LoadSection(""" { "List": [ @@ -304,6 +332,7 @@ IEnumerator IEnumerable.GetEnumerator() [Fact] public void ConvertToUnsupportedContainerWillBeCreatedButWillRemainEmpty() { + // language=json var section = JsonStringConfigSource.LoadSection(""" { "List": ["a", "b"] @@ -322,7 +351,12 @@ public void ConvertToUnsupportedContainerWillBeCreatedButWillRemainEmpty() [InlineData(typeof(List))] public void ConvertToContainerUsingList(Type containerType) { - var section = JsonStringConfigSource.LoadSection("{ \"Container\": [ 1, 1 ] }", "Container"); + // language=json + var section = JsonStringConfigSource.LoadSection(""" + { + "Container": [ 1, 1 ] + } + """, "Container"); var value = new ObjectArgumentValue(section, []); var container = value.ConvertTo(containerType, new()); @@ -334,11 +368,15 @@ public void ConvertToContainerUsingList(Type containerType) [Fact] public void ConvertToContainerUsingHashSet() { - var containerType = typeof(HashSet); - var section = JsonStringConfigSource.LoadSection("{ \"Container\": [ 1, 1, 2, 2 ] }", "Container"); + // language=json + var section = JsonStringConfigSource.LoadSection(""" + { + "Container": [ 1, 1, 2, 2 ] + } + """, "Container"); var value = new ObjectArgumentValue(section, []); - var container = value.ConvertTo(containerType, new()); + var container = value.ConvertTo(typeof(HashSet), new()); var set = Assert.IsType>(container); Assert.Equal([1, 2], set); @@ -350,6 +388,7 @@ public void ConvertToContainerUsingHashSet() [InlineData(typeof(Dictionary))] public void ConvertToContainerUsingDictionary(Type containerType) { + // language=json var section = JsonStringConfigSource.LoadSection(""" { "Container": { @@ -363,7 +402,7 @@ public void ConvertToContainerUsingDictionary(Type containerType) var container = value.ConvertTo(containerType, new()); var dictionary = Assert.IsType>(container); - Assert.Equal(new Dictionary() { { "a", 1 }, { "b", 2 } }, dictionary); + Assert.Equal(new Dictionary { { "a", 1 }, { "b", 2 } }, dictionary); } [Theory] @@ -372,6 +411,7 @@ public void ConvertToContainerUsingDictionary(Type containerType) [InlineData(typeof(Dictionary))] public void ConvertToContainerUsingDictionaryUsingStringArgumentValueToConvertKey(Type containerType) { + // language=json var section = JsonStringConfigSource.LoadSection(""" { "Container": { @@ -385,12 +425,12 @@ public void ConvertToContainerUsingDictionaryUsingStringArgumentValueToConvertKe var container = value.ConvertTo(containerType, new()); var dictionary = Assert.IsType>(container); - Assert.Equal(new Dictionary() { { 1, 2 }, { 3, 4 } }, dictionary); + Assert.Equal(new Dictionary { { 1, 2 }, { 3, 4 } }, dictionary); } class DictionaryWithoutPublicDefaultConstructor : IDictionary { - private readonly IDictionary backing; + readonly IDictionary backing; public int this[string key] { get => backing[key]; set => backing[key] = value; } @@ -490,6 +530,7 @@ IEnumerator IEnumerable.GetEnumerator() [Fact] public void ConvertToCustomAbstractDictionaryThrows() { + // language=json var section = JsonStringConfigSource.LoadSection(""" { "Container": { @@ -537,6 +578,7 @@ IEnumerator IEnumerable.GetEnumerator() [Fact] public void ConvertToCustomReadOnlyDictionaryCreatesEmpty() { + // language=json var section = JsonStringConfigSource.LoadSection(""" { "Container": { @@ -561,6 +603,7 @@ class PrivateImplWithPublicCtor : AnAbstractClass, IAmAnInterface [InlineData(typeof(AConcreteClass), typeof(ConcreteImplOfConcreteClass))] public void ConvertToExplicitType(Type targetType, Type expectedType) { + // language=json var section = JsonStringConfigSource.LoadSection($$""" { "Ctor": { "type": "{{expectedType.AssemblyQualifiedName}}"} @@ -583,6 +626,7 @@ class WithTypeArgumentClassCtor : AnAbstractClass [Fact] public void ConvertToExplicitTypeUsingTypeAsConstructorArgument() { + // language=json var section = JsonStringConfigSource.LoadSection(""" { "Ctor": { @@ -627,6 +671,7 @@ public WithOverloads(int a, TimeSpan b, Uri c, string d = "d") [InlineData(",\"d\": \"DValue\"", "DValue")] public void ConvertToExplicitTypePickingConstructorOverloadWithMostMatchingArguments(string dJson, string? d) { + // language=json var section = JsonStringConfigSource.LoadSection($$""" { "Ctor": { @@ -652,6 +697,7 @@ public void ConvertToExplicitTypePickingConstructorOverloadWithMostMatchingArgum [Fact] public void ConvertToExplicitTypeMatchingArgumentsCaseInsensitively() { + // language=json var section = JsonStringConfigSource.LoadSection($$""" { "Ctor": { @@ -687,6 +733,7 @@ class WithSimilarOverloads : IAmAnInterface [Fact] public void ConvertToExplicitTypePickingConstructorOverloadWithMostStrings() { + // language=json var section = JsonStringConfigSource.LoadSection(""" { "Ctor": { @@ -722,11 +769,12 @@ class OnlyDifferentTypeOverloads : IAmAnInterface [Fact] public void ConvertToExplicitTypePickingFirstMatchWhenOtherwiseAmbiguous() { + // language=json var section = JsonStringConfigSource.LoadSection(""" { "Ctor": { "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+OnlyDifferentTypeOverloads, Serilog.Settings.Configuration.Tests", - "value": 123, + "value": 123 } } """, "Ctor"); @@ -760,6 +808,7 @@ public WithDefaults(int a, int b = 2, int c = 3) [InlineData(",\"b\": 7, \"c\": 8", 7, 8)] public void ConvertToExplicitTypeFillingInDefaultsInConstructor(string json, int b, int c) { + // language=json var section = JsonStringConfigSource.LoadSection($$""" { "Ctor": { @@ -789,6 +838,7 @@ class WithParamsArray : IAmAnInterface [Fact] public void ConvertToExplicitTypeWithExplicitTypeConstructorArgument() { + // language=json var section = JsonStringConfigSource.LoadSection(""" { "Ctor": { @@ -824,7 +874,12 @@ public void ConvertToExplicitTypeWithExplicitTypeConstructorArgument() [InlineData(typeof(double), 10.2D, "10.2")] public void ConvertToPrimitives(Type type, object expected, string sectionValue) { - var section = JsonStringConfigSource.LoadSection($"{{ \"Serilog\": {sectionValue} }}", "Serilog"); + // language=json + var section = JsonStringConfigSource.LoadSection($$""" + { + "Serilog": {{sectionValue}} + } + """, "Serilog"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(type, new()); @@ -837,7 +892,12 @@ public void ConvertToPrimitives(Type type, object expected, string sectionValue) [Fact] public void ConvertToNullablePrimitive() { - var section = JsonStringConfigSource.LoadSection("{ \"Serilog\": 123 }", "Serilog"); + // language=json + var section = JsonStringConfigSource.LoadSection(""" + { + "Serilog": 123 + } + """, "Serilog"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(int?), new()); @@ -849,7 +909,12 @@ public void ConvertToNullablePrimitive() [Fact] public void ConvertToNullWhenEmptyNullable() { - var section = JsonStringConfigSource.LoadSection("{ \"Serilog\": null }", "Serilog"); + // language=json + var section = JsonStringConfigSource.LoadSection(""" + { + "Serilog": null + } + """, "Serilog"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(int?), new()); @@ -860,7 +925,12 @@ public void ConvertToNullWhenEmptyNullable() [Fact] public void ConvertToPlainClass() { - var section = JsonStringConfigSource.LoadSection("{ \"Serilog\": { \"foo\" : \"bar\" } }", "Serilog"); + // language=json + var section = JsonStringConfigSource.LoadSection(""" + { + "Serilog": { "foo" : "bar" } + } + """, "Serilog"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(TestDummies.DummyLoggerConfigurationExtensions.Binding), new()); @@ -870,7 +940,7 @@ public void ConvertToPlainClass() Assert.Null(binding.Abc); } - private struct PlainStruct + struct PlainStruct { public string? A { get; set; } public string? B { get; set; } @@ -879,7 +949,12 @@ private struct PlainStruct [Fact] public void ConvertToPlainStruct() { - var section = JsonStringConfigSource.LoadSection("{ \"Serilog\": { \"A\" : \"1\" } }", "Serilog"); + // language=json + var section = JsonStringConfigSource.LoadSection(""" + { + "Serilog": { "A" : "1" } + } + """, "Serilog"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(PlainStruct), new()); @@ -895,8 +970,12 @@ public void ConvertToPlainStruct() [Fact] public void ConvertToNullWhenStructIsNull() { - - var section = JsonStringConfigSource.LoadSection("{ \"Serilog\": null }", "Serilog"); + // language=json + var section = JsonStringConfigSource.LoadSection(""" + { + "Serilog": null + } + """, "Serilog"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(PlainStruct), new()); diff --git a/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs b/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs index b97d4ca..6ab7e5c 100644 --- a/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs @@ -331,7 +331,7 @@ public void StringValuesConvertToPrimitivesUsingAlternativeFormatProvider() { var value = new StringArgumentValue("1.234,56"); - var formatProvider = new NumberFormatInfo() + var formatProvider = new NumberFormatInfo { NumberDecimalSeparator = ",", NumberGroupSeparator = ".", @@ -372,5 +372,4 @@ public void StringValuesConvertToString(string expected) Assert.Equal(expected, actual); } - } From a6fe8ec2e2e3d0167239d911bad8a413a8ba6747 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ce=CC=81dric=20Luthi?= Date: Mon, 19 Feb 2024 09:43:00 +0100 Subject: [PATCH 07/13] More minor code cleanups --- .../ObjectArgumentValueTests.cs | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs index 8175077..49f8d5a 100644 --- a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs @@ -1,10 +1,9 @@ -using Microsoft.Extensions.Configuration; +using System.Collections; +using System.Globalization; +using Microsoft.Extensions.Configuration; using Serilog.Configuration; using Serilog.Events; using Serilog.Settings.Configuration.Tests.Support; -using System.Collections; -using System.Diagnostics.CodeAnalysis; -using System.Globalization; using TestDummies; using TestDummies.Console; @@ -490,7 +489,7 @@ public bool Remove(KeyValuePair item) return backing.Remove(item); } - public bool TryGetValue(string key, [MaybeNullWhen(false)] out int value) + public bool TryGetValue(string key, out int value) { return backing.TryGetValue(key, out value); } @@ -519,7 +518,7 @@ abstract class CustomAbstractDictionary : IDictionary public abstract IEnumerator> GetEnumerator(); public abstract bool Remove(string key); public abstract bool Remove(KeyValuePair item); - public abstract bool TryGetValue(string key, [MaybeNullWhen(false)] out int value); + public abstract bool TryGetValue(string key, out int value); IEnumerator IEnumerable.GetEnumerator() { @@ -564,7 +563,7 @@ public IEnumerator> GetEnumerator() throw new NotImplementedException(); } - public bool TryGetValue(string key, [MaybeNullWhen(false)] out int value) + public bool TryGetValue(string key, out int value) { throw new NotImplementedException(); } @@ -592,9 +591,7 @@ public void ConvertToCustomReadOnlyDictionaryCreatesEmpty() ConvertToReturnsType(value); } - class PrivateImplWithPublicCtor : AnAbstractClass, IAmAnInterface - { - } + class PrivateImplWithPublicCtor : AnAbstractClass, IAmAnInterface; [Theory] [InlineData(typeof(AbstractClass), typeof(ConcreteClass))] @@ -631,7 +628,7 @@ public void ConvertToExplicitTypeUsingTypeAsConstructorArgument() { "Ctor": { "$type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+WithTypeArgumentClassCtor, Serilog.Settings.Configuration.Tests", - "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+PrivateImplWithPublicCtor, Serilog.Settings.Configuration.Tests", + "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+PrivateImplWithPublicCtor, Serilog.Settings.Configuration.Tests" } } """, "Ctor"); @@ -698,7 +695,7 @@ public void ConvertToExplicitTypePickingConstructorOverloadWithMostMatchingArgum public void ConvertToExplicitTypeMatchingArgumentsCaseInsensitively() { // language=json - var section = JsonStringConfigSource.LoadSection($$""" + var section = JsonStringConfigSource.LoadSection(""" { "Ctor": { "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+WithOverloads, Serilog.Settings.Configuration.Tests", From d0a1ba01fb92bc92c0ad1c83dacc73e2aaa28c77 Mon Sep 17 00:00:00 2001 From: Christoffer Gersen Date: Sat, 24 Feb 2024 11:34:10 +0100 Subject: [PATCH 08/13] Support collections as constructor arguments --- .../Configuration/ObjectArgumentValue.cs | 222 ++++++++++----- .../ObjectArgumentValueTests.cs | 268 +++++++++++++++++- 2 files changed, 415 insertions(+), 75 deletions(-) diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs index 151ce5b..4751a2a 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs @@ -1,5 +1,4 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq.Expressions; using System.Reflection; using Microsoft.Extensions.Configuration; @@ -46,13 +45,16 @@ public ObjectArgumentValue(IConfigurationSection section, IReadOnlyCollection>(ctorExpression).Compile().Invoke(); - } + // Without a type explicitly specified, attempt to call ctor of toType + if (TryCallCtorImplicit(_section, toType, resolutionContext, out ctorResult)) + return ctorResult; // MS Config binding can work with a limited set of primitive types and collections return _section.Get(toType); @@ -76,33 +78,41 @@ bool TryCreateContainer([NotNullWhen(true)] 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; + if (IsConstructableDictionary(toType, elementType, out var concreteType, out var keyType, out var valueType, out var addMethod)) + { + result = Activator.CreateInstance(concreteType) ?? throw new InvalidOperationException($"Activator.CreateInstance returned null for {concreteType}"); - var configurationElements = _section.GetChildren().ToArray(); - result = Activator.CreateInstance(toType) ?? throw new InvalidOperationException($"Activator.CreateInstance returned null for {toType}"); + foreach (var section in _section.GetChildren()) + { + var argumentValue = ConfigurationReader.GetArgumentValue(section, _configurationAssemblies); + var key = new StringArgumentValue(section.Key).ConvertTo(keyType, resolutionContext); + var value = argumentValue.ConvertTo(valueType, resolutionContext); + addMethod.Invoke(result, new[] { key, value }); + } + return true; + } + else if (IsConstructableContainer(toType, elementType, out concreteType, out addMethod)) + { + result = Activator.CreateInstance(concreteType) ?? throw new InvalidOperationException($"Activator.CreateInstance returned null for {concreteType}"); - for (int i = 0; i < configurationElements.Length; ++i) + foreach (var section in _section.GetChildren()) + { + var argumentValue = ConfigurationReader.GetArgumentValue(section, _configurationAssemblies); + var value = argumentValue.ConvertTo(elementType, resolutionContext); + addMethod.Invoke(result, new[] { value }); + } + return true; + } + else { - var argumentValue = ConfigurationReader.GetArgumentValue(configurationElements[i], _configurationAssemblies); - var value = argumentValue.ConvertTo(elementType, resolutionContext); - addMethod.Invoke(result, new[] { value }); + return false; } - - return true; } } - internal static bool TryBuildCtorExpression( - IConfigurationSection section, Type parameterType, ResolutionContext resolutionContext, [NotNullWhen(true)] out NewExpression? ctorExpression) + bool TryCallCtorExplicit( + IConfigurationSection section, ResolutionContext resolutionContext, [NotNullWhen(true)] out object? value) { - ctorExpression = null; - var typeDirective = section.GetValue("$type") switch { not null => "$type", @@ -116,21 +126,39 @@ internal static bool TryBuildCtorExpression( var type = typeDirective switch { not null => Type.GetType(section.GetValue(typeDirective)!, throwOnError: false), - null => parameterType, + null => null, }; if (type is null or { IsAbstract: true }) { + value = null; return false; } + else + { + var suppliedArguments = section.GetChildren().Where(s => s.Key != typeDirective) + .ToDictionary(s => s.Key, StringComparer.OrdinalIgnoreCase); + return TryCallCtor(type, suppliedArguments, resolutionContext, out value); + } - var suppliedArguments = section.GetChildren().Where(s => s.Key != typeDirective) + } + + bool TryCallCtorImplicit( + IConfigurationSection section, Type parameterType, ResolutionContext resolutionContext, out object? value) + { + var suppliedArguments = section.GetChildren() .ToDictionary(s => s.Key, StringComparer.OrdinalIgnoreCase); + return TryCallCtor(parameterType, suppliedArguments, resolutionContext, out value); + } + + bool TryCallCtor(Type type, Dictionary suppliedArguments, ResolutionContext resolutionContext, [NotNullWhen(true)] out object? value) + { + value = null; if (suppliedArguments.Count == 0 && type.GetConstructor(Type.EmptyTypes) is ConstructorInfo parameterlessCtor) { - ctorExpression = Expression.New(parameterlessCtor); + value = parameterlessCtor.Invoke([]); return true; } @@ -163,76 +191,126 @@ where gr.All(z => z.argumentBindResult.success) return false; } - var ctorArguments = new List(); - foreach (var argumentValue in ctor.ArgumentValues) + var ctorArguments = new object?[ctor.ArgumentValues.Count]; + for (var i = 0; i < ctor.ArgumentValues.Count; i++) { - if (TryBindToCtorArgument(argumentValue.Value, argumentValue.Type, resolutionContext, out var argumentExpression)) + var argument = ctor.ArgumentValues[i]; + var valueValue = argument.Value; + if (valueValue is IConfigurationSection s) { - ctorArguments.Add(argumentExpression); - } - else - { - return false; + var argumentValue = ConfigurationReader.GetArgumentValue(s, _configurationAssemblies); + valueValue = argumentValue.ConvertTo(argument.Type, resolutionContext); } + ctorArguments[i] = valueValue; } - ctorExpression = Expression.New(ctor.ConstructorInfo, ctorArguments); + value = ctor.ConstructorInfo.Invoke(ctorArguments); return true; + } - static bool TryBindToCtorArgument(object value, Type type, ResolutionContext resolutionContext, [NotNullWhen(true)] out Expression? argumentExpression) + static bool IsContainer(Type type, [NotNullWhen(true)] out Type? elementType) + { + elementType = null; + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) { - argumentExpression = null; - - if (value is IConfigurationSection s) + elementType = type.GetGenericArguments()[0]; + return true; + } + foreach (var iface in type.GetInterfaces()) + { + if (iface.IsGenericType) { - if (s.Value is string argValue) + if (iface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) { - var stringArgumentValue = new StringArgumentValue(argValue); - try - { - argumentExpression = Expression.Constant( - stringArgumentValue.ConvertTo(type, resolutionContext), - type); - - return true; - } - catch (Exception) - { - return false; - } + elementType = iface.GetGenericArguments()[0]; + return true; } - else if (s.GetChildren().Any()) - { - if (TryBuildCtorExpression(s, type, resolutionContext, out var ctorExpression)) - { - argumentExpression = ctorExpression; - return true; - } + } + } - return false; + return false; + } + + static bool IsConstructableDictionary(Type type, Type elementType, [NotNullWhen(true)] out Type? concreteType, [NotNullWhen(true)] out Type? keyType, [NotNullWhen(true)] out Type? valueType, [NotNullWhen(true)] out MethodInfo? addMethod) + { + concreteType = null; + keyType = null; + valueType = null; + addMethod = null; + if (!elementType.IsGenericType || elementType.GetGenericTypeDefinition() != typeof(KeyValuePair<,>)) + { + return false; + } + var argumentTypes = elementType.GetGenericArguments(); + keyType = argumentTypes[0]; + valueType = argumentTypes[1]; + if (type.IsAbstract) + { + concreteType = typeof(Dictionary<,>).MakeGenericType(argumentTypes); + if (!type.IsAssignableFrom(concreteType)) + { + return false; + } + } + else + { + concreteType = type; + } + if (concreteType.GetConstructor(Type.EmptyTypes) == null) + { + return false; + } + foreach (var method in concreteType.GetMethods()) + { + if (!method.IsStatic && method.Name == "Add") + { + var parameters = method.GetParameters(); + if (parameters.Length == 2 && parameters[0].ParameterType == keyType && parameters[1].ParameterType == valueType) + { + addMethod = method; + return true; } } - - argumentExpression = Expression.Constant(value, type); - return true; } + return false; } - static bool IsContainer(Type type, [NotNullWhen(true)] out Type? elementType) + static bool IsConstructableContainer(Type type, Type elementType, [NotNullWhen(true)] out Type? concreteType, [NotNullWhen(true)] out MethodInfo? addMethod) { - elementType = null; - foreach (var iface in type.GetInterfaces()) + addMethod = null; + if (type.IsAbstract) { - if (iface.IsGenericType) + concreteType = typeof(List<>).MakeGenericType(elementType); + if (!type.IsAssignableFrom(concreteType)) { - if (iface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + concreteType = typeof(HashSet<>).MakeGenericType(elementType); + if (!type.IsAssignableFrom(concreteType)) { - elementType = iface.GetGenericArguments()[0]; + concreteType = null; + return false; + } + } + } + else + { + concreteType = type; + } + if (concreteType.GetConstructor(Type.EmptyTypes) == null) + { + return false; + } + foreach (var method in concreteType.GetMethods()) + { + if (!method.IsStatic && method.Name == "Add") + { + var parameters = method.GetParameters(); + if (parameters.Length == 1 && parameters[0].ParameterType == elementType) + { + addMethod = method; return true; } } } - return false; } } diff --git a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs index 49f8d5a..51ff402 100644 --- a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs @@ -345,8 +345,11 @@ public void ConvertToUnsupportedContainerWillBeCreatedButWillRemainEmpty() } [Theory] + [InlineData(typeof(IEnumerable))] [InlineData(typeof(ICollection))] + [InlineData(typeof(IReadOnlyCollection))] [InlineData(typeof(IList))] + [InlineData(typeof(IReadOnlyList))] [InlineData(typeof(List))] public void ConvertToContainerUsingList(Type containerType) { @@ -364,8 +367,13 @@ public void ConvertToContainerUsingList(Type containerType) Assert.Equal([1, 1], list); } - [Fact] - public void ConvertToContainerUsingHashSet() + [Theory] + [InlineData(typeof(ISet))] +#if NET5_0_OR_GREATER + [InlineData(typeof(IReadOnlySet))] +#endif + [InlineData(typeof(HashSet))] + public void ConvertToContainerUsingHashSet(Type containerType) { // language=json var section = JsonStringConfigSource.LoadSection(""" @@ -375,13 +383,48 @@ public void ConvertToContainerUsingHashSet() """, "Container"); var value = new ObjectArgumentValue(section, []); - var container = value.ConvertTo(typeof(HashSet), new()); + var container = value.ConvertTo(containerType, new()); var set = Assert.IsType>(container); Assert.Equal([1, 2], set); } + [Fact] + public void ConvertToForcedHashSetImplementationWithCustomComparer() + { + // In .Net Framework HashSet is not part of mscorlib, but inside System.Core + // As a result the type string "System.Collections.Generic.HashSet`1[[System.String]]" will fail + // Using AssemblyQualifiedName to automatically switch to the correct type string, depending of framework + + // language=json + var json = $$""" + { + "Container": + { + "type": "{{typeof(HashSet).AssemblyQualifiedName}}", + "collection": [ + "a", + "A", + "b", + "b" + ], + "comparer": "System.StringComparer::OrdinalIgnoreCase" + } + } + """; + var section = JsonStringConfigSource.LoadSection(json, "Container"); + var value = new ObjectArgumentValue(section, []); + + var container = value.ConvertTo(typeof(IEnumerable), new()); + + var set = Assert.IsType>(container); + Assert.Equal(["a", "b"], set); + } + [Theory] + [InlineData(typeof(IEnumerable>))] + [InlineData(typeof(ICollection>))] + [InlineData(typeof(IReadOnlyCollection>))] [InlineData(typeof(IDictionary))] [InlineData(typeof(IReadOnlyDictionary))] [InlineData(typeof(Dictionary))] @@ -405,6 +448,9 @@ public void ConvertToContainerUsingDictionary(Type containerType) } [Theory] + [InlineData(typeof(IEnumerable>))] + [InlineData(typeof(ICollection>))] + [InlineData(typeof(IReadOnlyCollection>))] [InlineData(typeof(IDictionary))] [InlineData(typeof(IReadOnlyDictionary))] [InlineData(typeof(Dictionary))] @@ -500,6 +546,29 @@ IEnumerator IEnumerable.GetEnumerator() } } + [Fact] + public void ConvertToContainerUsingDictionaryWithoutPublicDefaultConstructor() + { + // language=json + var json = """ + { + "Container": { + "values": + { + "a": 1, + "b": 2 + } + } + } + """; + var section = JsonStringConfigSource.LoadSection(json, "Container"); + var value = new ObjectArgumentValue(section, []); + + var dictionary = ConvertToReturnsType(value); + + Assert.Equal(new Dictionary { { "a", 1 }, { "b", 2 } }, dictionary); + } + abstract class CustomAbstractDictionary : IDictionary { public abstract int this[string key] { get; set; } @@ -832,6 +901,199 @@ class WithParamsArray : IAmAnInterface public WithParamsArray(params int[] values) { Values = values; } } + [Fact] + public void ConvertToExplicitTypeWithParamsConstructorArgument() + { + // language=json + var json = """ + { + "Ctor": { + "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+WithParamsArray, Serilog.Settings.Configuration.Tests", + "values": [1, 2, 3] + } + } + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); + var value = new ObjectArgumentValue(section, []); + + var result = value.ConvertTo(typeof(IAmAnInterface), new()); + + var actual = Assert.IsType(result); + Assert.Equal([1, 2, 3], actual.Values); + } + + [Theory] + [InlineData(typeof(IEnumerable))] + [InlineData(typeof(ICollection))] + [InlineData(typeof(IReadOnlyCollection))] + [InlineData(typeof(IList))] + [InlineData(typeof(IReadOnlyList))] + [InlineData(typeof(List))] + public void ConvertToExplicitTypeWithContainerConstructorArgument(Type containerType) + { + var expectedType = typeof(GenericClass<>).MakeGenericType(containerType); + var valueProp = expectedType.GetProperty(nameof(GenericClass.Value)); + + // language=json + var json = $$""" + { + "Ctor": { + "type": "Serilog.Settings.Configuration.Tests.Support.GenericClass`1[[{{containerType.AssemblyQualifiedName}}]], Serilog.Settings.Configuration.Tests", + "value": [1, 2, 3] + } + } + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); + var value = new ObjectArgumentValue(section, []); + + var result = value.ConvertTo(typeof(IAmAnInterface), new()); + + Assert.IsType(expectedType, result); + var list = Assert.IsType>(valueProp?.GetValue(result)); + Assert.Equal([1, 2, 3], list); + } + + [Theory] + [InlineData(typeof(ISet))] +#if NET5_0_OR_GREATER + [InlineData(typeof(IReadOnlySet))] +#endif + [InlineData(typeof(HashSet))] + public void ConvertToExplicitTypeWithSetConstructorArgument(Type containerType) + { + var expectedType = typeof(GenericClass<>).MakeGenericType(containerType); + var valueProp = expectedType.GetProperty(nameof(GenericClass.Value)); + + // language=json + var json = $$""" + { + "Ctor": { + "type": "Serilog.Settings.Configuration.Tests.Support.GenericClass`1[[{{containerType.AssemblyQualifiedName}}]], Serilog.Settings.Configuration.Tests", + "value": [ 1, 1, 2, 2 ] + } + } + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); + var value = new ObjectArgumentValue(section, []); + + var result = value.ConvertTo(typeof(IAmAnInterface), new()); + + Assert.IsType(expectedType, result); + var set = Assert.IsType>(valueProp?.GetValue(result)); + Assert.Equal([1, 2], set); + } + + + [Theory] + [InlineData(typeof(IEnumerable>))] + [InlineData(typeof(ICollection>))] + [InlineData(typeof(IReadOnlyCollection>))] + [InlineData(typeof(IDictionary))] + [InlineData(typeof(IReadOnlyDictionary))] + [InlineData(typeof(Dictionary))] + public void ConvertToExplicitTypeWithDictionaryConstructorArgument(Type containerType) + { + var expectedType = typeof(GenericClass<>).MakeGenericType(containerType); + var valueProp = expectedType.GetProperty(nameof(GenericClass.Value)); + + // language=json + var json = $$""" + { + "Ctor": { + "type": "Serilog.Settings.Configuration.Tests.Support.GenericClass`1[[{{containerType.AssemblyQualifiedName}}]], Serilog.Settings.Configuration.Tests", + "value": { + "a": 1, + "b": 2 + } + } + } + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); + var value = new ObjectArgumentValue(section, []); + + var result = value.ConvertTo(typeof(IAmAnInterface), new()); + + Assert.IsType(expectedType, result); + var dictionary = Assert.IsType>(valueProp?.GetValue(result)); + Assert.Equal(new Dictionary { { "a", 1 }, { "b", 2 } }, dictionary); + } + + [Fact] + public void ConvertToExplicitTypeWithStructConstructorArgument() + { + // language=json + var json = """ + { + "Ctor": { + "type": "Serilog.Settings.Configuration.Tests.Support.GenericClass`1[[Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+PlainStruct, Serilog.Settings.Configuration.Tests]], Serilog.Settings.Configuration.Tests", + "value": { "A" : "1" } + } + } + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); + var value = new ObjectArgumentValue(section, []); + + var result = value.ConvertTo(typeof(IAmAnInterface), new()); + + var actual = Assert.IsType>(result); + Assert.Equal("1", actual.Value.A); + Assert.Null(actual.Value.B); + } + + [Fact] + public void ConvertToExplicitTypeWithClassConstructorArgument() + { + // language=json + var json = """ + { + "Ctor": { + "type": "Serilog.Settings.Configuration.Tests.Support.GenericClass`1[[TestDummies.DummyLoggerConfigurationExtensions+Binding, TestDummies]], Serilog.Settings.Configuration.Tests", + "value": { "foo" : "bar" } + } + } + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); + var value = new ObjectArgumentValue(section, []); + + var result = value.ConvertTo(typeof(IAmAnInterface), new()); + + var actual = Assert.IsType>(result); + Assert.Equal("bar", actual.Value.Foo); + Assert.Null(actual.Value.Abc); + } + + readonly struct Struct : IAmAnInterface + { + public readonly string String { get; } + + public Struct(string str) { String = str; } + } + + [Fact] + public void ConvertToExplicitTypeWithExplicitStructConstructorArgument() + { + // language=json + var json = """ + { + "Ctor": { + "type": "Serilog.Settings.Configuration.Tests.Support.GenericClass`1[[Serilog.Settings.Configuration.Tests.Support.IAmAnInterface, Serilog.Settings.Configuration.Tests]], Serilog.Settings.Configuration.Tests", + "value": { + "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+Struct, Serilog.Settings.Configuration.Tests", + "str" : "abc" + } + } + } + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); + var value = new ObjectArgumentValue(section, []); + + var result = value.ConvertTo(typeof(IAmAnInterface), new()); + + var actual = Assert.IsType>(result); + var structValue = Assert.IsType(actual.Value); + Assert.Equal("abc", structValue.String); + } + [Fact] public void ConvertToExplicitTypeWithExplicitTypeConstructorArgument() { From 64a0130b0437e9c9fedffaa30d8f2d4d7cb5df6b Mon Sep 17 00:00:00 2001 From: Christoffer Gersen Date: Sat, 24 Feb 2024 11:38:21 +0100 Subject: [PATCH 09/13] Place JSON strings in variable before use To avoid JSON001 Invalid JSON pattern analyzer errors in IDE --- .../ObjectArgumentValueTests.cs | 169 +++++++++++------- 1 file changed, 101 insertions(+), 68 deletions(-) diff --git a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs index 51ff402..d038a6c 100644 --- a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs @@ -24,11 +24,12 @@ static T ConvertToReturnsType(ObjectArgumentValue value, ResolutionContext? r public void ConvertToIConfigurationSection() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Serilog": {} } - """, "Serilog"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Serilog"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(IConfigurationSection), new()); @@ -40,7 +41,7 @@ public void ConvertToIConfigurationSection() public void ConvertToLoggerConfigurationCallback() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Serilog": { "WriteTo": [{ @@ -50,7 +51,8 @@ public void ConvertToLoggerConfigurationCallback() "Enrich": ["WithDummyThreadId"] } } - """, "Serilog"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Serilog"); var value = new ObjectArgumentValue(section, [typeof(DummyRollingFileSink).Assembly]); var configure = ConvertToReturnsType>(value); @@ -70,7 +72,7 @@ public void ConvertToLoggerConfigurationCallback() public void ConvertToLoggerSinkConfigurationCallback() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "WriteTo": [{ "Name": "Dummy", @@ -79,7 +81,8 @@ public void ConvertToLoggerSinkConfigurationCallback() } }] } - """, "WriteTo"); + """; + var section = JsonStringConfigSource.LoadSection(json, "WriteTo"); var value = new ObjectArgumentValue(section, [typeof(DummyConfigurationSink).Assembly]); var configureSinks = ConvertToReturnsType>(value); @@ -100,7 +103,7 @@ public void ConvertToLoggerSinkConfigurationCallback() public void ConvertToLoggerEnrichmentConfiguration() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Enrich": [{ "Name": "AtLevel", @@ -110,7 +113,8 @@ public void ConvertToLoggerEnrichmentConfiguration() } }] } - """, "Enrich"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Enrich"); var value = new ObjectArgumentValue(section, [typeof(LoggerEnrichmentConfiguration).Assembly, typeof(DummyThreadIdEnricher).Assembly]); var configureEnrichment = ConvertToReturnsType>(value); @@ -134,11 +138,12 @@ public void ConvertToLoggerEnrichmentConfiguration() public void ConvertToConfigurationCallbackThrows() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Configure": {} } - """, "Configure"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Configure"); var value = new ObjectArgumentValue(section, []); var ex = Assert.Throws(() => value.ConvertTo(typeof(Action), new())); @@ -150,11 +155,12 @@ public void ConvertToConfigurationCallbackThrows() public void ConvertToArrayUsingStringArgumentValueForElements() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Array": [ "Information", 3, null ] } - """, "Array"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Array"); var value = new ObjectArgumentValue(section, []); var array = ConvertToReturnsType(value); @@ -190,11 +196,12 @@ public void ConvertToArrayOfArraysPassingContext() public void ConvertToArrayRecursingObjectArgumentValuePassingAssemblies() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Array": [{ "WriteTo": [{ "Name": "DummyConsole", "Args": {} }] }] } - """, "Array"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Array"); var value = new ObjectArgumentValue(section, [typeof(DummyConsoleSink).Assembly]); var configureCalls = ConvertToReturnsType[]>(value); @@ -214,14 +221,15 @@ public void ConvertToArrayRecursingObjectArgumentValuePassingAssemblies() public void ConvertToArrayWithDifferentImplementations() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Array": [ "Serilog.Settings.Configuration.Tests.Support.ConcreteImpl::Instance, Serilog.Settings.Configuration.Tests", "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+PrivateImplWithPublicCtor, Serilog.Settings.Configuration.Tests" ] } - """, "Array"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Array"); var value = new ObjectArgumentValue(section, []); var array = ConvertToReturnsType(value); @@ -235,11 +243,12 @@ public void ConvertToArrayWithDifferentImplementations() public void ConvertToContainerUsingStringArgumentValueForElements() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "List": [ "Information", 3, null ] } - """, "List"); + """; + var section = JsonStringConfigSource.LoadSection(json, "List"); var value = new ObjectArgumentValue(section, []); var list = ConvertToReturnsType>(value); @@ -258,11 +267,12 @@ public void ConvertToNestedContainerPassingContext() }; // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "List": [ [ 1, 2 ], [ 3, 4 ], [ "1.234,56" ] ] } - """, "List"); + """; + var section = JsonStringConfigSource.LoadSection(json, "List"); var value = new ObjectArgumentValue(section, []); var array = ConvertToReturnsType>>(value, new(readerOptions: new() { FormatProvider = formatProvider })); @@ -274,11 +284,12 @@ public void ConvertToNestedContainerPassingContext() public void ConvertToContainerRecursingObjectArgumentValuePassingAssemblies() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "List": [{ "WriteTo": [{ "Name": "DummyConsole", "Args": {} }] }] } - """, "List"); + """; + var section = JsonStringConfigSource.LoadSection(json, "List"); var value = new ObjectArgumentValue(section, [typeof(DummyConsoleSink).Assembly]); var configureCalls = ConvertToReturnsType>>(value); @@ -298,14 +309,15 @@ public void ConvertToContainerRecursingObjectArgumentValuePassingAssemblies() public void ConvertToListWithDifferentImplementations() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "List": [ "Serilog.Settings.Configuration.Tests.Support.ConcreteImpl::Instance, Serilog.Settings.Configuration.Tests", "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+PrivateImplWithPublicCtor, Serilog.Settings.Configuration.Tests" ] } - """, "List"); + """; + var section = JsonStringConfigSource.LoadSection(json, "List"); var value = new ObjectArgumentValue(section, []); var list = ConvertToReturnsType>(value); @@ -332,11 +344,12 @@ IEnumerator IEnumerable.GetEnumerator() public void ConvertToUnsupportedContainerWillBeCreatedButWillRemainEmpty() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "List": ["a", "b"] } - """, "List"); + """; + var section = JsonStringConfigSource.LoadSection(json, "List"); var value = new ObjectArgumentValue(section, []); var unsupported = ConvertToReturnsType(value); @@ -354,11 +367,12 @@ public void ConvertToUnsupportedContainerWillBeCreatedButWillRemainEmpty() public void ConvertToContainerUsingList(Type containerType) { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Container": [ 1, 1 ] } - """, "Container"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Container"); var value = new ObjectArgumentValue(section, []); var container = value.ConvertTo(containerType, new()); @@ -376,11 +390,12 @@ public void ConvertToContainerUsingList(Type containerType) public void ConvertToContainerUsingHashSet(Type containerType) { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Container": [ 1, 1, 2, 2 ] } - """, "Container"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Container"); var value = new ObjectArgumentValue(section, []); var container = value.ConvertTo(containerType, new()); @@ -431,14 +446,15 @@ public void ConvertToForcedHashSetImplementationWithCustomComparer() public void ConvertToContainerUsingDictionary(Type containerType) { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Container": { "a": 1, "b": 2 } } - """, "Container"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Container"); var value = new ObjectArgumentValue(section, []); var container = value.ConvertTo(containerType, new()); @@ -457,14 +473,15 @@ public void ConvertToContainerUsingDictionary(Type containerType) public void ConvertToContainerUsingDictionaryUsingStringArgumentValueToConvertKey(Type containerType) { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Container": { "1": 2, "3": 4 } } - """, "Container"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Container"); var value = new ObjectArgumentValue(section, []); var container = value.ConvertTo(containerType, new()); @@ -599,14 +616,15 @@ IEnumerator IEnumerable.GetEnumerator() public void ConvertToCustomAbstractDictionaryThrows() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Container": { "a": 1, "b": 2 } } - """, "Container"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Container"); var value = new ObjectArgumentValue(section, []); Assert.Throws(() => value.ConvertTo(typeof(CustomAbstractDictionary), new())); @@ -647,14 +665,15 @@ IEnumerator IEnumerable.GetEnumerator() public void ConvertToCustomReadOnlyDictionaryCreatesEmpty() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Container": { "a": 1, "b": 2 } } - """, "Container"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Container"); var value = new ObjectArgumentValue(section, []); ConvertToReturnsType(value); @@ -670,11 +689,12 @@ class PrivateImplWithPublicCtor : AnAbstractClass, IAmAnInterface; public void ConvertToExplicitType(Type targetType, Type expectedType) { // language=json - var section = JsonStringConfigSource.LoadSection($$""" + var json = $$""" { "Ctor": { "type": "{{expectedType.AssemblyQualifiedName}}"} } - """, "Ctor"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); var value = new ObjectArgumentValue(section, []); var result = value.ConvertTo(targetType, new()); @@ -693,14 +713,15 @@ class WithTypeArgumentClassCtor : AnAbstractClass public void ConvertToExplicitTypeUsingTypeAsConstructorArgument() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Ctor": { "$type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+WithTypeArgumentClassCtor, Serilog.Settings.Configuration.Tests", "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+PrivateImplWithPublicCtor, Serilog.Settings.Configuration.Tests" } } - """, "Ctor"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); var value = new ObjectArgumentValue(section, []); var result = value.ConvertTo(typeof(AnAbstractClass), new()); @@ -738,7 +759,7 @@ public WithOverloads(int a, TimeSpan b, Uri c, string d = "d") public void ConvertToExplicitTypePickingConstructorOverloadWithMostMatchingArguments(string dJson, string? d) { // language=json - var section = JsonStringConfigSource.LoadSection($$""" + var json = $$""" { "Ctor": { "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+WithOverloads, Serilog.Settings.Configuration.Tests", @@ -748,7 +769,8 @@ public void ConvertToExplicitTypePickingConstructorOverloadWithMostMatchingArgum {{dJson}} } } - """, "Ctor"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); var value = new ObjectArgumentValue(section, []); var result = value.ConvertTo(typeof(IAmAnInterface), new()); @@ -764,7 +786,7 @@ public void ConvertToExplicitTypePickingConstructorOverloadWithMostMatchingArgum public void ConvertToExplicitTypeMatchingArgumentsCaseInsensitively() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Ctor": { "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+WithOverloads, Serilog.Settings.Configuration.Tests", @@ -773,7 +795,8 @@ public void ConvertToExplicitTypeMatchingArgumentsCaseInsensitively() "C": "http://dot.com/" } } - """, "Ctor"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); var value = new ObjectArgumentValue(section, []); var result = value.ConvertTo(typeof(IAmAnInterface), new()); @@ -800,7 +823,7 @@ class WithSimilarOverloads : IAmAnInterface public void ConvertToExplicitTypePickingConstructorOverloadWithMostStrings() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Ctor": { "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+WithSimilarOverloads, Serilog.Settings.Configuration.Tests", @@ -809,7 +832,8 @@ public void ConvertToExplicitTypePickingConstructorOverloadWithMostStrings() "c": 3 } } - """, "Ctor"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); var value = new ObjectArgumentValue(section, []); var result = value.ConvertTo(typeof(IAmAnInterface), new()); @@ -836,14 +860,15 @@ class OnlyDifferentTypeOverloads : IAmAnInterface public void ConvertToExplicitTypePickingFirstMatchWhenOtherwiseAmbiguous() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Ctor": { "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+OnlyDifferentTypeOverloads, Serilog.Settings.Configuration.Tests", "value": 123 } } - """, "Ctor"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); var value = new ObjectArgumentValue(section, []); var result = value.ConvertTo(typeof(IAmAnInterface), new()); @@ -872,18 +897,19 @@ public WithDefaults(int a, int b = 2, int c = 3) [InlineData(",\"b\": 5", 5, 3)] [InlineData(",\"c\": 6", 2, 6)] [InlineData(",\"b\": 7, \"c\": 8", 7, 8)] - public void ConvertToExplicitTypeFillingInDefaultsInConstructor(string json, int b, int c) + public void ConvertToExplicitTypeFillingInDefaultsInConstructor(string jsonPart, int b, int c) { // language=json - var section = JsonStringConfigSource.LoadSection($$""" + var json = $$""" { "Ctor": { "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+WithDefaults, Serilog.Settings.Configuration.Tests", "a": 1 - {{json}} + {{jsonPart}} } } - """, "Ctor"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); var value = new ObjectArgumentValue(section, []); var result = value.ConvertTo(typeof(IAmAnInterface), new()); @@ -1098,7 +1124,7 @@ public void ConvertToExplicitTypeWithExplicitStructConstructorArgument() public void ConvertToExplicitTypeWithExplicitTypeConstructorArgument() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Ctor": { "type": "Serilog.Settings.Configuration.Tests.Support.GenericClass`1[[Serilog.Settings.Configuration.Tests.Support.IAmAnInterface, Serilog.Settings.Configuration.Tests]], Serilog.Settings.Configuration.Tests", @@ -1107,7 +1133,8 @@ public void ConvertToExplicitTypeWithExplicitTypeConstructorArgument() } } } - """, "Ctor"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); var value = new ObjectArgumentValue(section, []); var result = value.ConvertTo(typeof(IAmAnInterface), new()); @@ -1134,11 +1161,12 @@ public void ConvertToExplicitTypeWithExplicitTypeConstructorArgument() public void ConvertToPrimitives(Type type, object expected, string sectionValue) { // language=json - var section = JsonStringConfigSource.LoadSection($$""" + var json = $$""" { "Serilog": {{sectionValue}} } - """, "Serilog"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Serilog"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(type, new()); @@ -1152,11 +1180,12 @@ public void ConvertToPrimitives(Type type, object expected, string sectionValue) public void ConvertToNullablePrimitive() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Serilog": 123 } - """, "Serilog"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Serilog"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(int?), new()); @@ -1169,11 +1198,12 @@ public void ConvertToNullablePrimitive() public void ConvertToNullWhenEmptyNullable() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Serilog": null } - """, "Serilog"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Serilog"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(int?), new()); @@ -1185,11 +1215,12 @@ public void ConvertToNullWhenEmptyNullable() public void ConvertToPlainClass() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Serilog": { "foo" : "bar" } } - """, "Serilog"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Serilog"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(TestDummies.DummyLoggerConfigurationExtensions.Binding), new()); @@ -1209,11 +1240,12 @@ struct PlainStruct public void ConvertToPlainStruct() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Serilog": { "A" : "1" } } - """, "Serilog"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Serilog"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(PlainStruct), new()); @@ -1230,11 +1262,12 @@ public void ConvertToPlainStruct() public void ConvertToNullWhenStructIsNull() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + var json = """ { "Serilog": null } - """, "Serilog"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Serilog"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(PlainStruct), new()); From 5847ea74a305fe3e2075466c2fc84975e49ac547 Mon Sep 17 00:00:00 2001 From: Eric Hiller Date: Sat, 24 Feb 2024 09:51:18 -0600 Subject: [PATCH 10/13] Update README.md Correct JSON for Destructuring example --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index b0efd41..8fb3e07 100644 --- a/README.md +++ b/README.md @@ -328,13 +328,13 @@ Destructuring means extracting pieces of information from an object and create p { "Name": "With", "Args": { - "policy": "policy": "MySecondNamespace.SecondDestructuringPolicy, MySecondAssembly" + "policy": "MySecondNamespace.SecondDestructuringPolicy, MySecondAssembly" } }, { "Name": "With", "Args": { - "policy": "policy": "MyThirdNamespace.ThirdDestructuringPolicy, MyThirdAssembly" + "policy": "MyThirdNamespace.ThirdDestructuringPolicy, MyThirdAssembly" } }, ], From eca00c29d4dd85d6e7488098e5a1713052f07d7a Mon Sep 17 00:00:00 2001 From: eleazarcelis Date: Wed, 20 Mar 2024 11:29:34 -0300 Subject: [PATCH 11/13] Clarification of the location of the 'Serilog' section in appsettings.json file. --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 8fb3e07..2eb4531 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ A Serilog settings provider that reads from [Microsoft.Extensions.Configuration](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1) sources, including .NET Core's `appsettings.json` file. -By default, configuration is read from the `Serilog` section. +By default, configuration is read from the `Serilog` section that should be at the **top level** of the configuration file. ```json { From 39c07a90de0dabf2223bfaaa24a50fffc520857d Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 13 May 2024 16:26:37 +1000 Subject: [PATCH 12/13] Minor version bump - new features incoming --- .../Serilog.Settings.Configuration.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj index 8e5a021..c8338fc 100644 --- a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj +++ b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj @@ -3,7 +3,7 @@ Microsoft.Extensions.Configuration (appsettings.json) support for Serilog. - 8.0.1 + 8.1.0 Serilog Contributors From cfe1b523c3354f153a14a93796ec9a8d8ab70ea4 Mon Sep 17 00:00:00 2001 From: Nicholas Blumhardt Date: Mon, 13 May 2024 16:29:17 +1000 Subject: [PATCH 13/13] Revert minor version bump - forgot about the versioning policy used here :) [skip ci] --- .../Serilog.Settings.Configuration.csproj | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj index c8338fc..8e5a021 100644 --- a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj +++ b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj @@ -3,7 +3,7 @@ Microsoft.Extensions.Configuration (appsettings.json) support for Serilog. - 8.1.0 + 8.0.1 Serilog Contributors