diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 0000000..acd3bc6 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,41 @@ +# If this file is renamed, the incrementing run attempt number will be reset. + +name: CI + +on: + push: + branches: [ "dev", "main" ] + pull_request: + branches: [ "dev", "main" ] + +env: + CI_BUILD_NUMBER_BASE: ${{ github.run_number }} + CI_TARGET_BRANCH: ${{ github.head_ref || github.ref_name }} + +jobs: + build: + + # The build must run on Windows so that .NET Framework targets can be built and tested. + runs-on: windows-latest + + permissions: + contents: write + + steps: + - uses: actions/checkout@v4 + - name: Setup + uses: actions/setup-dotnet@v4 + with: + dotnet-version: 9.0.x + - name: Compute build number + shell: bash + run: | + echo "CI_BUILD_NUMBER=$(($CI_BUILD_NUMBER_BASE+2300))" >> $GITHUB_ENV + - name: Build and Publish + env: + DOTNET_CLI_TELEMETRY_OPTOUT: true + NUGET_API_KEY: ${{ secrets.NUGET_API_KEY }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + shell: pwsh + run: | + ./Build.ps1 diff --git a/.gitignore b/.gitignore index 80de031..318ac62 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ bin/ obj/ test/ artifacts/ + +.DS_Store diff --git a/Build.ps1 b/Build.ps1 index 1513382..e798284 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -1,38 +1,79 @@ +Write-Output "build: Tool versions follow" + +dotnet --version +dotnet --list-sdks + Write-Output "build: Build started" Push-Location $PSScriptRoot +try { + if(Test-Path .\artifacts) { + Write-Output "build: Cleaning ./artifacts" + Remove-Item ./artifacts -Force -Recurse + } -if(Test-Path .\artifacts) { - Write-Output "build: Cleaning .\artifacts" - Remove-Item .\artifacts -Force -Recurse -} + & dotnet restore --no-cache -$branch = @{ $true = $env:APPVEYOR_REPO_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$env:APPVEYOR_REPO_BRANCH -ne $NULL]; -$revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:APPVEYOR_BUILD_NUMBER, 10); $false = "local" }[$env:APPVEYOR_BUILD_NUMBER -ne $NULL]; -$suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)))-$revision"}[$branch -eq "main" -and $revision -ne "local"] -$commitHash = $(git rev-parse --short HEAD) -$buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""] + $dbp = [Xml] (Get-Content .\Directory.Version.props) + $versionPrefix = $dbp.Project.PropertyGroup.VersionPrefix -Write-Output "build: Package version suffix is $suffix" -Write-Output "build: Build version suffix is $buildSuffix" + Write-Output "build: Package version prefix is $versionPrefix" -& dotnet build --configuration Release --version-suffix=$buildSuffix /p:ContinuousIntegrationBuild=true + $branch = @{ $true = $env:CI_TARGET_BRANCH; $false = $(git symbolic-ref --short -q HEAD) }[$NULL -ne $env:CI_TARGET_BRANCH]; + $revision = @{ $true = "{0:00000}" -f [convert]::ToInt32("0" + $env:CI_BUILD_NUMBER, 10); $false = "local" }[$NULL -ne $env:CI_BUILD_NUMBER]; + $suffix = @{ $true = ""; $false = "$($branch.Substring(0, [math]::Min(10,$branch.Length)) -replace '([^a-zA-Z0-9\-]*)', '')-$revision"}[$branch -eq "main" -and $revision -ne "local"] + $commitHash = $(git rev-parse --short HEAD) + $buildSuffix = @{ $true = "$($suffix)-$($commitHash)"; $false = "$($branch)-$($commitHash)" }[$suffix -ne ""] -if($LASTEXITCODE -ne 0) { throw 'build failed' } + Write-Output "build: Package version suffix is $suffix" + Write-Output "build: Build version suffix is $buildSuffix" -if($suffix) { - & dotnet pack src\Serilog.Settings.Configuration --configuration Release --no-build --no-restore -o artifacts --version-suffix=$suffix -} else { - & dotnet pack src\Serilog.Settings.Configuration --configuration Release --no-build --no-restore -o artifacts -} + & dotnet build -c Release --version-suffix=$buildSuffix /p:ContinuousIntegrationBuild=true + if($LASTEXITCODE -ne 0) { throw "Build failed" } + + foreach ($src in Get-ChildItem src/*) { + Push-Location $src + + Write-Output "build: Packaging project in $src" + + if ($suffix) { + & dotnet pack -c Release --no-build --no-restore -o ../../artifacts --version-suffix=$suffix + } else { + & dotnet pack -c Release --no-build --no-restore -o ../../artifacts + } + if($LASTEXITCODE -ne 0) { throw "Packaging failed" } -if($LASTEXITCODE -ne 0) { throw 'pack failed' } + Pop-Location + } -Write-Output "build: Testing" + foreach ($test in Get-ChildItem test/*.Tests) { + Push-Location $test -# 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 + Write-Output "build: Testing project in $test" -if($LASTEXITCODE -ne 0) { throw 'unit tests failed' } \ No newline at end of file + & dotnet test -c Release --no-build --no-restore + if($LASTEXITCODE -ne 0) { throw "Testing failed" } + + Pop-Location + } + + if ($env:NUGET_API_KEY) { + # GitHub Actions will only supply this to branch builds and not PRs. We publish + # builds from any branch this action targets (i.e. main and dev). + + Write-Output "build: Publishing NuGet packages" + + foreach ($nupkg in Get-ChildItem artifacts/*.nupkg) { + & dotnet nuget push -k $env:NUGET_API_KEY -s https://api.nuget.org/v3/index.json "$nupkg" + if($LASTEXITCODE -ne 0) { throw "Publishing failed" } + } + + if (!($suffix)) { + Write-Output "build: Creating release for version $versionPrefix" + + iex "gh release create v$versionPrefix --title v$versionPrefix --generate-notes $(get-item ./artifacts/*.nupkg) $(get-item ./artifacts/*.snupkg)" + } + } +} finally { + Pop-Location +} diff --git a/Directory.Build.props b/Directory.Build.props index 736814b..c114992 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,11 +1,25 @@ + + latest True - true + + true $(MSBuildThisFileDirectory)assets/Serilog.snk - enable false enable + enable + true + true + true + true + snupkg + + + + + diff --git a/Directory.Version.props b/Directory.Version.props new file mode 100644 index 0000000..8c3cc16 --- /dev/null +++ b/Directory.Version.props @@ -0,0 +1,6 @@ + + + + 9.0.0 + + diff --git a/README.md b/README.md index 8fb3e07..17d3910 100644 --- a/README.md +++ b/README.md @@ -1,8 +1,8 @@ -# Serilog.Settings.Configuration [![Build status](https://ci.appveyor.com/api/projects/status/r2bgfimd9ocr61px/branch/master?svg=true)](https://ci.appveyor.com/project/serilog/serilog-settings-configuration/branch/master) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Settings.Configuration.svg?style=flat)](https://www.nuget.org/packages/Serilog.Settings.Configuration/) +# Serilog.Settings.Configuration [![Build status](https://github.com/serilog/serilog-settings-configuration/actions/workflows/ci.yml/badge.svg?branch=dev)](https://github.com/serilog/serilog-settings-configuration/actions) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Settings.Configuration.svg?style=flat)](https://www.nuget.org/packages/Serilog.Settings.Configuration/) 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 { @@ -349,7 +349,7 @@ public record MyDto(int Id, int Name); public class FirstDestructuringPolicy : IDestructuringPolicy { - public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, + public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, [NotNullWhen(true)] out LogEventPropertyValue? result) { if (value is not MyDto dto) diff --git a/appveyor.yml b/appveyor.yml deleted file mode 100644 index eb9d04e..0000000 --- a/appveyor.yml +++ /dev/null @@ -1,52 +0,0 @@ -version: '{build}' - -skip_tags: true - -image: - - Visual Studio 2022 - - Ubuntu - - macOS - -build_script: -- pwsh: | - if ($isWindows) { - Invoke-WebRequest "https://dot.net/v1/dotnet-install.ps1" -OutFile "./dotnet-install.ps1" - ./dotnet-install.ps1 -JSonFile global.json -Architecture x64 -InstallDir 'C:\Program Files\dotnet' - ./Build.ps1 - } - if ($isLinux) { - Invoke-WebRequest "https://dot.net/v1/dotnet-install.sh" -OutFile "./dotnet-install.sh" - sudo chmod u+x dotnet-install.sh - sudo ./dotnet-install.sh --jsonfile global.json --architecture x64 --install-dir '/usr/share/dotnet' - ./Build.ps1 - } - if ($isMacOS) { - Invoke-WebRequest "https://dot.net/v1/dotnet-install.sh" -OutFile "./dotnet-install.sh" - sudo chmod u+x dotnet-install.sh - sudo ./dotnet-install.sh --jsonfile global.json --architecture x64 --install-dir '/usr/local/share/dotnet' - ./Build.ps1 - } - -test: off - -artifacts: -- path: artifacts/Serilog.*.nupkg -- path: artifacts/Serilog.*.snupkg - -deploy: -- provider: NuGet - api_key: - secure: JIfNMRv3l/2dmM/i//mpeEKqgxyEcnGr8XFlEoSDgp2JDVmRP8nUxc4gYznBvXQV - on: - branch: /^(main|dev)$/ - OS: Windows_NT -- provider: GitHub - auth_token: - secure: p4LpVhBKxGS5WqucHxFQ5c7C8cP74kbNB0Z8k9Oxx/PMaDQ1+ibmoexNqVU5ZlmX - artifacts: - /Serilog.*\.nupkg/ - /Serilog.*\.snupkg/ - tag: v$(appveyor_build_version) - on: - branch: main - OS: Windows_NT diff --git a/global.json b/global.json index b7e3357..db8627a 100644 --- a/global.json +++ b/global.json @@ -1,6 +1,6 @@ { "sdk": { - "version": "8.0.100", + "version": "9.0.100", "allowPrerelease": false, "rollForward": "latestFeature" } diff --git a/sample/Sample/Sample.csproj b/sample/Sample/Sample.csproj index 4e8277a..1c8a2a7 100644 --- a/sample/Sample/Sample.csproj +++ b/sample/Sample/Sample.csproj @@ -1,8 +1,9 @@  - net462;net6.0;net7.0;net8.0 + net462;net8.0;net9.0 Exe + false @@ -14,15 +15,15 @@ - - - - - + + + + + - + - + diff --git a/serilog-settings-configuration.sln b/serilog-settings-configuration.sln index f0209fd..4e06158 100644 --- a/serilog-settings-configuration.sln +++ b/serilog-settings-configuration.sln @@ -9,7 +9,6 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{62D0B9 ProjectSection(SolutionItems) = preProject .editorconfig = .editorconfig .gitignore = .gitignore - appveyor.yml = appveyor.yml Build.ps1 = Build.ps1 CHANGES.md = CHANGES.md Directory.Build.props = Directory.Build.props @@ -18,6 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{62D0B9 README.md = README.md serilog-settings-configuration.sln.DotSettings = serilog-settings-configuration.sln.DotSettings global.json = global.json + Directory.Version.props = Directory.Version.props EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3}" @@ -34,6 +34,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestDummies", "test\TestDum EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "test\TestApp\TestApp.csproj", "{1B6E08F3-16C9-4912-BEEE-57DB78C92A12}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = ".github", ".github", "{21A409AD-2C11-41A8-88C8-360062EA8E48}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "workflows", "workflows", "{391B84C5-A7BA-4BE1-95A0-E459FA61D971}" + ProjectSection(SolutionItems) = preProject + .github\workflows\ci.yml = .github\workflows\ci.yml + EndProjectSection +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -68,6 +75,7 @@ Global {A00E5E32-54F9-401A-BBA1-2F6FCB6366CD} = {D24872B9-57F3-42A7-BC8D-F9DA222FCE1B} {B7CF5068-DD19-4868-A268-5280BDE90361} = {D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3} {1B6E08F3-16C9-4912-BEEE-57DB78C92A12} = {D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3} + {391B84C5-A7BA-4BE1-95A0-E459FA61D971} = {21A409AD-2C11-41A8-88C8-360062EA8E48} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {485F8843-42D7-4267-B5FB-20FE9181DEE9} diff --git a/serilog-settings-configuration.sln.DotSettings b/serilog-settings-configuration.sln.DotSettings index c946077..d47fb00 100644 --- a/serilog-settings-configuration.sln.DotSettings +++ b/serilog-settings-configuration.sln.DotSettings @@ -514,6 +514,21 @@ II.2.12 <HandlesEvent /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static readonly fields (private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Private" Description="Constant fields (private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Type parameters"><ElementKinds><Kind Name="TYPE_PARAMETER" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="T" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Private" Description="Instance fields (private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Instance" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Instance fields (not private)"><ElementKinds><Kind Name="FIELD" /><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local variables"><ElementKinds><Kind Name="LOCAL_VARIABLE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Constant fields (not private)"><ElementKinds><Kind Name="CONSTANT_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static fields (not private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Parameters"><ElementKinds><Kind Name="PARAMETER" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Enum members"><ElementKinds><Kind Name="ENUM_MEMBER" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Types and namespaces"><ElementKinds><Kind Name="NAMESPACE" /><Kind Name="CLASS" /><Kind Name="STRUCT" /><Kind Name="ENUM" /><Kind Name="DELEGATE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Local constants"><ElementKinds><Kind Name="LOCAL_CONSTANT" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /></Policy> + <Policy><Descriptor Staticness="Any" AccessRightKinds="Any" Description="Interfaces"><ElementKinds><Kind Name="INTERFACE" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="I" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Protected, ProtectedInternal, Internal, Public, PrivateProtected" Description="Static readonly fields (not private)"><ElementKinds><Kind Name="READONLY_FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /></Policy> + <Policy><Descriptor Staticness="Static" AccessRightKinds="Private" Description="Static fields (private)"><ElementKinds><Kind Name="FIELD" /></ElementKinds></Descriptor><Policy Inspect="True" Prefix="_" Suffix="" Style="aaBb" /></Policy> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="aaBb" /> <Policy Inspect="True" Prefix="" Suffix="" Style="AaBb" /> @@ -550,6 +565,7 @@ II.2.12 <HandlesEvent /> True True True + True True True True diff --git a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj index 826faf7..0b8d72a 100644 --- a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj +++ b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj @@ -2,36 +2,34 @@ Microsoft.Extensions.Configuration (appsettings.json) support for Serilog. - - 8.0.1 Serilog Contributors - - net462;netstandard2.0;net6.0;net7.0;net8.0 + net462;netstandard2.0;net8.0;net9.0 true - serilog;json + serilog;json;appsettings icon.png README.md Apache-2.0 https://github.com/serilog/serilog-settings-configuration $(PackageProjectUrl)/releases Serilog - true - true - true - snupkg - - + + + + + + - - + + diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationArgumentValue.cs new file mode 100644 index 0000000..239853e --- /dev/null +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationArgumentValue.cs @@ -0,0 +1,34 @@ +using System.Reflection; +using Microsoft.Extensions.Configuration; + +namespace Serilog.Settings.Configuration; + +abstract class ConfigurationArgumentValue +{ + public abstract object? ConvertTo(Type toType, ResolutionContext resolutionContext); + + public static ConfigurationArgumentValue FromSection(IConfigurationSection argumentSection, IReadOnlyCollection configurationAssemblies) + { + ConfigurationArgumentValue argumentValue; + + // Reject configurations where an element has both scalar and complex + // values as a result of reading multiple configuration sources. + if (argumentSection.Value != null && argumentSection.GetChildren().Any()) + throw new InvalidOperationException( + $"The value for the argument '{argumentSection.Path}' is assigned different value " + + "types in more than one configuration source. Ensure all configurations consistently " + + "use either a scalar (int, string, boolean) or a complex (array, section, list, " + + "POCO, etc.) type for this argument value."); + + if (argumentSection.Value != null) + { + argumentValue = new StringArgumentValue(argumentSection.Value); + } + else + { + argumentValue = new ObjectArgumentValue(argumentSection, configurationAssemblies); + } + + return argumentValue; + } +} diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs index e1f0c84..3b86170 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs @@ -200,12 +200,12 @@ void ApplyMinimumLevelConfiguration(IConfigurationSection directive, Action> GetMethodCalls(IConfiguration directive) + internal ILookup> GetMethodCalls(IConfiguration directive) { var children = directive.GetChildren().ToList(); var result = (from child in children where child.Value != null // Plain string - select new { Name = child.Value, Args = new Dictionary() }) + select new { Name = child.Value, Args = new Dictionary() }) .Concat( (from child in children where child.Value == null @@ -319,7 +319,7 @@ internal ILookup> GetMet select new { Name = argument.Key, - Value = GetArgumentValue(argument, _configurationAssemblies) + Value = ConfigurationArgumentValue.FromSection(argument, _configurationAssemblies) }).ToDictionary(p => p.Name, p => p.Value) select new { Name = name, Args = callArgs })) .ToLookup(p => p.Name, p => p.Args); @@ -336,31 +336,6 @@ static string GetSectionName(IConfigurationSection s) } } - internal static IConfigurationArgumentValue GetArgumentValue(IConfigurationSection argumentSection, IReadOnlyCollection configurationAssemblies) - { - IConfigurationArgumentValue argumentValue; - - // Reject configurations where an element has both scalar and complex - // values as a result of reading multiple configuration sources. - if (argumentSection.Value != null && argumentSection.GetChildren().Any()) - throw new InvalidOperationException( - $"The value for the argument '{argumentSection.Path}' is assigned different value " + - "types in more than one configuration source. Ensure all configurations consistently " + - "use either a scalar (int, string, boolean) or a complex (array, section, list, " + - "POCO, etc.) type for this argument value."); - - if (argumentSection.Value != null) - { - argumentValue = new StringArgumentValue(argumentSection.Value); - } - else - { - argumentValue = new ObjectArgumentValue(argumentSection, configurationAssemblies); - } - - return argumentValue; - } - static IReadOnlyCollection LoadConfigurationAssemblies(IConfiguration section, AssemblyFinder assemblyFinder) { var serilogAssembly = typeof(ILogger).Assembly; @@ -404,7 +379,7 @@ This is most likely because the application is published as single-file. return assemblies; } - void CallConfigurationMethods(ILookup> methods, IReadOnlyCollection configurationMethods, object receiver) + void CallConfigurationMethods(ILookup> methods, IReadOnlyCollection configurationMethods, object receiver) { foreach (var method in methods.SelectMany(g => g.Select(x => new { g.Key, Value = x }))) { diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationArgumentValue.cs deleted file mode 100644 index 8a1e86f..0000000 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationArgumentValue.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace Serilog.Settings.Configuration; - -interface IConfigurationArgumentValue -{ - object? ConvertTo(Type toType, ResolutionContext resolutionContext); -} diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs index 8ab46a7..85783b4 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs @@ -1,14 +1,14 @@ using System.Diagnostics.CodeAnalysis; -using System.Linq.Expressions; using System.Reflection; using Microsoft.Extensions.Configuration; using Serilog.Configuration; +using Serilog.Debugging; namespace Serilog.Settings.Configuration; -class ObjectArgumentValue : IConfigurationArgumentValue +class ObjectArgumentValue : ConfigurationArgumentValue { readonly IConfigurationSection _section; readonly IReadOnlyCollection _configurationAssemblies; @@ -21,7 +21,7 @@ public ObjectArgumentValue(IConfigurationSection section, IReadOnlyCollection logger/sink config parameter? var typeInfo = toType.GetTypeInfo(); if (typeInfo.IsGenericType && - typeInfo.GetGenericTypeDefinition() is Type genericType && genericType == typeof(Action<>)) + typeInfo.GetGenericTypeDefinition() is {} genericType && genericType == typeof(Action<>)) { var configType = typeInfo.GenericTypeArguments[0]; IConfigurationReader configReader = new ConfigurationReader(_section, _configurationAssemblies, resolutionContext); @@ -39,20 +39,23 @@ public ObjectArgumentValue(IConfigurationSection section, IReadOnlyCollection new Action(configReader.Configure), _ when configType == typeof(LoggerSinkConfiguration) => new Action(configReader.ApplySinks), _ when configType == typeof(LoggerEnrichmentConfiguration) => new Action(configReader.ApplyEnrichment), - _ => throw new ArgumentException($"Configuration resolution for Action<{configType.Name}> parameter type at the path {_section.Path} is not implemented.") + _ => throw new ArgumentException($"Configuration resolution for `Action<{configType.Name}>` parameter type at the path `{_section.Path}` is not implemented.") }; } if (toType.IsArray) return CreateArray(); + // Only try to call ctor when type is explicitly specified in _section + if (TryCallCtorExplicit(_section, resolutionContext, out var ctorResult)) + return ctorResult; + if (IsContainer(toType, out var elementType) && TryCreateContainer(out var container)) return container; - if (TryBuildCtorExpression(_section, toType, resolutionContext, out var ctorExpression)) - { - return Expression.Lambda>(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); @@ -62,9 +65,9 @@ object CreateArray() var arrayElementType = toType.GetElementType()!; var configurationElements = _section.GetChildren().ToArray(); var array = Array.CreateInstance(arrayElementType, configurationElements.Length); - for (int i = 0; i < configurationElements.Length; ++i) + for (var i = 0; i < configurationElements.Length; ++i) { - var argumentValue = ConfigurationReader.GetArgumentValue(configurationElements[i], _configurationAssemblies); + var argumentValue = FromSection(configurationElements[i], _configurationAssemblies); var value = argumentValue.ConvertTo(arrayElementType, resolutionContext); array.SetValue(value, i); } @@ -76,33 +79,40 @@ 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 = FromSection(section, _configurationAssemblies); + var key = new StringArgumentValue(section.Key).ConvertTo(keyType, resolutionContext); + var value = argumentValue.ConvertTo(valueType, resolutionContext); + addMethod.Invoke(result, [key, value]); + } + return true; + } - for (int i = 0; i < configurationElements.Length; ++i) + if (IsConstructableContainer(toType, elementType, out concreteType, out addMethod)) { - var argumentValue = ConfigurationReader.GetArgumentValue(configurationElements[i], _configurationAssemblies); - var value = argumentValue.ConvertTo(elementType, resolutionContext); - addMethod.Invoke(result, [value]); + result = Activator.CreateInstance(concreteType) ?? throw new InvalidOperationException($"Activator.CreateInstance returned null for {concreteType}"); + + foreach (var section in _section.GetChildren()) + { + var argumentValue = FromSection(section, _configurationAssemblies); + var value = argumentValue.ConvertTo(elementType, resolutionContext); + addMethod.Invoke(result, [value]); + } + return true; } - return true; + return false; } } - 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,111 +126,128 @@ 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); + } - if (suppliedArguments.Count == 0 && - type.GetConstructor(Type.EmptyTypes) is ConstructorInfo parameterlessCtor) - { - ctorExpression = Expression.New(parameterlessCtor); - return true; - } + bool TryCallCtor(Type type, Dictionary suppliedArguments, ResolutionContext resolutionContext, [NotNullWhen(true)] out object? value) + { + var binding = type.GetConstructors() + .Select(ci => + { + var args = new List(); + var matches = 0; + var stringMatches = 0; + var suppliedNames = new HashSet(StringComparer.OrdinalIgnoreCase); + foreach (var p in ci.GetParameters()) + { + if (suppliedArguments.TryGetValue(p.Name ?? "", out var argValue)) + { + args.Add(argValue); + matches += 1; + + if (p.ParameterType == typeof(string)) + { + stringMatches += 1; + } + + if (p.Name != null) + { + suppliedNames.Add(p.Name); + } + } + else + { + if (p.HasDefaultValue) + { + args.Add(p.DefaultValue); + } + else + { + return new { ci, args, isCallable = false, matches, stringMatches, + usedArguments = suppliedNames }; + } + } + } + + return new { ci, args, isCallable = true, matches, stringMatches, + usedArguments = suppliedNames }; + }) + .Where(binding => binding.isCallable) + .OrderByDescending(binding => binding.matches) + .ThenByDescending(binding => binding.stringMatches) + .ThenBy(binding => binding.args.Count) + .FirstOrDefault(); - var ctor = - (from c in type.GetConstructors() - from p in c.GetParameters() - let argumentBindResult = suppliedArguments.TryGetValue(p.Name ?? "", out var argValue) switch - { - true => new { success = true, hasMatch = true, value = (object?)argValue }, - false => p.HasDefaultValue switch - { - true => new { success = true, hasMatch = false, value = (object?)p.DefaultValue }, - false => new { success = false, hasMatch = false, value = (object?)null }, - }, - } - group new { argumentBindResult, p.ParameterType } by c into gr - where gr.All(z => z.argumentBindResult.success) - let matchedArgs = gr.Where(z => z.argumentBindResult.hasMatch).ToList() - orderby matchedArgs.Count descending, - matchedArgs.Count(p => p.ParameterType == typeof(string)) descending - select new - { - ConstructorInfo = gr.Key, - ArgumentValues = gr.Select(z => new { Value = z.argumentBindResult.value, Type = z.ParameterType }) - .ToList() - }).FirstOrDefault(); - - if (ctor is null) + if (binding == null) { + value = null; return false; } - var ctorArguments = new List(); - foreach (var argumentValue in ctor.ArgumentValues) + for (var i = 0; i < binding.ci.GetParameters().Length; ++i) { - if (TryBindToCtorArgument(argumentValue.Value, argumentValue.Type, resolutionContext, out var argumentExpression)) - { - ctorArguments.Add(argumentExpression); - } - else + if (binding.args[i] is IConfigurationSection section) { - return false; + var argumentValue = FromSection(section, _configurationAssemblies); + binding.args[i] = argumentValue.ConvertTo(binding.ci.GetParameters()[i].ParameterType, resolutionContext); } } - ctorExpression = Expression.New(ctor.ConstructorInfo, ctorArguments); - return true; + value = binding.ci.Invoke(binding.args.ToArray()); - static bool TryBindToCtorArgument(object value, Type type, ResolutionContext resolutionContext, [NotNullWhen(true)] out Expression? argumentExpression) + foreach (var pi in type.GetProperties(BindingFlags.Instance | BindingFlags.Public)) { - argumentExpression = null; - - if (value is IConfigurationSection s) + if (!binding.usedArguments.Contains(pi.Name) && + suppliedArguments.TryGetValue(pi.Name, out var section) + && pi.CanWrite && + // This avoids trying to call esoteric indexers and so on. + pi.GetSetMethod(false)?.GetParameters().Length == 1) { - if (s.Value is string argValue) + var propertyValue = FromSection(section, _configurationAssemblies); + try { - var stringArgumentValue = new StringArgumentValue(argValue); - try - { - argumentExpression = Expression.Constant( - stringArgumentValue.ConvertTo(type, resolutionContext), - type); - - return true; - } - catch (Exception) - { - return false; - } + pi.SetValue(value, propertyValue.ConvertTo(pi.PropertyType, resolutionContext)); } - else if (s.GetChildren().Any()) + catch (Exception ex) { - if (TryBuildCtorExpression(s, type, resolutionContext, out var ctorExpression)) - { - argumentExpression = ctorExpression; - return true; - } - - return false; + SelfLog.WriteLine($"Serilog.Settings.Configuration: Property setter on {type} failed: {ex}"); } } - - argumentExpression = Expression.Constant(value, type); - return true; } + + return true; } static bool IsContainer(Type type, [NotNullWhen(true)] out Type? elementType) { elementType = null; + if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(IEnumerable<>)) + { + elementType = type.GetGenericArguments()[0]; + return true; + } foreach (var iface in type.GetInterfaces()) { if (iface.IsGenericType) @@ -235,4 +262,87 @@ static bool IsContainer(Type type, [NotNullWhen(true)] out Type? elementType) 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 is { IsStatic: false, Name: "Add" }) + { + var parameters = method.GetParameters(); + if (parameters.Length == 2 && parameters[0].ParameterType == keyType && parameters[1].ParameterType == valueType) + { + addMethod = method; + return true; + } + } + } + return false; + } + + static bool IsConstructableContainer(Type type, Type elementType, [NotNullWhen(true)] out Type? concreteType, [NotNullWhen(true)] out MethodInfo? addMethod) + { + addMethod = null; + if (type.IsAbstract) + { + concreteType = typeof(List<>).MakeGenericType(elementType); + if (!type.IsAssignableFrom(concreteType)) + { + concreteType = typeof(HashSet<>).MakeGenericType(elementType); + if (!type.IsAssignableFrom(concreteType)) + { + concreteType = null; + return false; + } + } + } + else + { + concreteType = type; + } + if (concreteType.GetConstructor(Type.EmptyTypes) == null) + { + return false; + } + foreach (var method in concreteType.GetMethods()) + { + if (method is { IsStatic: false, Name: "Add" }) + { + var parameters = method.GetParameters(); + if (parameters.Length == 1 && parameters[0].ParameterType == elementType) + { + addMethod = method; + return true; + } + } + } + return false; + } } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs index 2ba8dcc..894273c 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs @@ -6,7 +6,7 @@ namespace Serilog.Settings.Configuration; -class StringArgumentValue : IConfigurationArgumentValue +class StringArgumentValue : ConfigurationArgumentValue { readonly string _providedValue; @@ -24,7 +24,7 @@ public StringArgumentValue(string providedValue) { typeof(Type), s => Type.GetType(s, throwOnError:true)! }, }; - public object? ConvertTo(Type toType, ResolutionContext resolutionContext) + public override object? ConvertTo(Type toType, ResolutionContext resolutionContext) { var argumentValue = Environment.ExpandEnvironmentVariables(_providedValue); @@ -51,7 +51,7 @@ public StringArgumentValue(string providedValue) } if (toTypeInfo.IsEnum) - return Enum.Parse(toType, argumentValue); + return Enum.Parse(toType, argumentValue, ignoreCase: true); var convertor = ExtendedTypeConversions .Where(t => t.Key.GetTypeInfo().IsAssignableFrom(toTypeInfo)) diff --git a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs index d505ec1..5aeec1f 100644 --- a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs @@ -60,7 +60,7 @@ public void PropertyEnrichmentIsApplied() [Theory] [InlineData(null)] [InlineData("")] - public void CanReadWithoutSerilogSection(string sectionName) + public void CanReadWithoutSerilogSection(string? sectionName) { LogEvent? evt = null; diff --git a/test/Serilog.Settings.Configuration.Tests/LoggerConfigurationExtensionsTests.cs b/test/Serilog.Settings.Configuration.Tests/LoggerConfigurationExtensionsTests.cs index 39a0df9..583a53a 100644 --- a/test/Serilog.Settings.Configuration.Tests/LoggerConfigurationExtensionsTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/LoggerConfigurationExtensionsTests.cs @@ -139,4 +139,23 @@ public void ReadFromConfigurationSectionDoesNotThrowWhenTryingToCallConfiguratio .CreateLogger(); } + + [Fact] + [Trait("BugFix", "https://github.com/serilog/serilog-settings-configuration/issues/332")] + public void ReadFromConfiguration_ShouldNot_Throw_When_MinimumLevel_Default_Set_But_MinimumLevel_Value_Is_Empty_String() + { + var configuration = new ConfigurationBuilder() + .AddInMemoryCollection(new Dictionary + { + ["Serilog"] = "", + ["Serilog:MinimumLevel"] = "", + ["Serilog:MinimumLevel:Default"] = "Information", + + }) + .Build(); + + new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .CreateLogger(); + } } diff --git a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs index 49f8d5a..b7f3791 100644 --- a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs @@ -3,9 +3,13 @@ using Microsoft.Extensions.Configuration; using Serilog.Configuration; using Serilog.Events; +using Serilog.Formatting; +using Serilog.Formatting.Display; using Serilog.Settings.Configuration.Tests.Support; using TestDummies; using TestDummies.Console; +// ReSharper disable NotDisposedResourceIsReturned +// ReSharper disable UnusedAutoPropertyAccessor.Local // ReSharper disable UnusedMember.Local // ReSharper disable UnusedParameter.Local @@ -15,20 +19,21 @@ namespace Serilog.Settings.Configuration.Tests; public class ObjectArgumentValueTests { - static T ConvertToReturnsType(ObjectArgumentValue value, ResolutionContext? resolutionContext = null) + static T AssertConvertsToType(ObjectArgumentValue value, ResolutionContext? resolutionContext = null) { return Assert.IsType(value.ConvertTo(typeof(T), resolutionContext ?? new())); } [Fact] - public void ConvertToIConfigurationSection() + public void ConvertsToIConfigurationSection() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { - "Serilog": {} + "section": {} } - """, "Serilog"); + """; + var section = JsonStringConfigSource.LoadSection(json, "section"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(IConfigurationSection), new()); @@ -37,12 +42,12 @@ public void ConvertToIConfigurationSection() } [Fact] - public void ConvertToLoggerConfigurationCallback() + public void ConvertsToLoggerConfigurationCallback() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { - "Serilog": { + "callback": { "WriteTo": [{ "Name": "DummyRollingFile", "Args": {"pathFormat" : "C:\\"} @@ -50,10 +55,11 @@ public void ConvertToLoggerConfigurationCallback() "Enrich": ["WithDummyThreadId"] } } - """, "Serilog"); + """; + var section = JsonStringConfigSource.LoadSection(json, "callback"); var value = new ObjectArgumentValue(section, [typeof(DummyRollingFileSink).Assembly]); - var configure = ConvertToReturnsType>(value); + var configure = AssertConvertsToType>(value); var config = new LoggerConfiguration(); configure(config); @@ -67,10 +73,10 @@ public void ConvertToLoggerConfigurationCallback() } [Fact] - public void ConvertToLoggerSinkConfigurationCallback() + public void ConvertsToLoggerSinkConfigurationCallback() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { "WriteTo": [{ "Name": "Dummy", @@ -79,10 +85,11 @@ public void ConvertToLoggerSinkConfigurationCallback() } }] } - """, "WriteTo"); + """; + var section = JsonStringConfigSource.LoadSection(json, "WriteTo"); var value = new ObjectArgumentValue(section, [typeof(DummyConfigurationSink).Assembly]); - var configureSinks = ConvertToReturnsType>(value); + var configureSinks = AssertConvertsToType>(value); var config = new LoggerConfiguration(); configureSinks(config.WriteTo); @@ -97,10 +104,10 @@ public void ConvertToLoggerSinkConfigurationCallback() } [Fact] - public void ConvertToLoggerEnrichmentConfiguration() + public void ConvertsToLoggerEnrichmentConfiguration() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { "Enrich": [{ "Name": "AtLevel", @@ -110,10 +117,11 @@ 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); + var configureEnrichment = AssertConvertsToType>(value); var config = new LoggerConfiguration(); config.WriteTo.DummyRollingFile(""); @@ -131,39 +139,41 @@ public void ConvertToLoggerEnrichmentConfiguration() } [Fact] - public void ConvertToConfigurationCallbackThrows() + public void ConvertToUnrecognizedConfigurationCallbackThrows() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { - "Configure": {} + "configure": {} } - """, "Configure"); + """; + var section = JsonStringConfigSource.LoadSection(json, "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); + Assert.Equal("Configuration resolution for `Action` parameter type at the path `configure` is not implemented.", ex.Message); } [Fact] - public void ConvertToArrayUsingStringArgumentValueForElements() + public void ConvertsToEnumArrayUsingStringArgumentValueForElements() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { "Array": [ "Information", 3, null ] } - """, "Array"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Array"); var value = new ObjectArgumentValue(section, []); - var array = ConvertToReturnsType(value); + var array = AssertConvertsToType(value); Assert.Equal([LogEventLevel.Information, LogEventLevel.Warning, null], array); } [Fact] - public void ConvertToArrayOfArraysPassingContext() + public void ConvertsToArrayOfArraysPassingContext() { var formatProvider = new NumberFormatInfo { @@ -173,7 +183,7 @@ public void ConvertToArrayOfArraysPassingContext() }; // language=json - var json = """ + const string json = """ { "Array": [ [ 1, 2 ], [ 3, 4 ], [ "1.234,56" ] ] } @@ -181,23 +191,24 @@ public void ConvertToArrayOfArraysPassingContext() var section = JsonStringConfigSource.LoadSection(json, "Array"); var value = new ObjectArgumentValue(section, []); - var array = ConvertToReturnsType(value, new(readerOptions: new() { FormatProvider = formatProvider })); + var array = AssertConvertsToType(value, new(readerOptions: new() { FormatProvider = formatProvider })); Assert.Equal([[1, 2], [3, 4], [1_234.56M]], array); } [Fact] - public void ConvertToArrayRecursingObjectArgumentValuePassingAssemblies() + public void ConvertsToArrayRecursingObjectArgumentValuePassingAssemblies() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string 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); + var configureCalls = AssertConvertsToType[]>(value); var configure = Assert.Single(configureCalls); var config = new LoggerConfiguration(); @@ -211,20 +222,21 @@ public void ConvertToArrayRecursingObjectArgumentValuePassingAssemblies() } [Fact] - public void ConvertToArrayWithDifferentImplementations() + public void ConvertsToArrayWithDifferentImplementations() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string 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); + var array = AssertConvertsToType(value); Assert.Collection(array, first => Assert.IsType(first), @@ -232,23 +244,24 @@ public void ConvertToArrayWithDifferentImplementations() } [Fact] - public void ConvertToContainerUsingStringArgumentValueForElements() + public void ConvertsToContainerUsingStringArgumentValueForElements() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { "List": [ "Information", 3, null ] } - """, "List"); + """; + var section = JsonStringConfigSource.LoadSection(json, "List"); var value = new ObjectArgumentValue(section, []); - var list = ConvertToReturnsType>(value); + var list = AssertConvertsToType>(value); Assert.Equal([LogEventLevel.Information, LogEventLevel.Warning, null], list); } [Fact] - public void ConvertToNestedContainerPassingContext() + public void ConvertsToNestedContainerPassingContext() { var formatProvider = new NumberFormatInfo { @@ -258,30 +271,32 @@ public void ConvertToNestedContainerPassingContext() }; // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string 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 })); + var array = AssertConvertsToType>>(value, new(readerOptions: new() { FormatProvider = formatProvider })); Assert.Equal([[1, 2], [3, 4], [1_234.56M]], array); } [Fact] - public void ConvertToContainerRecursingObjectArgumentValuePassingAssemblies() + public void ConvertsToContainerRecursingObjectArgumentValuePassingAssemblies() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string 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); + var configureCalls = AssertConvertsToType>>(value); var configure = Assert.Single(configureCalls); var config = new LoggerConfiguration(); @@ -295,20 +310,21 @@ public void ConvertToContainerRecursingObjectArgumentValuePassingAssemblies() } [Fact] - public void ConvertToListWithDifferentImplementations() + public void ConvertsToListWithDifferentImplementations() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string 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); + var list = AssertConvertsToType>(value); Assert.Collection(list, first => Assert.IsType(first), @@ -319,7 +335,7 @@ class UnsupportedContainer : IEnumerable { public IEnumerator GetEnumerator() { - return Enumerable.Empty().GetEnumerator(); + yield break; } IEnumerator IEnumerable.GetEnumerator() @@ -332,30 +348,35 @@ IEnumerator IEnumerable.GetEnumerator() public void ConvertToUnsupportedContainerWillBeCreatedButWillRemainEmpty() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { "List": ["a", "b"] } - """, "List"); + """; + var section = JsonStringConfigSource.LoadSection(json, "List"); var value = new ObjectArgumentValue(section, []); - var unsupported = ConvertToReturnsType(value); + var unsupported = AssertConvertsToType(value); Assert.Empty(unsupported); } [Theory] + [InlineData(typeof(IEnumerable))] [InlineData(typeof(ICollection))] + [InlineData(typeof(IReadOnlyCollection))] [InlineData(typeof(IList))] + [InlineData(typeof(IReadOnlyList))] [InlineData(typeof(List))] public void ConvertToContainerUsingList(Type containerType) { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { "Container": [ 1, 1 ] } - """, "Container"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Container"); var value = new ObjectArgumentValue(section, []); var container = value.ConvertTo(containerType, new()); @@ -364,38 +385,80 @@ 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 ConvertsToContainerUsingHashSet(Type containerType) { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { "Container": [ 1, 1, 2, 2 ] } - """, "Container"); + """; + var section = JsonStringConfigSource.LoadSection(json, "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 ConvertsToForcedHashSetImplementationWithCustomComparer() + { + // 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 on 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))] - public void ConvertToContainerUsingDictionary(Type containerType) + public void ConvertsToContainerUsingDictionary(Type containerType) { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { "Container": { "a": 1, "b": 2 } } - """, "Container"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Container"); var value = new ObjectArgumentValue(section, []); var container = value.ConvertTo(containerType, new()); @@ -405,20 +468,24 @@ public void ConvertToContainerUsingDictionary(Type containerType) } [Theory] + [InlineData(typeof(IEnumerable>))] + [InlineData(typeof(ICollection>))] + [InlineData(typeof(IReadOnlyCollection>))] [InlineData(typeof(IDictionary))] [InlineData(typeof(IReadOnlyDictionary))] [InlineData(typeof(Dictionary))] - public void ConvertToContainerUsingDictionaryUsingStringArgumentValueToConvertKey(Type containerType) + public void ConvertsToContainerUsingDictionaryUsingStringArgumentValueToConvertKey(Type containerType) { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { "Container": { "1": 2, "3": 4 } } - """, "Container"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Container"); var value = new ObjectArgumentValue(section, []); var container = value.ConvertTo(containerType, new()); @@ -500,6 +567,29 @@ IEnumerator IEnumerable.GetEnumerator() } } + [Fact] + public void ConvertsToContainerUsingDictionaryWithoutPublicDefaultConstructor() + { + // language=json + const string json = """ + { + "Container": { + "values": + { + "a": 1, + "b": 2 + } + } + } + """; + var section = JsonStringConfigSource.LoadSection(json, "Container"); + var value = new ObjectArgumentValue(section, []); + + var dictionary = AssertConvertsToType(value); + + Assert.Equal(new Dictionary { { "a", 1 }, { "b", 2 } }, dictionary); + } + abstract class CustomAbstractDictionary : IDictionary { public abstract int this[string key] { get; set; } @@ -530,14 +620,15 @@ IEnumerator IEnumerable.GetEnumerator() public void ConvertToCustomAbstractDictionaryThrows() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string 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())); @@ -578,17 +669,18 @@ IEnumerator IEnumerable.GetEnumerator() public void ConvertToCustomReadOnlyDictionaryCreatesEmpty() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { "Container": { "a": 1, "b": 2 } } - """, "Container"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Container"); var value = new ObjectArgumentValue(section, []); - ConvertToReturnsType(value); + AssertConvertsToType(value); } class PrivateImplWithPublicCtor : AnAbstractClass, IAmAnInterface; @@ -598,14 +690,15 @@ class PrivateImplWithPublicCtor : AnAbstractClass, IAmAnInterface; [InlineData(typeof(IAmAnInterface), typeof(PrivateImplWithPublicCtor))] [InlineData(typeof(AnAbstractClass), typeof(PrivateImplWithPublicCtor))] [InlineData(typeof(AConcreteClass), typeof(ConcreteImplOfConcreteClass))] - public void ConvertToExplicitType(Type targetType, Type expectedType) + public void ConvertsToExplicitType(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()); @@ -621,17 +714,18 @@ class WithTypeArgumentClassCtor : AnAbstractClass } [Fact] - public void ConvertToExplicitTypeUsingTypeAsConstructorArgument() + public void ConvertsToExplicitTypeUsingTypeAsConstructorArgument() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string 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()); @@ -666,20 +760,20 @@ public WithOverloads(int a, TimeSpan b, Uri c, string d = "d") [Theory] [InlineData("", null)] [InlineData(",\"d\": \"DValue\"", "DValue")] - public void ConvertToExplicitTypePickingConstructorOverloadWithMostMatchingArguments(string dJson, string? d) + public void ConvertsToExplicitTypePickingConstructorOverloadWithMostMatchingArguments(string dJson, string? d) { - // language=json - var section = JsonStringConfigSource.LoadSection($$""" + var json = $$""" { "Ctor": { "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+WithOverloads, Serilog.Settings.Configuration.Tests", "a": 1, "b": "23:59:59", - "c": "http://dot.com/" + "c": "https://example.com/" {{dJson}} } } - """, "Ctor"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); var value = new ObjectArgumentValue(section, []); var result = value.ConvertTo(typeof(IAmAnInterface), new()); @@ -687,24 +781,25 @@ public void ConvertToExplicitTypePickingConstructorOverloadWithMostMatchingArgum 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(new Uri("https://example.com/"), actual.C); Assert.Equal(d, actual.D); } [Fact] - public void ConvertToExplicitTypeMatchingArgumentsCaseInsensitively() + public void ConvertsToExplicitTypeMatchingArgumentsCaseInsensitively() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { "Ctor": { "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+WithOverloads, Serilog.Settings.Configuration.Tests", "A": 1, "B": "23:59:59", - "C": "http://dot.com/" + "C": "https://example.com/" } } - """, "Ctor"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); var value = new ObjectArgumentValue(section, []); var result = value.ConvertTo(typeof(IAmAnInterface), new()); @@ -712,7 +807,7 @@ public void ConvertToExplicitTypeMatchingArgumentsCaseInsensitively() 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(new Uri("https://example.com/"), actual.C); } class WithSimilarOverloads : IAmAnInterface @@ -731,7 +826,7 @@ class WithSimilarOverloads : IAmAnInterface public void ConvertToExplicitTypePickingConstructorOverloadWithMostStrings() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { "Ctor": { "type": "Serilog.Settings.Configuration.Tests.ObjectArgumentValueTests+WithSimilarOverloads, Serilog.Settings.Configuration.Tests", @@ -740,7 +835,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()); @@ -767,14 +863,15 @@ class OnlyDifferentTypeOverloads : IAmAnInterface public void ConvertToExplicitTypePickingFirstMatchWhenOtherwiseAmbiguous() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string 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()); @@ -803,18 +900,18 @@ 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 ConvertsToExplicitTypeFillingInDefaultsInConstructor(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()); @@ -833,10 +930,202 @@ class WithParamsArray : IAmAnInterface } [Fact] - public void ConvertToExplicitTypeWithExplicitTypeConstructorArgument() + public void ConvertsToExplicitTypeWithParamsConstructorArgument() + { + // language=json + const string 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 ConvertsToExplicitTypeWithContainerConstructorArgument(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 ConvertsToExplicitTypeWithDictionaryConstructorArgument(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 ConvertsToExplicitTypeWithStructConstructorArgument() + { + // language=json + const string 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 ConvertsToExplicitTypeWithClassConstructorArgument() + { + // language=json + const string 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 string String { get; } + public Struct(string str) { String = str; } + } + + [Fact] + public void ConvertsToExplicitTypeWithExplicitStructConstructorArgument() + { + // language=json + const string 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 ConvertsToExplicitTypeWithExplicitTypeConstructorArgument() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { "Ctor": { "type": "Serilog.Settings.Configuration.Tests.Support.GenericClass`1[[Serilog.Settings.Configuration.Tests.Support.IAmAnInterface, Serilog.Settings.Configuration.Tests]], Serilog.Settings.Configuration.Tests", @@ -845,7 +1134,8 @@ public void ConvertToExplicitTypeWithExplicitTypeConstructorArgument() } } } - """, "Ctor"); + """; + var section = JsonStringConfigSource.LoadSection(json, "Ctor"); var value = new ObjectArgumentValue(section, []); var result = value.ConvertTo(typeof(IAmAnInterface), new()); @@ -869,14 +1159,15 @@ public void ConvertToExplicitTypeWithExplicitTypeConstructorArgument() [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) + public void ConvertsToPrimitives(Type type, object expected, string sectionValue) { // language=json - var section = JsonStringConfigSource.LoadSection($$""" + var json = $$""" { - "Serilog": {{sectionValue}} + "value": {{sectionValue}} } - """, "Serilog"); + """; + var section = JsonStringConfigSource.LoadSection(json, "value"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(type, new()); @@ -887,14 +1178,15 @@ public void ConvertToPrimitives(Type type, object expected, string sectionValue) // 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() + public void ConvertsToNullablePrimitive() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { - "Serilog": 123 + "value": 123 } - """, "Serilog"); + """; + var section = JsonStringConfigSource.LoadSection(json, "value"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(int?), new()); @@ -904,14 +1196,15 @@ public void ConvertToNullablePrimitive() // While ObjectArgumentValue supports converting to a nullable primitive, this is normally handled by StringArgumentValue [Fact] - public void ConvertToNullWhenEmptyNullable() + public void ConvertsToNullWhenEmptyNullable() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { - "Serilog": null + "value": null } - """, "Serilog"); + """; + var section = JsonStringConfigSource.LoadSection(json, "value"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(int?), new()); @@ -920,14 +1213,15 @@ public void ConvertToNullWhenEmptyNullable() } [Fact] - public void ConvertToPlainClass() + public void ConvertsToPlainClass() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { - "Serilog": { "foo" : "bar" } + "value": { "foo" : "bar" } } - """, "Serilog"); + """; + var section = JsonStringConfigSource.LoadSection(json, "value"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(TestDummies.DummyLoggerConfigurationExtensions.Binding), new()); @@ -944,14 +1238,15 @@ struct PlainStruct } [Fact] - public void ConvertToPlainStruct() + public void ConvertsToPlainStruct() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { - "Serilog": { "A" : "1" } + "value": { "A" : "1" } } - """, "Serilog"); + """; + var section = JsonStringConfigSource.LoadSection(json, "value"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(PlainStruct), new()); @@ -965,18 +1260,54 @@ public void ConvertToPlainStruct() // 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() + public void ConvertsToNullWhenStructIsNull() { // language=json - var section = JsonStringConfigSource.LoadSection(""" + const string json = """ { - "Serilog": null + "value": null } - """, "Serilog"); + """; + var section = JsonStringConfigSource.LoadSection(json, "value"); var value = new ObjectArgumentValue(section, []); var actual = value.ConvertTo(typeof(PlainStruct), new()); Assert.Null(actual); } + + // This is intended to mirror Serilog.Sinks.Email's options type. + // https://github.com/serilog/serilog-settings-configuration/issues/417 + public class NestedComplexType + { + public string? Host { get; set; } + public ITextFormatter? Subject { get; set; } + } + + [Fact] + public void ConstructsNestedComplexObjects() + { + // language=json + const string json = """ + { + "options": { + "subject": { + "type": "Serilog.Formatting.Display.MessageTemplateTextFormatter, Serilog", + "outputTemplate": "Serilog test" + }, + "host": "localhost" + } + } + """; + + var section = JsonStringConfigSource.LoadSection(json, "options"); + var value = new ObjectArgumentValue(section, []); + + var actual = AssertConvertsToType(value); + Assert.Equal("localhost", actual.Host); + var formatter = Assert.IsType(actual.Subject); + var sw = new StringWriter(); + formatter.Format(Some.LogEvent(), sw); + Assert.Equal("Serilog test", sw.ToString()); + } } 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 f9e1e11..d3ac6cc 100644 --- a/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj +++ b/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj @@ -1,9 +1,10 @@ - + net48 - $(TargetFrameworks);net6.0;net7.0;net8.0 + $(TargetFrameworks);net8.0;net9.0 false + false @@ -18,15 +19,15 @@ - - - - - - + + + + + + - - + + diff --git a/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs b/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs index 6ab7e5c..be172cd 100644 --- a/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs @@ -54,7 +54,7 @@ public void StringValuesConvertToDefaultInstancesIfTargetIsAbstractClass() // a full-qualified type name should not be considered a static member accessor [InlineData("My.NameSpace.Class, MyAssembly, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", null, null)] - public void TryParseStaticMemberAccessorReturnsExpectedResults(string input, string? expectedAccessorType, string expectedPropertyName) + public void TryParseStaticMemberAccessorReturnsExpectedResults(string? input, string? expectedAccessorType, string? expectedPropertyName) { var actual = StringArgumentValue.TryParseStaticMemberAccessor(input, out var actualAccessorType, @@ -222,10 +222,12 @@ public void ReferencingUndeclaredLevelSwitchThrows() Assert.Contains("\"LevelSwitches\":{\"$mySwitch\":", ex.Message); } - [Fact] - public void StringValuesConvertToEnumByName() + [Theory] + [InlineData("Information")] + [InlineData("information")] + public void StringValuesConvertToEnumByName(string level) { - var value = new StringArgumentValue(nameof(LogEventLevel.Information)); + var value = new StringArgumentValue(level); var actual = value.ConvertTo(typeof(LogEventLevel), new()); diff --git a/test/TestApp-net9.0/.gitignore b/test/TestApp-net9.0/.gitignore new file mode 100644 index 0000000..1cc29f1 --- /dev/null +++ b/test/TestApp-net9.0/.gitignore @@ -0,0 +1,2 @@ +FodyWeavers.xml +FodyWeavers.xsd diff --git a/test/TestApp-net9.0/Program.cs b/test/TestApp-net9.0/Program.cs new file mode 100644 index 0000000..033b43d --- /dev/null +++ b/test/TestApp-net9.0/Program.cs @@ -0,0 +1,70 @@ +using System.Reflection; +using Microsoft.Extensions.Configuration; +using Serilog; +using Serilog.Debugging; +using Serilog.Settings.Configuration; + +if (args.Length == 1 && args[0] == "is-single-file") +{ + if (typeof(Program).Assembly.GetManifestResourceNames().Any(e => e.StartsWith("costura."))) + { + Console.WriteLine(true); + return 0; + } + // IL3000: 'System.Reflection.Assembly.Location' always returns an empty string for assemblies embedded in a single-file app +#pragma warning disable IL3000 + Console.WriteLine(string.IsNullOrEmpty(Assembly.GetEntryAssembly()?.Location)); +#pragma warning restore + return 0; +} + +SelfLog.Enable(Console.Error); + +Thread.CurrentThread.Name = "Main thread"; +const string outputTemplate = "({ThreadName}) [{Level}] {Message}{NewLine}"; + +var configurationValues = new Dictionary(); +var minimumLevelOnly = args.Contains("--minimum-level-only"); +if (minimumLevelOnly) +{ + configurationValues["Serilog:MinimumLevel"] = "Verbose"; +} +else +{ + configurationValues["Serilog:Enrich:0"] = "WithThreadName"; + configurationValues["Serilog:WriteTo:0:Name"] = "Console"; + configurationValues["Serilog:WriteTo:0:Args:outputTemplate"] = outputTemplate; +} + +if (args.Contains("--using-thread")) configurationValues["Serilog:Using:Thread"] = "Serilog.Enrichers.Thread"; +if (args.Contains("--using-console")) configurationValues["Serilog:Using:Console"] = "Serilog.Sinks.Console"; + +var assemblies = new List(); +if (args.Contains("--assembly-thread")) assemblies.Add(typeof(ThreadLoggerConfigurationExtensions).Assembly); +if (args.Contains("--assembly-console")) assemblies.Add(typeof(ConsoleLoggerConfigurationExtensions).Assembly); + +try +{ + var configuration = new ConfigurationBuilder().AddInMemoryCollection(configurationValues).Build(); + var options = assemblies.Count > 0 ? new ConfigurationReaderOptions(assemblies.ToArray()) : null; + var loggerConfiguration = new LoggerConfiguration().ReadFrom.Configuration(configuration, options); + if (minimumLevelOnly) + { + loggerConfiguration + .Enrich.WithThreadName() + .WriteTo.Console(outputTemplate: outputTemplate); + } + var logger = loggerConfiguration.CreateLogger(); + logger.Information("Expected success"); + return 0; +} +catch (InvalidOperationException exception) when (exception.Message.StartsWith("No Serilog:Using configuration section is defined and no Serilog assemblies were found.")) +{ + Console.WriteLine("Expected exception"); + return 0; +} +catch (Exception exception) +{ + Console.Error.WriteLine(exception); + return 1; +} diff --git a/test/TestApp-net9.0/Serilog.Settings.Configuration.0.0.0-IntegrationTest.0.snupkg b/test/TestApp-net9.0/Serilog.Settings.Configuration.0.0.0-IntegrationTest.0.snupkg new file mode 100644 index 0000000..bf7bdf1 Binary files /dev/null and b/test/TestApp-net9.0/Serilog.Settings.Configuration.0.0.0-IntegrationTest.0.snupkg differ diff --git a/test/TestApp-net9.0/TestApp.csproj b/test/TestApp-net9.0/TestApp.csproj new file mode 100644 index 0000000..585e706 --- /dev/null +++ b/test/TestApp-net9.0/TestApp.csproj @@ -0,0 +1,33 @@ + + + + Exe + net48 + embedded + false + false + false + none + true + true + false + + NU1902;NU1903 + + + + + + + + + + + + + + + + + + diff --git a/test/TestApp-net9.0/nuget.config b/test/TestApp-net9.0/nuget.config new file mode 100644 index 0000000..cfec8fc --- /dev/null +++ b/test/TestApp-net9.0/nuget.config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/test/TestApp/TestApp.csproj b/test/TestApp/TestApp.csproj index 2c045b4..585e706 100644 --- a/test/TestApp/TestApp.csproj +++ b/test/TestApp/TestApp.csproj @@ -10,6 +10,9 @@ none true true + false + + NU1902;NU1903 @@ -22,9 +25,9 @@ - - - + + + diff --git a/test/TestDummies/DummyLoggerConfigurationExtensions.cs b/test/TestDummies/DummyLoggerConfigurationExtensions.cs index ea87024..d7118f6 100644 --- a/test/TestDummies/DummyLoggerConfigurationExtensions.cs +++ b/test/TestDummies/DummyLoggerConfigurationExtensions.cs @@ -133,12 +133,9 @@ public static LoggerConfiguration Dummy( this LoggerSinkConfiguration loggerSinkConfiguration, Action wrappedSinkAction) { - return LoggerSinkConfiguration.Wrap( - loggerSinkConfiguration, + return loggerSinkConfiguration.Sink(LoggerSinkConfiguration.Wrap( s => new DummyWrappingSink(s), - wrappedSinkAction, - LogEventLevel.Verbose, - levelSwitch: null); + wrappedSinkAction)); } public static LoggerConfiguration WithDummyHardCodedString( diff --git a/test/TestDummies/TestDummies.csproj b/test/TestDummies/TestDummies.csproj index c66eed1..43f8c6d 100644 --- a/test/TestDummies/TestDummies.csproj +++ b/test/TestDummies/TestDummies.csproj @@ -2,15 +2,16 @@ netstandard2.0;net462 + false - + - +