diff --git a/.editorconfig b/.editorconfig index 3687ef4e..55dcd648 100644 --- a/.editorconfig +++ b/.editorconfig @@ -15,3 +15,5 @@ end_of_line = lf [*.{cmd, bat}] end_of_line = crlf + +csharp_style_namespace_declarations = file_scoped:suggestion diff --git a/.gitignore b/.gitignore index 29e6b229..c90bd95b 100644 --- a/.gitignore +++ b/.gitignore @@ -132,7 +132,7 @@ publish/ # Publish Web Output *.[Pp]ublish.xml *.azurePubxml -# TODO: Comment the next line if you want to checkin your web deploy settings +# TODO: Comment the next line if you want to checkin your web deploy settings # but database connection strings (with potential passwords) will be unencrypted *.pubxml *.publishproj @@ -200,7 +200,4 @@ FakesAssemblies/ project.lock.json -#Test files -*.txt - artifacts/ diff --git a/Build.ps1 b/Build.ps1 index f58ea2ec..15133822 100644 --- a/Build.ps1 +++ b/Build.ps1 @@ -1,60 +1,38 @@ -echo "build: Build started" +Write-Output "build: Build started" Push-Location $PSScriptRoot if(Test-Path .\artifacts) { - echo "build: Cleaning .\artifacts" - Remove-Item .\artifacts -Force -Recurse + 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 ""] -echo "build: Package version suffix is $suffix" -echo "build: Build version suffix is $buildSuffix" - -foreach ($src in gci src/*) { - Push-Location $src - - echo "build: Packaging project in $src" - - & dotnet build -c Release --version-suffix=$buildSuffix - - if($suffix) { - & dotnet pack -c Release --include-source --no-build -o ../../artifacts --version-suffix=$suffix -p:ContinuousIntegrationBuild=true - } else { - & dotnet pack -c Release --include-source --no-build -o ../../artifacts -p:ContinuousIntegrationBuild=true - } - if($LASTEXITCODE -ne 0) { exit 1 } +Write-Output "build: Package version suffix is $suffix" +Write-Output "build: Build version suffix is $buildSuffix" - Pop-Location -} - -foreach ($test in gci test/*.Tests) { - Push-Location $test - - echo "build: Testing project in $test" +& dotnet build --configuration Release --version-suffix=$buildSuffix /p:ContinuousIntegrationBuild=true - & dotnet test -c Release - if($LASTEXITCODE -ne 0) { exit 3 } +if($LASTEXITCODE -ne 0) { throw 'build failed' } - Pop-Location +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 } -foreach ($test in gci test/*.PerformanceTests) { - Push-Location $test +if($LASTEXITCODE -ne 0) { throw 'pack failed' } - echo "build: Building performance test project in $test" +Write-Output "build: Testing" - & dotnet build -c Release - if($LASTEXITCODE -ne 0) { exit 2 } - - Pop-Location -} +# 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 -Pop-Location \ No newline at end of file +if($LASTEXITCODE -ne 0) { throw 'unit tests failed' } \ No newline at end of file diff --git a/Directory.Build.props b/Directory.Build.props index c402103f..736814b0 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -2,10 +2,10 @@ latest True - + true + $(MSBuildThisFileDirectory)assets/Serilog.snk + enable + false + enable - - - - diff --git a/README.md b/README.md index cc4c7043..9695b7b7 100644 --- a/README.md +++ b/README.md @@ -56,7 +56,7 @@ For a more sophisticated example go to the [sample](sample/Sample) folder. Root section name can be changed: -```json +```yaml { "CustomSection": { ... @@ -65,8 +65,9 @@ Root section name can be changed: ``` ```csharp +var options = new ConfigurationReaderOptions { SectionName = "CustomSection" }; var logger = new LoggerConfiguration() - .ReadFrom.Configuration(configuration, sectionName: "CustomSection") + .ReadFrom.Configuration(configuration, options) .CreateLogger(); ``` @@ -74,7 +75,7 @@ var logger = new LoggerConfiguration() `Using` section contains list of **assemblies** in which configuration methods (`WriteTo.File()`, `Enrich.WithThreadId()`) reside. -```json +```yaml "Serilog": { "Using": [ "Serilog.Sinks.Console", "Serilog.Enrichers.Thread", /* ... */ ], // ... @@ -83,7 +84,7 @@ var logger = new LoggerConfiguration() For .NET Core projects build tools produce `.deps.json` files and this package implements a convention using `Microsoft.Extensions.DependencyModel` to find any package among dependencies with `Serilog` anywhere in the name and pulls configuration methods from it, so the `Using` section in example above can be omitted: -```json +```yaml { "Serilog": { "MinimumLevel": "Debug", @@ -106,8 +107,9 @@ In case of [non-standard](#azure-functions-v2-v3) dependency management you can ```csharp var functionDependencyContext = DependencyContext.Load(typeof(Startup).Assembly); +var options = new ConfigurationReaderOptions(functionDependencyContext) { SectionName = "AzureFunctionsJobHost:Serilog" }; var logger = new LoggerConfiguration() - .ReadFrom.Configuration(hostConfig, sectionName: "AzureFunctionsJobHost:Serilog", dependencyContext: functionDependencyContext) + .ReadFrom.Configuration(hostConfig, options) .CreateLogger(); ``` @@ -119,8 +121,9 @@ var configurationAssemblies = new[] typeof(ConsoleLoggerConfigurationExtensions).Assembly, typeof(FileLoggerConfigurationExtensions).Assembly, }; +var options = new ConfigurationReaderOptions(configurationAssemblies); var logger = new LoggerConfiguration() - .ReadFrom.Configuration(configuration, configurationAssemblies) + .ReadFrom.Configuration(configuration, options) .CreateLogger(); ``` @@ -183,6 +186,22 @@ You can also declare `LoggingLevelSwitch`-es in custom section and reference the Level updates to switches are also respected for a dynamic update. +Since version 7.0.0, both declared switches (i.e. `Serilog:LevelSwitches` section) and minimum level override switches (i.e. `Serilog:MinimumLevel:Override` section) are exposed through a callback on the reader options so that a reference can be kept: + +```csharp +var allSwitches = new Dictionary(); +var options = new ConfigurationReaderOptions +{ + OnLevelSwitchCreated = (switchName, levelSwitch) => allSwitches[switchName] = levelSwitch +}; + +var logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration, options) + .CreateLogger(); + +LoggingLevelSwitch controlSwitch = allSwitches["$controlSwitch"]; +``` + ### WriteTo, Enrich, AuditTo, Destructure sections These sections support simplified syntax, for example the following is valid if no arguments are needed by the sinks: @@ -195,7 +214,7 @@ Or alternatively, the long-form (`"Name":` ...) syntax from the example above ca By `Microsoft.Extensions.Configuration.Json` convention, array syntax implicitly defines index for each element in order to make unique paths for configuration keys. So the example above is equivalent to: -```json +```yaml "WriteTo": { "0": "Console", "1": "DiagnosticTrace" @@ -204,7 +223,7 @@ By `Microsoft.Extensions.Configuration.Json` convention, array syntax implicitly And -```json +```yaml "WriteTo:0": "Console", "WriteTo:1": "DiagnosticTrace" ``` @@ -213,7 +232,7 @@ And When overriding settings with [environment variables](https://docs.microsoft.com/en-us/aspnet/core/fundamentals/configuration/?view=aspnetcore-3.1#environment-variables) it becomes less convenient and fragile, so you can specify custom names: -```json +```yaml "WriteTo": { "ConsoleSink": "Console", "DiagnosticTraceSink": { "Name": "DiagnosticTrace" } @@ -226,9 +245,9 @@ This section defines a static list of key-value pairs that will enrich log event ### Filter section -This section defines filters that will be applied to log events. It is especially usefull in combination with _[Serilog.Expressions](https://github.com/serilog/serilog-expressions)_ (or legacy _[Serilog.Filters.Expressions](https://github.com/serilog/serilog-filters-expressions)_) package so you can write expression in text form: +This section defines filters that will be applied to log events. It is especially useful in combination with _[Serilog.Expressions](https://github.com/serilog/serilog-expressions)_ (or legacy _[Serilog.Filters.Expressions](https://github.com/serilog/serilog-filters-expressions)_) package so you can write expression in text form: -```json +```yaml "Filter": [{ "Name": "ByIncludingOnly", "Args": { @@ -239,7 +258,7 @@ This section defines filters that will be applied to log events. It is especiall Using this package you can also declare `LoggingFilterSwitch`-es in custom section and reference them for filter parameters: -```json +```yaml { "Serilog": { "FilterSwitches": { "filterSwitch": "Application = 'Sample'" }, @@ -256,11 +275,27 @@ Using this package you can also declare `LoggingFilterSwitch`-es in custom secti Level updates to switches are also respected for a dynamic update. +Since version 7.0.0, filter switches are exposed through a callback on the reader options so that a reference can be kept: + +```csharp +var filterSwitches = new Dictionary(); +var options = new ConfigurationReaderOptions +{ + OnFilterSwitchCreated = (switchName, filterSwitch) => filterSwitches[switchName] = filterSwitch +}; + +var logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration, options) + .CreateLogger(); + +ILoggingFilterSwitch filterSwitch = filterSwitches["filterSwitch"]; +``` + ### Nested configuration sections Some Serilog packages require a reference to a logger configuration object. The sample program in this project illustrates this with the following entry configuring the _[Serilog.Sinks.Async](https://github.com/serilog/serilog-sinks-async)_ package to wrap the _[Serilog.Sinks.File](https://github.com/serilog/serilog-sinks-file)_ package. The `configure` parameter references the File sink configuration: -```json +```yaml "WriteTo:Async": { "Name": "Async", "Args": { @@ -282,11 +317,13 @@ Some Serilog packages require a reference to a logger configuration object. The When the configuration specifies a discrete value for a parameter (such as a string literal), the package will attempt to convert that value to the target method's declared CLR type of the parameter. Additional explicit handling is provided for parsing strings to `Uri`, `TimeSpan`, `enum`, arrays and custom collections. +Since version 7.0.0, conversion will use the invariant culture (`CultureInfo.InvariantCulture`) as long as the `ReadFrom.Configuration(IConfiguration configuration, ConfigurationReaderOptions options)` method is used. Obsolete methods use the current culture to preserve backward compatibility. + ### Static member support Static member access can be used for passing to the configuration argument via [special](https://github.com/serilog/serilog-settings-configuration/blob/dev/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs#L35) syntax: -```json +```yaml { "Args": { "encoding": "System.Text.Encoding::UTF8", @@ -299,7 +336,7 @@ Static member access can be used for passing to the configuration argument via [ If the parameter value is not a discrete value, it will try to find a best matching public constructor for the argument: -```json +```yaml { "Name": "Console", "Args": { @@ -307,6 +344,7 @@ If the parameter value is not a discrete value, it will try to find a best match // `type` (or $type) is optional, must be specified for abstract declared parameter types "type": "Serilog.Templates.ExpressionTemplate, Serilog.Expressions", "template": "[{@t:HH:mm:ss} {@l:u3} {Coalesce(SourceContext, '')}] {@m}\n{@x}" + } } } ``` @@ -317,7 +355,7 @@ For other cases the package will use the configuration binding system provided b If parameter type is an interface or an abstract class you need to specify the full type name that implements abstract type. The implementation type should have parameterless constructor. -```json +```yaml "Destructure": [ { "Name": "With", "Args": { "policy": "Sample.CustomPolicy, Sample" } }, ... @@ -377,8 +415,9 @@ public class Startup : FunctionsStartup var functionDependencyContext = DependencyContext.Load(typeof(Startup).Assembly); var hostConfig = sp.GetRequiredService(); + var options = new ConfigurationReaderOptions(functionDependencyContext) { SectionName = "AzureFunctionsJobHost:Serilog" }; var logger = new LoggerConfiguration() - .ReadFrom.Configuration(hostConfig, sectionName: "AzureFunctionsJobHost:Serilog", dependencyContext: functionDependencyContext) + .ReadFrom.Configuration(hostConfig, options) .CreateLogger(); return new SerilogLoggerProvider(logger, dispose: true); @@ -405,3 +444,7 @@ In order to make auto-discovery of configuration assemblies work, modify Functio ``` + +### Versioning + +This package tracks the versioning and target framework support of its [_Microsoft.Extensions.Configuration_](https://nuget.org/packages/Microsoft.Extensions.Configuration) dependency. diff --git a/appveyor.yml b/appveyor.yml index 272c7d79..b8049994 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -5,9 +5,27 @@ skip_tags: true image: - Visual Studio 2022 - Ubuntu + - macOS build_script: -- pwsh: ./Build.ps1 +- 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 diff --git a/global.json b/global.json new file mode 100644 index 00000000..1d927afd --- /dev/null +++ b/global.json @@ -0,0 +1,7 @@ +{ + "sdk": { + "version": "7.0.201", + "allowPrerelease": false, + "rollForward": "latestFeature" + } +} diff --git a/sample/Sample/CustomFilter.cs b/sample/Sample/CustomFilter.cs new file mode 100644 index 00000000..99083c61 --- /dev/null +++ b/sample/Sample/CustomFilter.cs @@ -0,0 +1,21 @@ +using Serilog.Core; +using Serilog.Events; + +namespace Sample; + +// The filter syntax in the sample configuration file is +// processed by the Serilog.Filters.Expressions package. +public class CustomFilter : ILogEventFilter +{ + readonly LogEventLevel _levelFilter; + + public CustomFilter(LogEventLevel levelFilter = LogEventLevel.Information) + { + _levelFilter = levelFilter; + } + + public bool IsEnabled(LogEvent logEvent) + { + return logEvent.Level >= _levelFilter; + } +} diff --git a/sample/Sample/CustomPolicy.cs b/sample/Sample/CustomPolicy.cs new file mode 100644 index 00000000..56e1b2ae --- /dev/null +++ b/sample/Sample/CustomPolicy.cs @@ -0,0 +1,24 @@ +using System.Diagnostics.CodeAnalysis; +using Serilog.Core; +using Serilog.Events; + +namespace Sample; + +public class CustomPolicy : IDestructuringPolicy +{ + public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, [NotNullWhen(true)] out LogEventPropertyValue? result) + { + result = null; + + if (value is LoginData loginData) + { + result = new StructureValue( + new List + { + new("Username", new ScalarValue(loginData.Username)) + }); + } + + return (result != null); + } +} diff --git a/sample/Sample/LoginData.cs b/sample/Sample/LoginData.cs new file mode 100644 index 00000000..2d517a74 --- /dev/null +++ b/sample/Sample/LoginData.cs @@ -0,0 +1,8 @@ +namespace Sample; + +public class LoginData +{ + public string? Username; + // ReSharper disable once NotAccessedField.Global + public string? Password; +} diff --git a/sample/Sample/Program.cs b/sample/Sample/Program.cs index 4f4d7f9f..8440c57f 100644 --- a/sample/Sample/Program.cs +++ b/sample/Sample/Program.cs @@ -1,106 +1,46 @@ -using System; - -using System.IO; -using System.Linq; -using System.Collections.Generic; -using System.Threading; - -using Microsoft.Extensions.Configuration; - +using Microsoft.Extensions.Configuration; +using Sample; using Serilog; using Serilog.Core; -using Serilog.Events; using Serilog.Debugging; // ReSharper disable UnusedType.Global -namespace Sample -{ - public class Program - { - public static void Main(string[] args) - { - SelfLog.Enable(Console.Error); - - Thread.CurrentThread.Name = "Main thread"; - - var configuration = new ConfigurationBuilder() - .SetBasePath(Directory.GetCurrentDirectory()) - .AddJsonFile(path: "appsettings.json", optional: false, reloadOnChange: true) - .Build(); +SelfLog.Enable(Console.Error); - var logger = new LoggerConfiguration() - .ReadFrom.Configuration(configuration) - .CreateLogger(); +Thread.CurrentThread.Name = "Main thread"; - logger.Information("Args: {Args}", args); +var configuration = new ConfigurationBuilder() + .SetBasePath(Directory.GetCurrentDirectory()) + .AddJsonFile(path: "appsettings.json", optional: false, reloadOnChange: true) + .Build(); - do - { - logger.ForContext().Information("Hello, world!"); - logger.ForContext().Error("Hello, world!"); - logger.ForContext(Constants.SourceContextPropertyName, "Microsoft").Warning("Hello, world!"); - logger.ForContext(Constants.SourceContextPropertyName, "Microsoft").Error("Hello, world!"); - logger.ForContext(Constants.SourceContextPropertyName, "MyApp.Something.Tricky").Verbose("Hello, world!"); +var logger = new LoggerConfiguration() + .ReadFrom.Configuration(configuration) + .CreateLogger(); - logger.Information("Destructure with max object nesting depth:\n{@NestedObject}", - new { FiveDeep = new { Two = new { Three = new { Four = new { Five = "the end" } } } } }); +logger.Information("Args: {Args}", args); - logger.Information("Destructure with max string length:\n{@LongString}", - new { TwentyChars = "0123456789abcdefghij" }); - - logger.Information("Destructure with max collection count:\n{@BigData}", - new { TenItems = new[] { "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten" } }); - - logger.Information("Destructure with policy to strip password:\n{@LoginData}", - new LoginData { Username = "BGates", Password = "isityearoflinuxyet" }); - - Console.WriteLine("\nPress \"q\" to quit, or any other key to run again.\n"); - } - while (!args.Contains("--run-once") && (Console.ReadKey().KeyChar != 'q')); - } - } - - // The filter syntax in the sample configuration file is - // processed by the Serilog.Filters.Expressions package. - public class CustomFilter : ILogEventFilter - { - readonly LogEventLevel _levelFilter; - - public CustomFilter(LogEventLevel levelFilter = LogEventLevel.Information) - { - _levelFilter = levelFilter; - } +do +{ + logger.ForContext().Information("Hello, world!"); + logger.ForContext().Error("Hello, world!"); + logger.ForContext(Constants.SourceContextPropertyName, "Microsoft").Warning("Hello, world!"); + logger.ForContext(Constants.SourceContextPropertyName, "Microsoft").Error("Hello, world!"); + logger.ForContext(Constants.SourceContextPropertyName, "MyApp.Something.Tricky").Verbose("Hello, world!"); - public bool IsEnabled(LogEvent logEvent) - { - return logEvent.Level >= _levelFilter; - } - } + logger.Information("Destructure with max object nesting depth:\n{@NestedObject}", + new { FiveDeep = new { Two = new { Three = new { Four = new { Five = "the end" } } } } }); - public class LoginData - { - public string Username; - // ReSharper disable once NotAccessedField.Global - public string Password; - } + logger.Information("Destructure with max string length:\n{@LongString}", + new { TwentyChars = "0123456789abcdefghij" }); - public class CustomPolicy : IDestructuringPolicy - { - public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventPropertyValue result) - { - result = null; + logger.Information("Destructure with max collection count:\n{@BigData}", + new { TenItems = new[] { "one", "two", "three", "four", "five", "six", "seven", "eight", "nine", "ten" } }); - if (value is LoginData loginData) - { - result = new StructureValue( - new List - { - new("Username", new ScalarValue(loginData.Username)) - }); - } + logger.Information("Destructure with policy to strip password:\n{@LoginData}", + new LoginData { Username = "BGates", Password = "isityearoflinuxyet" }); - return (result != null); - } - } + Console.WriteLine("\nPress \"q\" to quit, or any other key to run again.\n"); } +while (!args.Contains("--run-once") && (Console.ReadKey().KeyChar != 'q')); diff --git a/sample/Sample/Sample.csproj b/sample/Sample/Sample.csproj index 25aedf5b..6357b2be 100644 --- a/sample/Sample/Sample.csproj +++ b/sample/Sample/Sample.csproj @@ -1,7 +1,7 @@  - net6.0;netcoreapp3.1;net462 + net6.0;net7.0;net462 Exe @@ -14,7 +14,7 @@ - + @@ -22,6 +22,7 @@ + diff --git a/serilog-settings-configuration.sln b/serilog-settings-configuration.sln index ec9c9b22..f0209fd7 100644 --- a/serilog-settings-configuration.sln +++ b/serilog-settings-configuration.sln @@ -17,6 +17,7 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "assets", "assets", "{62D0B9 LICENSE = LICENSE README.md = README.md serilog-settings-configuration.sln.DotSettings = serilog-settings-configuration.sln.DotSettings + global.json = global.json EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3}" @@ -31,6 +32,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Sample", "sample\Sample\Sam EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TestDummies", "test\TestDummies\TestDummies.csproj", "{B7CF5068-DD19-4868-A268-5280BDE90361}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "TestApp", "test\TestApp\TestApp.csproj", "{1B6E08F3-16C9-4912-BEEE-57DB78C92A12}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -53,6 +56,8 @@ Global {B7CF5068-DD19-4868-A268-5280BDE90361}.Debug|Any CPU.Build.0 = Debug|Any CPU {B7CF5068-DD19-4868-A268-5280BDE90361}.Release|Any CPU.ActiveCfg = Release|Any CPU {B7CF5068-DD19-4868-A268-5280BDE90361}.Release|Any CPU.Build.0 = Release|Any CPU + {1B6E08F3-16C9-4912-BEEE-57DB78C92A12}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1B6E08F3-16C9-4912-BEEE-57DB78C92A12}.Release|Any CPU.ActiveCfg = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -62,6 +67,7 @@ Global {F793C6E8-C40A-4018-8884-C97E2BE38A54} = {D551DCB0-7771-4D01-BEBD-F7B57D1CF0E3} {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} 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 2fdb7b2f..c9460776 100644 --- a/serilog-settings-configuration.sln.DotSettings +++ b/serilog-settings-configuration.sln.DotSettings @@ -1,6 +1,6 @@  - - + + True True True @@ -477,7 +477,7 @@ II.2.12 <HandlesEvent /> </Patterns> CustomLayout - + True False True @@ -538,11 +538,13 @@ 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" /> + True True True True True True + True True True True @@ -551,11 +553,11 @@ II.2.12 <HandlesEvent /> True True True - - - - + + + + <data /> <data><IncludeFilters /><ExcludeFilters /></data> True - True \ No newline at end of file + True diff --git a/src/Serilog.Settings.Configuration/ConfigurationLoggerConfigurationExtensions.cs b/src/Serilog.Settings.Configuration/ConfigurationLoggerConfigurationExtensions.cs index aa709456..0a2bb7d3 100644 --- a/src/Serilog.Settings.Configuration/ConfigurationLoggerConfigurationExtensions.cs +++ b/src/Serilog.Settings.Configuration/ConfigurationLoggerConfigurationExtensions.cs @@ -1,4 +1,4 @@ -// Copyright 2013-2016 Serilog Contributors +// Copyright 2013-2016 Serilog Contributors // // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. @@ -12,7 +12,6 @@ // See the License for the specific language governing permissions and // limitations under the License. -using System; using System.Reflection; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyModel; @@ -20,189 +19,229 @@ using Serilog.Settings.Configuration; using Serilog.Settings.Configuration.Assemblies; -namespace Serilog +namespace Serilog; + +/// +/// Extends with support for System.Configuration appSettings elements. +/// +public static class ConfigurationLoggerConfigurationExtensions { /// - /// Extends with support for System.Configuration appSettings elements. + /// Configuration section name required by this package. + /// + public const string DefaultSectionName = "Serilog"; + + /// + /// Reads logger settings from the provided configuration object using the provided section name. Generally this + /// is preferable over the other method that takes a configuration section. Only this version will populate + /// IConfiguration parameters on target methods. /// - public static class ConfigurationLoggerConfigurationExtensions + /// Logger setting configuration. + /// A configuration object which contains a Serilog section. + /// A section name for section which contains a Serilog section. + /// The dependency context from which sink/enricher packages can be located. If not supplied, the platform + /// default will be used. + /// An object allowing configuration to continue. + [Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, ConfigurationReaderOptions readerOptions) instead.")] + public static LoggerConfiguration Configuration( + this LoggerSettingsConfiguration settingConfiguration, + IConfiguration configuration, + string sectionName, + DependencyContext? dependencyContext = null) { - /// - /// Configuration section name required by this package. - /// - public const string DefaultSectionName = "Serilog"; - - /// - /// Reads logger settings from the provided configuration object using the provided section name. Generally this - /// is preferable over the other method that takes a configuration section. Only this version will populate - /// IConfiguration parameters on target methods. - /// - /// Logger setting configuration. - /// A configuration object which contains a Serilog section. - /// A section name for section which contains a Serilog section. - /// The dependency context from which sink/enricher packages can be located. If not supplied, the platform - /// default will be used. - /// An object allowing configuration to continue. - public static LoggerConfiguration Configuration( - this LoggerSettingsConfiguration settingConfiguration, - IConfiguration configuration, - string sectionName, - DependencyContext dependencyContext = null) - { - if (settingConfiguration == null) throw new ArgumentNullException(nameof(settingConfiguration)); - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - if (sectionName == null) throw new ArgumentNullException(nameof(sectionName)); - - var assemblyFinder = dependencyContext == null - ? AssemblyFinder.Auto() - : AssemblyFinder.ForDependencyContext(dependencyContext); - - return settingConfiguration.Settings( - new ConfigurationReader( - configuration.GetSection(sectionName), - assemblyFinder, - configuration)); - } - - /// - /// Reads logger settings from the provided configuration object using the default section name. Generally this - /// is preferable over the other method that takes a configuration section. Only this version will populate - /// IConfiguration parameters on target methods. - /// - /// Logger setting configuration. - /// A configuration object which contains a Serilog section. - /// The dependency context from which sink/enricher packages can be located. If not supplied, the platform - /// default will be used. - /// An object allowing configuration to continue. - public static LoggerConfiguration Configuration( - this LoggerSettingsConfiguration settingConfiguration, - IConfiguration configuration, - DependencyContext dependencyContext = null) - => Configuration(settingConfiguration, configuration, DefaultSectionName, dependencyContext); - - /// - /// Reads logger settings from the provided configuration section. Generally it is preferable to use the other - /// extension method that takes the full configuration object. - /// - /// Logger setting configuration. - /// The Serilog configuration section - /// The dependency context from which sink/enricher packages can be located. If not supplied, the platform - /// default will be used. - /// An object allowing configuration to continue. - [Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, string sectionName, DependencyContext dependencyContext) instead.")] - public static LoggerConfiguration ConfigurationSection( - this LoggerSettingsConfiguration settingConfiguration, - IConfigurationSection configSection, - DependencyContext dependencyContext = null) - { - if (settingConfiguration == null) throw new ArgumentNullException(nameof(settingConfiguration)); - if (configSection == null) throw new ArgumentNullException(nameof(configSection)); - - var assemblyFinder = dependencyContext == null - ? AssemblyFinder.Auto() - : AssemblyFinder.ForDependencyContext(dependencyContext); - - return settingConfiguration.Settings( - new ConfigurationReader( - configSection, - assemblyFinder, - configuration: null)); - } - - /// - /// Reads logger settings from the provided configuration object using the provided section name. Generally this - /// is preferable over the other method that takes a configuration section. Only this version will populate - /// IConfiguration parameters on target methods. - /// - /// Logger setting configuration. - /// A configuration object which contains a Serilog section. - /// A section name for section which contains a Serilog section. - /// Defines how the package identifies assemblies to scan for sinks and other types. - /// An object allowing configuration to continue. - public static LoggerConfiguration Configuration( - this LoggerSettingsConfiguration settingConfiguration, - IConfiguration configuration, - string sectionName, - ConfigurationAssemblySource configurationAssemblySource) - { - if (settingConfiguration == null) throw new ArgumentNullException(nameof(settingConfiguration)); - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - if (sectionName == null) throw new ArgumentNullException(nameof(sectionName)); - - var assemblyFinder = AssemblyFinder.ForSource(configurationAssemblySource); - - return settingConfiguration.Settings(new ConfigurationReader(configuration.GetSection(sectionName), assemblyFinder, configuration)); - } - - /// - /// Reads logger settings from the provided configuration object using the default section name. Generally this - /// is preferable over the other method that takes a configuration section. Only this version will populate - /// IConfiguration parameters on target methods. - /// - /// Logger setting configuration. - /// A configuration object which contains a Serilog section. - /// Defines how the package identifies assemblies to scan for sinks and other types. - /// An object allowing configuration to continue. - public static LoggerConfiguration Configuration( - this LoggerSettingsConfiguration settingConfiguration, - IConfiguration configuration, - ConfigurationAssemblySource configurationAssemblySource) - => Configuration(settingConfiguration, configuration, DefaultSectionName, configurationAssemblySource); - - /// - /// Reads logger settings from the provided configuration section. Generally it is preferable to use the other - /// extension method that takes the full configuration object. - /// - /// Logger setting configuration. - /// The Serilog configuration section - /// Defines how the package identifies assemblies to scan for sinks and other types. - /// An object allowing configuration to continue. - [Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, string sectionName, ConfigurationAssemblySource configurationAssemblySource) instead.")] - public static LoggerConfiguration ConfigurationSection( - this LoggerSettingsConfiguration settingConfiguration, - IConfigurationSection configSection, - ConfigurationAssemblySource configurationAssemblySource) - { - if (settingConfiguration == null) throw new ArgumentNullException(nameof(settingConfiguration)); - if (configSection == null) throw new ArgumentNullException(nameof(configSection)); - - var assemblyFinder = AssemblyFinder.ForSource(configurationAssemblySource); - - return settingConfiguration.Settings(new ConfigurationReader(configSection, assemblyFinder, configuration: null)); - } - - /// - /// Reads logger settings from the provided configuration object using the provided section name. - /// - /// Logger setting configuration. - /// A configuration object which contains a Serilog section. - /// A section name for section which contains a Serilog section. - /// A collection of assemblies that contains sinks and other types. - /// An object allowing configuration to continue. - public static LoggerConfiguration Configuration( - this LoggerSettingsConfiguration settingConfiguration, - IConfiguration configuration, - string sectionName, - params Assembly[] assemblies) + if (settingConfiguration == null) throw new ArgumentNullException(nameof(settingConfiguration)); + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + if (sectionName == null) throw new ArgumentNullException(nameof(sectionName)); + + var readerOptions = new ConfigurationReaderOptions(dependencyContext) { SectionName = sectionName, FormatProvider = null }; + return Configuration(settingConfiguration, configuration, readerOptions); + } + + /// + /// Reads logger settings from the provided configuration object using the default section name. Generally this + /// is preferable over the other method that takes a configuration section. Only this version will populate + /// IConfiguration parameters on target methods. + /// + /// Logger setting configuration. + /// A configuration object which contains a Serilog section. + /// The dependency context from which sink/enricher packages can be located. If not supplied, the platform + /// default will be used. + /// An object allowing configuration to continue. + [Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, ConfigurationReaderOptions readerOptions) instead.")] + public static LoggerConfiguration Configuration( + this LoggerSettingsConfiguration settingConfiguration, + IConfiguration configuration, + DependencyContext dependencyContext) + => Configuration(settingConfiguration, configuration, DefaultSectionName, dependencyContext); + + /// + /// Reads logger settings from the provided configuration section. Generally it is preferable to use the other + /// extension method that takes the full configuration object. + /// + /// Logger setting configuration. + /// The Serilog configuration section + /// The dependency context from which sink/enricher packages can be located. If not supplied, the platform + /// default will be used. + /// An object allowing configuration to continue. + [Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, string sectionName, DependencyContext dependencyContext) instead.")] + public static LoggerConfiguration ConfigurationSection( + this LoggerSettingsConfiguration settingConfiguration, + IConfigurationSection configSection, + DependencyContext? dependencyContext = null) + { + if (settingConfiguration == null) throw new ArgumentNullException(nameof(settingConfiguration)); + if (configSection == null) throw new ArgumentNullException(nameof(configSection)); + + var assemblyFinder = dependencyContext == null + ? AssemblyFinder.Auto() + : AssemblyFinder.ForDependencyContext(dependencyContext); + + return settingConfiguration.Settings( + new ConfigurationReader( + configSection, + assemblyFinder, + new ConfigurationReaderOptions { FormatProvider = null }, + configuration: null)); + } + + /// + /// Reads logger settings from the provided configuration object using the provided section name. Generally this + /// is preferable over the other method that takes a configuration section. Only this version will populate + /// IConfiguration parameters on target methods. + /// + /// Logger setting configuration. + /// A configuration object which contains a Serilog section. + /// A section name for section which contains a Serilog section. + /// Defines how the package identifies assemblies to scan for sinks and other types. + /// An object allowing configuration to continue. + [Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, ConfigurationReaderOptions readerOptions) instead.")] + public static LoggerConfiguration Configuration( + this LoggerSettingsConfiguration settingConfiguration, + IConfiguration configuration, + string sectionName, + ConfigurationAssemblySource configurationAssemblySource) + { + if (settingConfiguration == null) throw new ArgumentNullException(nameof(settingConfiguration)); + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + if (sectionName == null) throw new ArgumentNullException(nameof(sectionName)); + + var readerOptions = new ConfigurationReaderOptions(configurationAssemblySource) { SectionName = sectionName, FormatProvider = null }; + return Configuration(settingConfiguration, configuration, readerOptions); + } + + /// + /// Reads logger settings from the provided configuration object using the default section name. Generally this + /// is preferable over the other method that takes a configuration section. Only this version will populate + /// IConfiguration parameters on target methods. + /// + /// Logger setting configuration. + /// A configuration object which contains a Serilog section. + /// Defines how the package identifies assemblies to scan for sinks and other types. + /// An object allowing configuration to continue. + [Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, ConfigurationReaderOptions readerOptions) instead.")] + public static LoggerConfiguration Configuration( + this LoggerSettingsConfiguration settingConfiguration, + IConfiguration configuration, + ConfigurationAssemblySource configurationAssemblySource) + => Configuration(settingConfiguration, configuration, DefaultSectionName, configurationAssemblySource); + + /// + /// Reads logger settings from the provided configuration section. Generally it is preferable to use the other + /// extension method that takes the full configuration object. + /// + /// Logger setting configuration. + /// The Serilog configuration section + /// Defines how the package identifies assemblies to scan for sinks and other types. + /// An object allowing configuration to continue. + [Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, string sectionName, ConfigurationAssemblySource configurationAssemblySource) instead.")] + public static LoggerConfiguration ConfigurationSection( + this LoggerSettingsConfiguration settingConfiguration, + IConfigurationSection configSection, + ConfigurationAssemblySource configurationAssemblySource) + { + if (settingConfiguration == null) throw new ArgumentNullException(nameof(settingConfiguration)); + if (configSection == null) throw new ArgumentNullException(nameof(configSection)); + + var assemblyFinder = AssemblyFinder.ForSource(configurationAssemblySource); + + return settingConfiguration.Settings(new ConfigurationReader(configSection, assemblyFinder, new ConfigurationReaderOptions { FormatProvider = null }, configuration: null)); + } + + /// + /// Reads logger settings from the provided configuration object using the provided section name. + /// + /// Logger setting configuration. + /// A configuration object which contains a Serilog section. + /// A section name for section which contains a Serilog section. + /// A collection of assemblies that contains sinks and other types. + /// An object allowing configuration to continue. + [Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, ConfigurationReaderOptions readerOptions) instead.")] + public static LoggerConfiguration Configuration( + this LoggerSettingsConfiguration settingConfiguration, + IConfiguration configuration, + string sectionName, + params Assembly[] assemblies) + { + if (settingConfiguration == null) throw new ArgumentNullException(nameof(settingConfiguration)); + if (configuration == null) throw new ArgumentNullException(nameof(configuration)); + if (sectionName == null) throw new ArgumentNullException(nameof(sectionName)); + + var readerOptions = new ConfigurationReaderOptions(assemblies) { SectionName = sectionName, FormatProvider = null }; + return Configuration(settingConfiguration, configuration, readerOptions); + } + + /// + /// Reads logger settings from the provided configuration object using the default section name. + /// + /// Logger setting configuration. + /// A configuration object which contains a Serilog section. + /// A collection of assemblies that contains sinks and other types. + /// An object allowing configuration to continue. + [Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, ConfigurationReaderOptions readerOptions) instead.")] + public static LoggerConfiguration Configuration( + this LoggerSettingsConfiguration settingConfiguration, + IConfiguration configuration, + params Assembly[] assemblies) + => Configuration(settingConfiguration, configuration, DefaultSectionName, assemblies); + + /// + /// Reads logger settings from the provided configuration object using the specified context. + /// + /// Logger setting configuration. + /// A configuration object which contains a Serilog section. + /// Options to adjust how the configuration object is processed. + /// An object allowing configuration to continue. + public static LoggerConfiguration Configuration( + this LoggerSettingsConfiguration settingConfiguration, + IConfiguration configuration, + ConfigurationReaderOptions? readerOptions = null) + { + var configurationReader = readerOptions switch { - if (settingConfiguration == null) throw new ArgumentNullException(nameof(settingConfiguration)); - if (configuration == null) throw new ArgumentNullException(nameof(configuration)); - if (sectionName == null) throw new ArgumentNullException(nameof(sectionName)); - - return settingConfiguration.Settings(new ConfigurationReader(configuration.GetSection(sectionName), assemblies, new ResolutionContext(configuration))); - } - - /// - /// Reads logger settings from the provided configuration object using the default section name. - /// - /// Logger setting configuration. - /// A configuration object which contains a Serilog section. - /// A collection of assemblies that contains sinks and other types. - /// An object allowing configuration to continue. - public static LoggerConfiguration Configuration( - this LoggerSettingsConfiguration settingConfiguration, - IConfiguration configuration, - params Assembly[] assemblies) - => Configuration(settingConfiguration, configuration, DefaultSectionName, assemblies); + { ConfigurationAssemblySource: {} } => GetConfigurationReader(configuration, readerOptions, readerOptions.ConfigurationAssemblySource.Value), + { Assemblies: {} } => GetConfigurationReader(configuration, readerOptions, readerOptions.Assemblies), + _ => GetConfigurationReader(configuration, readerOptions ?? new ConfigurationReaderOptions(), readerOptions?.DependencyContext), + }; + return settingConfiguration.Settings(configurationReader); + } + + static ConfigurationReader GetConfigurationReader(IConfiguration configuration, ConfigurationReaderOptions readerOptions, DependencyContext? dependencyContext) + { + var assemblyFinder = dependencyContext == null ? AssemblyFinder.Auto() : AssemblyFinder.ForDependencyContext(dependencyContext); + var section = string.IsNullOrWhiteSpace(readerOptions.SectionName) ? configuration : configuration.GetSection(readerOptions.SectionName!); + return new ConfigurationReader(section, assemblyFinder, readerOptions, configuration); + } + + static ConfigurationReader GetConfigurationReader(IConfiguration configuration, ConfigurationReaderOptions readerOptions, ConfigurationAssemblySource source) + { + var assemblyFinder = AssemblyFinder.ForSource(source); + var section = string.IsNullOrWhiteSpace(readerOptions.SectionName) ? configuration : configuration.GetSection(readerOptions.SectionName!); + return new ConfigurationReader(section, assemblyFinder, readerOptions, configuration); + } + + static ConfigurationReader GetConfigurationReader(IConfiguration configuration, ConfigurationReaderOptions readerOptions, IReadOnlyCollection assemblies) + { + var section = string.IsNullOrWhiteSpace(readerOptions.SectionName) ? configuration : configuration.GetSection(readerOptions.SectionName!); + return new ConfigurationReader(section, assemblies, new ResolutionContext(configuration, readerOptions)); } } diff --git a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj index 76b2299c..ed870186 100644 --- a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj +++ b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj @@ -2,20 +2,16 @@ Microsoft.Extensions.Configuration (appsettings.json) support for Serilog. - 3.4.0 - latest + + 7.0.0 Serilog Contributors - netstandard2.0;net451;net461 - true + + net462;netstandard2.0;net6.0;net7.0 true Serilog.Settings.Configuration - ../../assets/Serilog.snk - true - true - Serilog.Settings.Configuration serilog;json icon.png - https://github.com/serilog/serilog-settings-configuration/ Apache-2.0 Serilog true @@ -25,17 +21,19 @@ - - - - + - - + + + + + - - + + + + diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/AssemblyFinder.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/AssemblyFinder.cs index cc16a557..9582e6a4 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/AssemblyFinder.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/AssemblyFinder.cs @@ -1,50 +1,34 @@ -using System; -using System.Collections.Generic; -using System.Reflection; +using System.Reflection; using Microsoft.Extensions.DependencyModel; -namespace Serilog.Settings.Configuration.Assemblies +namespace Serilog.Settings.Configuration.Assemblies; + +abstract class AssemblyFinder { - abstract class AssemblyFinder - { - public abstract IReadOnlyList FindAssembliesContainingName(string nameToFind); + public abstract IReadOnlyList FindAssembliesContainingName(string nameToFind); - protected static bool IsCaseInsensitiveMatch(string text, string textToFind) - { - return text != null && text.ToLowerInvariant().Contains(textToFind.ToLowerInvariant()); - } + protected static bool IsCaseInsensitiveMatch(string? text, string textToFind) + { + return text != null && text.ToLowerInvariant().Contains(textToFind.ToLowerInvariant()); + } - public static AssemblyFinder Auto() - { - try - { - // Need to check `Assembly.GetEntryAssembly()` first because - // `DependencyContext.Default` throws an exception when `Assembly.GetEntryAssembly()` returns null - if (Assembly.GetEntryAssembly() != null && DependencyContext.Default != null) - { - return new DependencyContextAssemblyFinder(DependencyContext.Default); - } - } - catch (NotSupportedException) when (typeof(object).Assembly.Location is "") // bundled mode detection - { - } - - return new DllScanningAssemblyFinder(); - } + public static AssemblyFinder Auto() + { + return new CompositeAssemblyFinder(new DependencyContextAssemblyFinder(DependencyContext.Default), new DllScanningAssemblyFinder()); + } - public static AssemblyFinder ForSource(ConfigurationAssemblySource configurationAssemblySource) + public static AssemblyFinder ForSource(ConfigurationAssemblySource configurationAssemblySource) + { + return configurationAssemblySource switch { - return configurationAssemblySource switch - { - ConfigurationAssemblySource.UseLoadedAssemblies => Auto(), - ConfigurationAssemblySource.AlwaysScanDllFiles => new DllScanningAssemblyFinder(), - _ => throw new ArgumentOutOfRangeException(nameof(configurationAssemblySource), configurationAssemblySource, null), - }; - } + ConfigurationAssemblySource.UseLoadedAssemblies => Auto(), + ConfigurationAssemblySource.AlwaysScanDllFiles => new DllScanningAssemblyFinder(), + _ => throw new ArgumentOutOfRangeException(nameof(configurationAssemblySource), configurationAssemblySource, null), + }; + } - public static AssemblyFinder ForDependencyContext(DependencyContext dependencyContext) - { - return new DependencyContextAssemblyFinder(dependencyContext); - } + public static AssemblyFinder ForDependencyContext(DependencyContext dependencyContext) + { + return new DependencyContextAssemblyFinder(dependencyContext); } } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/CompositeAssemblyFinder.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/CompositeAssemblyFinder.cs new file mode 100644 index 00000000..9f3c9ef2 --- /dev/null +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/CompositeAssemblyFinder.cs @@ -0,0 +1,23 @@ +using System.Reflection; + +namespace Serilog.Settings.Configuration.Assemblies; + +class CompositeAssemblyFinder : AssemblyFinder +{ + readonly AssemblyFinder[] _assemblyFinders; + + public CompositeAssemblyFinder(params AssemblyFinder[] assemblyFinders) + { + _assemblyFinders = assemblyFinders; + } + + public override IReadOnlyList FindAssembliesContainingName(string nameToFind) + { + var assemblyNames = new List(); + foreach (var assemblyFinder in _assemblyFinders) + { + assemblyNames.AddRange(assemblyFinder.FindAssembliesContainingName(nameToFind)); + } + return assemblyNames; + } +} diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/ConfigurationAssemblySource.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/ConfigurationAssemblySource.cs index de5800cf..b6a60657 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/ConfigurationAssemblySource.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/ConfigurationAssemblySource.cs @@ -12,21 +12,20 @@ // See the License for the specific language governing permissions and // limitations under the License. -namespace Serilog.Settings.Configuration +namespace Serilog.Settings.Configuration; + +/// +/// Defines how the package will identify the assemblies which are scanned for sinks and other Type information. +/// +public enum ConfigurationAssemblySource { /// - /// Defines how the package will identify the assemblies which are scanned for sinks and other Type information. + /// Try to scan the assemblies already in memory. This is the default. If GetEntryAssembly is null, fallback to DLL scanning. /// - public enum ConfigurationAssemblySource - { - /// - /// Try to scan the assemblies already in memory. This is the default. If GetEntryAssembly is null, fallback to DLL scanning. - /// - UseLoadedAssemblies, + UseLoadedAssemblies, - /// - /// Scan for assemblies in DLLs from the working directory. This is the fallback when GetEntryAssembly is null. - /// - AlwaysScanDllFiles - } + /// + /// Scan for assemblies in DLLs from the working directory. This is the fallback when GetEntryAssembly is null. + /// + AlwaysScanDllFiles } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DependencyContextAssemblyFinder.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DependencyContextAssemblyFinder.cs index 7a6feaa6..2ed07ce9 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DependencyContextAssemblyFinder.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DependencyContextAssemblyFinder.cs @@ -1,37 +1,36 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; +using System.Reflection; using Microsoft.Extensions.DependencyModel; -namespace Serilog.Settings.Configuration.Assemblies +namespace Serilog.Settings.Configuration.Assemblies; + +sealed class DependencyContextAssemblyFinder : AssemblyFinder { - sealed class DependencyContextAssemblyFinder : AssemblyFinder + readonly DependencyContext? _dependencyContext; + + public DependencyContextAssemblyFinder(DependencyContext? dependencyContext) { - readonly DependencyContext _dependencyContext; + _dependencyContext = dependencyContext; + } - public DependencyContextAssemblyFinder(DependencyContext dependencyContext) - { - _dependencyContext = dependencyContext ?? throw new ArgumentNullException(nameof(dependencyContext)); - } + public override IReadOnlyList FindAssembliesContainingName(string nameToFind) + { + if (_dependencyContext == null) + return Array.Empty(); - public override IReadOnlyList FindAssembliesContainingName(string nameToFind) - { - var query = from library in _dependencyContext.RuntimeLibraries - where IsReferencingSerilog(library) - from assemblyName in library.GetDefaultAssemblyNames(_dependencyContext) - where IsCaseInsensitiveMatch(assemblyName.Name, nameToFind) - select assemblyName; + var query = from library in _dependencyContext.RuntimeLibraries + where IsReferencingSerilog(library) + from assemblyName in library.GetDefaultAssemblyNames(_dependencyContext) + where IsCaseInsensitiveMatch(assemblyName.Name, nameToFind) + select assemblyName; - return query.ToList().AsReadOnly(); - - static bool IsReferencingSerilog(Library library) - { - const string Serilog = "serilog"; - return library.Dependencies.Any(dependency => - dependency.Name.StartsWith(Serilog, StringComparison.OrdinalIgnoreCase) && - (dependency.Name.Length == Serilog.Length || dependency.Name[Serilog.Length] == '.')); - } + return query.ToList(); + + static bool IsReferencingSerilog(Library library) + { + const string Serilog = "serilog"; + return library.Dependencies.Any(dependency => + dependency.Name.StartsWith(Serilog, StringComparison.OrdinalIgnoreCase) && + (dependency.Name.Length == Serilog.Length || dependency.Name[Serilog.Length] == '.')); } } } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DllScanningAssemblyFinder.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DllScanningAssemblyFinder.cs index 9cc9139f..c38c88bf 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DllScanningAssemblyFinder.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DllScanningAssemblyFinder.cs @@ -1,65 +1,64 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; using System.Reflection; -namespace Serilog.Settings.Configuration.Assemblies +namespace Serilog.Settings.Configuration.Assemblies; + +sealed class DllScanningAssemblyFinder : AssemblyFinder { - sealed class DllScanningAssemblyFinder : AssemblyFinder + public override IReadOnlyList FindAssembliesContainingName(string nameToFind) { - public override IReadOnlyList FindAssembliesContainingName(string nameToFind) + var probeDirs = new List(); + + if (!string.IsNullOrEmpty(AppDomain.CurrentDomain.BaseDirectory)) { - var probeDirs = new List(); - - if (!string.IsNullOrEmpty(AppDomain.CurrentDomain.BaseDirectory)) - { - probeDirs.Add(AppDomain.CurrentDomain.BaseDirectory); + probeDirs.Add(AppDomain.CurrentDomain.BaseDirectory); #if NETFRAMEWORK - var privateBinPath = AppDomain.CurrentDomain.SetupInformation.PrivateBinPath; - if (!string.IsNullOrEmpty(privateBinPath)) + var privateBinPath = AppDomain.CurrentDomain.SetupInformation.PrivateBinPath; + if (!string.IsNullOrEmpty(privateBinPath)) + { + foreach (var path in privateBinPath.Split(';')) { - foreach (var path in privateBinPath.Split(';')) + if (Path.IsPathRooted(path)) { - if (Path.IsPathRooted(path)) - { - probeDirs.Add(path); - } - else - { - probeDirs.Add(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path)); - } + probeDirs.Add(path); + } + else + { + probeDirs.Add(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path)); } } -#endif } - else +#endif + } + else + { + var assemblyLocation = Path.GetDirectoryName(typeof(AssemblyFinder).Assembly.Location); + if (assemblyLocation != null) { - probeDirs.Add(Path.GetDirectoryName(typeof(AssemblyFinder).Assembly.Location)); + probeDirs.Add(assemblyLocation); } + } - var query = from probeDir in probeDirs - where Directory.Exists(probeDir) - from outputAssemblyPath in Directory.GetFiles(probeDir, "*.dll") - let assemblyFileName = Path.GetFileNameWithoutExtension(outputAssemblyPath) - where IsCaseInsensitiveMatch(assemblyFileName, nameToFind) - let assemblyName = TryGetAssemblyNameFrom(outputAssemblyPath) - where assemblyName != null - select assemblyName; + var query = from probeDir in probeDirs + where Directory.Exists(probeDir) + from outputAssemblyPath in Directory.GetFiles(probeDir, "*.dll") + let assemblyFileName = Path.GetFileNameWithoutExtension(outputAssemblyPath) + where IsCaseInsensitiveMatch(assemblyFileName, nameToFind) + let assemblyName = TryGetAssemblyNameFrom(outputAssemblyPath) + where assemblyName != null + select assemblyName; - return query.ToList().AsReadOnly(); + return query.ToList(); - static AssemblyName TryGetAssemblyNameFrom(string path) + static AssemblyName? TryGetAssemblyNameFrom(string path) + { + try { - try - { - return AssemblyName.GetAssemblyName(path); - } - catch (BadImageFormatException) - { - return null; - } + return AssemblyName.GetAssemblyName(path); + } + catch (BadImageFormatException) + { + return null; } } } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs index 642d0bfd..2a6c3170 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs @@ -1,7 +1,5 @@ -using System; -using System.Collections.Generic; -using System.Linq; using System.Reflection; +using System.Runtime.CompilerServices; using System.Text.RegularExpressions; using Microsoft.Extensions.Configuration; @@ -13,528 +11,589 @@ using Serilog.Events; using Serilog.Settings.Configuration.Assemblies; -namespace Serilog.Settings.Configuration +namespace Serilog.Settings.Configuration; + +class ConfigurationReader : IConfigurationReader { - class ConfigurationReader : IConfigurationReader - { - const string LevelSwitchNameRegex = @"^\${0,1}[A-Za-z]+[A-Za-z0-9]*$"; + const string LevelSwitchNameRegex = @"^\${0,1}[A-Za-z]+[A-Za-z0-9]*$"; - readonly IConfigurationSection _section; - readonly IReadOnlyCollection _configurationAssemblies; - readonly ResolutionContext _resolutionContext; + readonly IConfiguration _section; + readonly IReadOnlyCollection _configurationAssemblies; + readonly ResolutionContext _resolutionContext; + readonly IConfigurationRoot? _configurationRoot; - public ConfigurationReader(IConfigurationSection configSection, AssemblyFinder assemblyFinder, IConfiguration configuration = null) - { - _section = configSection ?? throw new ArgumentNullException(nameof(configSection)); - _configurationAssemblies = LoadConfigurationAssemblies(_section, assemblyFinder); - _resolutionContext = new ResolutionContext(configuration); - } + public ConfigurationReader(IConfiguration configSection, AssemblyFinder assemblyFinder, ConfigurationReaderOptions readerOptions, IConfiguration? configuration = null) + { + _section = configSection ?? throw new ArgumentNullException(nameof(configSection)); + _configurationAssemblies = LoadConfigurationAssemblies(_section, assemblyFinder); + _resolutionContext = new ResolutionContext(configuration, readerOptions); + _configurationRoot = configuration as IConfigurationRoot; + } - // Used internally for processing nested configuration sections -- see GetMethodCalls below. - internal ConfigurationReader(IConfigurationSection configSection, IReadOnlyCollection configurationAssemblies, ResolutionContext resolutionContext) - { - _section = configSection ?? throw new ArgumentNullException(nameof(configSection)); - _configurationAssemblies = configurationAssemblies ?? throw new ArgumentNullException(nameof(configurationAssemblies)); - _resolutionContext = resolutionContext ?? throw new ArgumentNullException(nameof(resolutionContext)); - } + // Used internally for processing nested configuration sections -- see GetMethodCalls below. + internal ConfigurationReader(IConfiguration configSection, IReadOnlyCollection configurationAssemblies, ResolutionContext resolutionContext) + { + _section = configSection ?? throw new ArgumentNullException(nameof(configSection)); + _configurationAssemblies = configurationAssemblies ?? throw new ArgumentNullException(nameof(configurationAssemblies)); + _resolutionContext = resolutionContext ?? throw new ArgumentNullException(nameof(resolutionContext)); + _configurationRoot = resolutionContext.HasAppConfiguration ? resolutionContext.AppConfiguration as IConfigurationRoot : null; + } - public void Configure(LoggerConfiguration loggerConfiguration) - { - ProcessLevelSwitchDeclarations(); - ProcessFilterSwitchDeclarations(); - - ApplyMinimumLevel(loggerConfiguration); - ApplyEnrichment(loggerConfiguration); - ApplyFilters(loggerConfiguration); - ApplyDestructuring(loggerConfiguration); - ApplySinks(loggerConfiguration); - ApplyAuditSinks(loggerConfiguration); - } + public void Configure(LoggerConfiguration loggerConfiguration) + { + ProcessLevelSwitchDeclarations(); + ProcessFilterSwitchDeclarations(); + + ApplyMinimumLevel(loggerConfiguration); + ApplyEnrichment(loggerConfiguration); + ApplyFilters(loggerConfiguration); + ApplyDestructuring(loggerConfiguration); + ApplySinks(loggerConfiguration); + ApplyAuditSinks(loggerConfiguration); + } - void ProcessFilterSwitchDeclarations() + void ProcessFilterSwitchDeclarations() + { + var filterSwitchesDirective = _section.GetSection("FilterSwitches"); + + foreach (var filterSwitchDeclaration in filterSwitchesDirective.GetChildren()) { - var filterSwitchesDirective = _section.GetSection("FilterSwitches"); + var filterSwitch = LoggingFilterSwitchProxy.Create(); + if (filterSwitch == null) + { + SelfLog.WriteLine($"FilterSwitches section found, but neither Serilog.Expressions nor Serilog.Filters.Expressions is referenced."); + break; + } - foreach (var filterSwitchDeclaration in filterSwitchesDirective.GetChildren()) + var switchName = filterSwitchDeclaration.Key; + // switchName must be something like $switch to avoid ambiguities + if (!IsValidSwitchName(switchName)) { - var filterSwitch = LoggingFilterSwitchProxy.Create(); - if (filterSwitch == null) - { - SelfLog.WriteLine($"FilterSwitches section found, but neither Serilog.Expressions nor Serilog.Filters.Expressions is referenced."); - break; - } + throw new FormatException($"\"{switchName}\" is not a valid name for a Filter Switch declaration. The first character of the name must be a letter or '$' sign, like \"FilterSwitches\" : {{\"$switchName\" : \"{{FilterExpression}}\"}}"); + } - var switchName = filterSwitchDeclaration.Key; - // switchName must be something like $switch to avoid ambiguities - if (!IsValidSwitchName(switchName)) - { - throw new FormatException($"\"{switchName}\" is not a valid name for a Filter Switch declaration. The first character of the name must be a letter or '$' sign, like \"FilterSwitches\" : {{\"$switchName\" : \"{{FilterExpression}}\"}}"); - } + SetFilterSwitch(throwOnError: true); + SubscribeToFilterExpressionChanges(); - SetFilterSwitch(throwOnError: true); - SubscribeToFilterExpressionChanges(); + _resolutionContext.AddFilterSwitch(switchName, filterSwitch); + _resolutionContext.ReaderOptions.OnFilterSwitchCreated?.Invoke(switchName, filterSwitch); - _resolutionContext.AddFilterSwitch(switchName, filterSwitch); + void SubscribeToFilterExpressionChanges() + { + ChangeToken.OnChange(filterSwitchDeclaration.GetReloadToken, () => SetFilterSwitch(throwOnError: false)); + } - void SubscribeToFilterExpressionChanges() + void SetFilterSwitch(bool throwOnError) + { + var filterExpr = filterSwitchDeclaration.Value; + if (string.IsNullOrWhiteSpace(filterExpr)) { - ChangeToken.OnChange(filterSwitchDeclaration.GetReloadToken, () => SetFilterSwitch(throwOnError: false)); + filterSwitch.Expression = null; + return; } - void SetFilterSwitch(bool throwOnError) + try { - var filterExpr = filterSwitchDeclaration.Value; - if (string.IsNullOrWhiteSpace(filterExpr)) - { - filterSwitch.Expression = null; - return; - } - - try + filterSwitch.Expression = filterExpr; + } + catch (Exception e) + { + var errMsg = $"The expression '{filterExpr}' is invalid filter expression: {e.Message}."; + if (throwOnError) { - filterSwitch.Expression = filterExpr; + throw new InvalidOperationException(errMsg, e); } - catch (Exception e) - { - var errMsg = $"The expression '{filterExpr}' is invalid filter expression: {e.Message}."; - if (throwOnError) - { - throw new InvalidOperationException(errMsg, e); - } - SelfLog.WriteLine(errMsg); - } + SelfLog.WriteLine(errMsg); } } } + } - void ProcessLevelSwitchDeclarations() + void ProcessLevelSwitchDeclarations() + { + var levelSwitchesDirective = _section.GetSection("LevelSwitches"); + foreach (var levelSwitchDeclaration in levelSwitchesDirective.GetChildren()) { - var levelSwitchesDirective = _section.GetSection("LevelSwitches"); - foreach (var levelSwitchDeclaration in levelSwitchesDirective.GetChildren()) + var switchName = levelSwitchDeclaration.Key; + var switchInitialLevel = levelSwitchDeclaration.Value; + // switchName must be something like $switch to avoid ambiguities + if (!IsValidSwitchName(switchName)) { - var switchName = levelSwitchDeclaration.Key; - var switchInitialLevel = levelSwitchDeclaration.Value; - // switchName must be something like $switch to avoid ambiguities - if (!IsValidSwitchName(switchName)) - { - throw new FormatException($"\"{switchName}\" is not a valid name for a Level Switch declaration. The first character of the name must be a letter or '$' sign, like \"LevelSwitches\" : {{\"$switchName\" : \"InitialLevel\"}}"); - } + throw new FormatException($"\"{switchName}\" is not a valid name for a Level Switch declaration. The first character of the name must be a letter or '$' sign, like \"LevelSwitches\" : {{\"$switchName\" : \"InitialLevel\"}}"); + } - LoggingLevelSwitch newSwitch; - if (string.IsNullOrEmpty(switchInitialLevel)) - { - newSwitch = new LoggingLevelSwitch(); - } - else - { - var initialLevel = ParseLogEventLevel(switchInitialLevel); - newSwitch = new LoggingLevelSwitch(initialLevel); - } + LoggingLevelSwitch newSwitch; + if (string.IsNullOrEmpty(switchInitialLevel)) + { + newSwitch = new LoggingLevelSwitch(); + } + else + { + var initialLevel = ParseLogEventLevel(switchInitialLevel!); + newSwitch = new LoggingLevelSwitch(initialLevel); + } - SubscribeToLoggingLevelChanges(levelSwitchDeclaration, newSwitch); + SubscribeToLoggingLevelChanges(levelSwitchDeclaration, newSwitch); - // make them available later on when resolving argument values - _resolutionContext.AddLevelSwitch(switchName, newSwitch); - } + // make them available later on when resolving argument values + var referenceName = _resolutionContext.AddLevelSwitch(switchName, newSwitch); + _resolutionContext.ReaderOptions.OnLevelSwitchCreated?.Invoke(referenceName, newSwitch); } + } + + void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration) + { + var minimumLevelDirective = _section.GetSection("MinimumLevel"); - void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration) + IConfigurationSection? defaultMinLevelDirective = GetDefaultMinLevelDirective(); + if (defaultMinLevelDirective?.Value != null) { - var minimumLevelDirective = _section.GetSection("MinimumLevel"); + ApplyMinimumLevelConfiguration(defaultMinLevelDirective, (configuration, levelSwitch) => configuration.ControlledBy(levelSwitch)); + } - var defaultMinLevelDirective = minimumLevelDirective.Value != null ? minimumLevelDirective : minimumLevelDirective.GetSection("Default"); - if (defaultMinLevelDirective.Value != null) + var minLevelControlledByDirective = minimumLevelDirective.GetSection("ControlledBy"); + if (minLevelControlledByDirective.Value != null) + { + var globalMinimumLevelSwitch = _resolutionContext.LookUpLevelSwitchByName(minLevelControlledByDirective.Value); + // not calling ApplyMinimumLevel local function because here we have a reference to a LogLevelSwitch already + loggerConfiguration.MinimumLevel.ControlledBy(globalMinimumLevelSwitch); + } + + foreach (var overrideDirective in minimumLevelDirective.GetSection("Override").GetChildren()) + { + var overridePrefix = overrideDirective.Key; + var overridenLevelOrSwitch = overrideDirective.Value; + if (Enum.TryParse(overridenLevelOrSwitch, out LogEventLevel _)) { - ApplyMinimumLevelConfiguration(defaultMinLevelDirective, (configuration, levelSwitch) => configuration.ControlledBy(levelSwitch)); + ApplyMinimumLevelConfiguration(overrideDirective, (configuration, levelSwitch) => + { + configuration.Override(overridePrefix, levelSwitch); + _resolutionContext.ReaderOptions.OnLevelSwitchCreated?.Invoke(overridePrefix, levelSwitch); + }); } - - var minLevelControlledByDirective = minimumLevelDirective.GetSection("ControlledBy"); - if (minLevelControlledByDirective.Value != null) + else if (!string.IsNullOrEmpty(overridenLevelOrSwitch)) { - var globalMinimumLevelSwitch = _resolutionContext.LookUpLevelSwitchByName(minLevelControlledByDirective.Value); + var overrideSwitch = _resolutionContext.LookUpLevelSwitchByName(overridenLevelOrSwitch!); // not calling ApplyMinimumLevel local function because here we have a reference to a LogLevelSwitch already - loggerConfiguration.MinimumLevel.ControlledBy(globalMinimumLevelSwitch); + loggerConfiguration.MinimumLevel.Override(overridePrefix, overrideSwitch); } + } + + void ApplyMinimumLevelConfiguration(IConfigurationSection directive, Action applyConfigAction) + { + var minimumLevel = ParseLogEventLevel(directive.Value!); + + var levelSwitch = new LoggingLevelSwitch(minimumLevel); + applyConfigAction(loggerConfiguration.MinimumLevel, levelSwitch); + + SubscribeToLoggingLevelChanges(directive, levelSwitch); + } - foreach (var overrideDirective in minimumLevelDirective.GetSection("Override").GetChildren()) + IConfigurationSection? GetDefaultMinLevelDirective() + { + var defaultLevelDirective = minimumLevelDirective.GetSection("Default"); + if (_configurationRoot != null && minimumLevelDirective.Value != null && defaultLevelDirective.Value != null) { - var overridePrefix = overrideDirective.Key; - var overridenLevelOrSwitch = overrideDirective.Value; - if (Enum.TryParse(overridenLevelOrSwitch, out LogEventLevel _)) + foreach (var provider in _configurationRoot.Providers.Reverse()) { - ApplyMinimumLevelConfiguration(overrideDirective, (configuration, levelSwitch) => configuration.Override(overridePrefix, levelSwitch)); - } - else - { - var overrideSwitch = _resolutionContext.LookUpLevelSwitchByName(overridenLevelOrSwitch); - // not calling ApplyMinimumLevel local function because here we have a reference to a LogLevelSwitch already - loggerConfiguration.MinimumLevel.Override(overridePrefix, overrideSwitch); + if (provider.TryGet(minimumLevelDirective.Path, out _)) + { + return _configurationRoot.GetSection(minimumLevelDirective.Path); + } + + if (provider.TryGet(defaultLevelDirective.Path, out _)) + { + return _configurationRoot.GetSection(defaultLevelDirective.Path); + } } + + return null; } - void ApplyMinimumLevelConfiguration(IConfigurationSection directive, Action applyConfigAction) - { - var minimumLevel = ParseLogEventLevel(directive.Value); + return minimumLevelDirective.Value != null ? minimumLevelDirective : minimumLevelDirective.GetSection("Default"); + } + } - var levelSwitch = new LoggingLevelSwitch(minimumLevel); - applyConfigAction(loggerConfiguration.MinimumLevel, levelSwitch); + void SubscribeToLoggingLevelChanges(IConfigurationSection levelSection, LoggingLevelSwitch levelSwitch) + { + ChangeToken.OnChange( + levelSection.GetReloadToken, + () => + { + if (Enum.TryParse(levelSection.Value, out LogEventLevel minimumLevel)) + levelSwitch.MinimumLevel = minimumLevel; + else + SelfLog.WriteLine($"The value {levelSection.Value} is not a valid Serilog level."); + }); + } - SubscribeToLoggingLevelChanges(directive, levelSwitch); - } + void ApplyFilters(LoggerConfiguration loggerConfiguration) + { + var filterDirective = _section.GetSection("Filter"); + if (filterDirective.GetChildren().Any()) + { + var methodCalls = GetMethodCalls(filterDirective); + CallConfigurationMethods(methodCalls, FindFilterConfigurationMethods(_configurationAssemblies, _resolutionContext.ReaderOptions.AllowInternalTypes, _resolutionContext.ReaderOptions.AllowInternalMethods), loggerConfiguration.Filter); } + } - void SubscribeToLoggingLevelChanges(IConfigurationSection levelSection, LoggingLevelSwitch levelSwitch) + void ApplyDestructuring(LoggerConfiguration loggerConfiguration) + { + var destructureDirective = _section.GetSection("Destructure"); + if (destructureDirective.GetChildren().Any()) { - ChangeToken.OnChange( - levelSection.GetReloadToken, - () => - { - if (Enum.TryParse(levelSection.Value, out LogEventLevel minimumLevel)) - levelSwitch.MinimumLevel = minimumLevel; - else - SelfLog.WriteLine($"The value {levelSection.Value} is not a valid Serilog level."); - }); + var methodCalls = GetMethodCalls(destructureDirective); + CallConfigurationMethods(methodCalls, FindDestructureConfigurationMethods(_configurationAssemblies, _resolutionContext.ReaderOptions.AllowInternalTypes, _resolutionContext.ReaderOptions.AllowInternalMethods), loggerConfiguration.Destructure); } + } - void ApplyFilters(LoggerConfiguration loggerConfiguration) + void ApplySinks(LoggerConfiguration loggerConfiguration) + { + var writeToDirective = _section.GetSection("WriteTo"); + if (writeToDirective.GetChildren().Any()) { - var filterDirective = _section.GetSection("Filter"); - if (filterDirective.GetChildren().Any()) - { - var methodCalls = GetMethodCalls(filterDirective); - CallConfigurationMethods(methodCalls, FindFilterConfigurationMethods(_configurationAssemblies), loggerConfiguration.Filter); - } + var methodCalls = GetMethodCalls(writeToDirective); + CallConfigurationMethods(methodCalls, FindSinkConfigurationMethods(_configurationAssemblies, _resolutionContext.ReaderOptions.AllowInternalTypes, _resolutionContext.ReaderOptions.AllowInternalMethods), loggerConfiguration.WriteTo); } + } - void ApplyDestructuring(LoggerConfiguration loggerConfiguration) + void ApplyAuditSinks(LoggerConfiguration loggerConfiguration) + { + var auditToDirective = _section.GetSection("AuditTo"); + if (auditToDirective.GetChildren().Any()) { - var destructureDirective = _section.GetSection("Destructure"); - if (destructureDirective.GetChildren().Any()) - { - var methodCalls = GetMethodCalls(destructureDirective); - CallConfigurationMethods(methodCalls, FindDestructureConfigurationMethods(_configurationAssemblies), loggerConfiguration.Destructure); - } + var methodCalls = GetMethodCalls(auditToDirective); + CallConfigurationMethods(methodCalls, FindAuditSinkConfigurationMethods(_configurationAssemblies, _resolutionContext.ReaderOptions.AllowInternalTypes, _resolutionContext.ReaderOptions.AllowInternalMethods), loggerConfiguration.AuditTo); } + } - void ApplySinks(LoggerConfiguration loggerConfiguration) + void IConfigurationReader.ApplySinks(LoggerSinkConfiguration loggerSinkConfiguration) + { + var methodCalls = GetMethodCalls(_section); + CallConfigurationMethods(methodCalls, FindSinkConfigurationMethods(_configurationAssemblies, _resolutionContext.ReaderOptions.AllowInternalTypes, _resolutionContext.ReaderOptions.AllowInternalMethods), loggerSinkConfiguration); + } + + void IConfigurationReader.ApplyEnrichment(LoggerEnrichmentConfiguration loggerEnrichmentConfiguration) + { + var methodCalls = GetMethodCalls(_section); + CallConfigurationMethods(methodCalls, FindEventEnricherConfigurationMethods(_configurationAssemblies, _resolutionContext.ReaderOptions.AllowInternalTypes, _resolutionContext.ReaderOptions.AllowInternalMethods), loggerEnrichmentConfiguration); + } + + void ApplyEnrichment(LoggerConfiguration loggerConfiguration) + { + var enrichDirective = _section.GetSection("Enrich"); + if (enrichDirective.GetChildren().Any()) { - var writeToDirective = _section.GetSection("WriteTo"); - if (writeToDirective.GetChildren().Any()) - { - var methodCalls = GetMethodCalls(writeToDirective); - CallConfigurationMethods(methodCalls, FindSinkConfigurationMethods(_configurationAssemblies), loggerConfiguration.WriteTo); - } + var methodCalls = GetMethodCalls(enrichDirective); + CallConfigurationMethods(methodCalls, FindEventEnricherConfigurationMethods(_configurationAssemblies, _resolutionContext.ReaderOptions.AllowInternalTypes, _resolutionContext.ReaderOptions.AllowInternalMethods), loggerConfiguration.Enrich); } - void ApplyAuditSinks(LoggerConfiguration loggerConfiguration) + var propertiesDirective = _section.GetSection("Properties"); + if (propertiesDirective.GetChildren().Any()) { - var auditToDirective = _section.GetSection("AuditTo"); - if (auditToDirective.GetChildren().Any()) + foreach (var enrichPropertyDirective in propertiesDirective.GetChildren()) { - var methodCalls = GetMethodCalls(auditToDirective); - CallConfigurationMethods(methodCalls, FindAuditSinkConfigurationMethods(_configurationAssemblies), loggerConfiguration.AuditTo); + // Null is an acceptable value here; annotations on Serilog need updating. + loggerConfiguration.Enrich.WithProperty(enrichPropertyDirective.Key, enrichPropertyDirective.Value!); } } + } - void IConfigurationReader.ApplySinks(LoggerSinkConfiguration loggerSinkConfiguration) + 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() }) + .Concat( + (from child in children + where child.Value == null + let name = GetSectionName(child) + let callArgs = (from argument in child.GetSection("Args").GetChildren() + select new + { + Name = argument.Key, + Value = GetArgumentValue(argument, _configurationAssemblies) + }).ToDictionary(p => p.Name, p => p.Value) + select new { Name = name, Args = callArgs })) + .ToLookup(p => p.Name, p => p.Args); + + return result; + + static string GetSectionName(IConfigurationSection s) { - var methodCalls = GetMethodCalls(_section); - CallConfigurationMethods(methodCalls, FindSinkConfigurationMethods(_configurationAssemblies), loggerSinkConfiguration); + var name = s.GetSection("Name"); + if (name.Value == null) + throw new InvalidOperationException($"The configuration value in {name.Path} has no 'Name' element."); + + return name.Value; } + } - void IConfigurationReader.ApplyEnrichment(LoggerEnrichmentConfiguration loggerEnrichmentConfiguration) + 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) { - var methodCalls = GetMethodCalls(_section); - CallConfigurationMethods(methodCalls, FindEventEnricherConfigurationMethods(_configurationAssemblies), loggerEnrichmentConfiguration); + argumentValue = new StringArgumentValue(argumentSection.Value); } - - void ApplyEnrichment(LoggerConfiguration loggerConfiguration) + else { - var enrichDirective = _section.GetSection("Enrich"); - if (enrichDirective.GetChildren().Any()) - { - var methodCalls = GetMethodCalls(enrichDirective); - CallConfigurationMethods(methodCalls, FindEventEnricherConfigurationMethods(_configurationAssemblies), loggerConfiguration.Enrich); - } - - var propertiesDirective = _section.GetSection("Properties"); - if (propertiesDirective.GetChildren().Any()) - { - foreach (var enrichPropertyDirective in propertiesDirective.GetChildren()) - { - loggerConfiguration.Enrich.WithProperty(enrichPropertyDirective.Key, enrichPropertyDirective.Value); - } - } + argumentValue = new ObjectArgumentValue(argumentSection, configurationAssemblies); } - internal ILookup> GetMethodCalls(IConfigurationSection directive) + return argumentValue; + } + + static IReadOnlyCollection LoadConfigurationAssemblies(IConfiguration section, AssemblyFinder assemblyFinder) + { + var serilogAssembly = typeof(ILogger).Assembly; + var assemblies = new HashSet { serilogAssembly }; + + var usingSection = section.GetSection("Using"); + if (usingSection.GetChildren().Any()) { - 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() }) - .Concat( - (from child in children - where child.Value == null - let name = GetSectionName(child) - let callArgs = (from argument in child.GetSection("Args").GetChildren() - select new - { - Name = argument.Key, - Value = GetArgumentValue(argument, _configurationAssemblies) - }).ToDictionary(p => p.Name, p => p.Value) - select new { Name = name, Args = callArgs })) - .ToLookup(p => p.Name, p => p.Args); - - return result; - - static string GetSectionName(IConfigurationSection s) + foreach (var simpleName in usingSection.GetChildren().Select(c => c.Value)) { - var name = s.GetSection("Name"); - if (name.Value == null) - throw new InvalidOperationException($"The configuration value in {name.Path} has no 'Name' element."); + if (string.IsNullOrWhiteSpace(simpleName)) + throw new InvalidOperationException( + $"A zero-length or whitespace assembly name was supplied to a {usingSection.Path} configuration statement."); - return name.Value; + var assembly = Assembly.Load(new AssemblyName(simpleName)); + assemblies.Add(assembly); } } - internal static IConfigurationArgumentValue GetArgumentValue(IConfigurationSection argumentSection, IReadOnlyCollection configurationAssemblies) + foreach (var assemblyName in assemblyFinder.FindAssembliesContainingName("serilog")) { - 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; + var assumed = Assembly.Load(assemblyName); + assemblies.Add(assumed); } - static IReadOnlyCollection LoadConfigurationAssemblies(IConfigurationSection section, AssemblyFinder assemblyFinder) + if (assemblies.Count == 1) { - var serilogAssembly = typeof(ILogger).Assembly; - var assemblies = new Dictionary { [serilogAssembly.FullName] = serilogAssembly }; + var message = $""" + No {usingSection.Path} configuration section is defined and no Serilog assemblies were found. + This is most likely because the application is published as single-file. + Either add a {usingSection.Path} section or explicitly specify assemblies that contains sinks and other types through the reader options. For example: + var options = new ConfigurationReaderOptions(typeof(ConsoleLoggerConfigurationExtensions).Assembly, typeof(SerilogExpression).Assembly); + new LoggerConfiguration().ReadFrom.Configuration(configuration, options); + """; + throw new InvalidOperationException(message); + } - var usingSection = section.GetSection("Using"); - if (usingSection.GetChildren().Any()) - { - foreach (var simpleName in usingSection.GetChildren().Select(c => c.Value)) - { - if (string.IsNullOrWhiteSpace(simpleName)) - throw new InvalidOperationException( - "A zero-length or whitespace assembly name was supplied to a Serilog.Using configuration statement."); + return assemblies; + } - var assembly = Assembly.Load(new AssemblyName(simpleName)); - if (!assemblies.ContainsKey(assembly.FullName)) - assemblies.Add(assembly.FullName, assembly); - } - } + void CallConfigurationMethods(ILookup> methods, IReadOnlyCollection configurationMethods, object receiver) + { + foreach (var method in methods.SelectMany(g => g.Select(x => new { g.Key, Value = x }))) + { + var methodInfo = SelectConfigurationMethod(configurationMethods, method.Key, method.Value.Keys.ToList()); - foreach (var assemblyName in assemblyFinder.FindAssembliesContainingName("serilog")) + if (methodInfo != null) { - var assumed = Assembly.Load(assemblyName); - if (assumed != null && !assemblies.ContainsKey(assumed.FullName)) - assemblies.Add(assumed.FullName, assumed); + var call = (from p in methodInfo.GetParameters().Skip(1) + let directive = method.Value.FirstOrDefault(s => ParameterNameMatches(p.Name, s.Key)) + select directive.Key == null + ? GetImplicitValueForNotSpecifiedKey(p, methodInfo) + : directive.Value.ConvertTo(p.ParameterType, _resolutionContext)).ToList(); + + call.Insert(0, receiver); + methodInfo.Invoke(null, call.ToArray()); } - - return assemblies.Values.ToList().AsReadOnly(); } + } - void CallConfigurationMethods(ILookup> methods, IReadOnlyCollection configurationMethods, object receiver) - { - foreach (var method in methods.SelectMany(g => g.Select(x => new { g.Key, Value = x }))) - { - var methodInfo = SelectConfigurationMethod(configurationMethods, method.Key, method.Value.Keys.ToList()); - - if (methodInfo != null) - { - var call = (from p in methodInfo.GetParameters().Skip(1) - let directive = method.Value.FirstOrDefault(s => ParameterNameMatches(p.Name, s.Key)) - select directive.Key == null - ? GetImplicitValueForNotSpecifiedKey(p, methodInfo) - : directive.Value.ConvertTo(p.ParameterType, _resolutionContext)).ToList(); - - call.Insert(0, receiver); - methodInfo.Invoke(null, call.ToArray()); - } - } - } + static bool HasImplicitValueWhenNotSpecified(ParameterInfo paramInfo) + { + return paramInfo.HasDefaultValue + // parameters of type IConfiguration are implicitly populated with provided Configuration + || paramInfo.ParameterType == typeof(IConfiguration); + } - static bool HasImplicitValueWhenNotSpecified(ParameterInfo paramInfo) + object? GetImplicitValueForNotSpecifiedKey(ParameterInfo parameter, MethodInfo methodToInvoke) + { + if (!HasImplicitValueWhenNotSpecified(parameter)) { - return paramInfo.HasDefaultValue - // parameters of type IConfiguration are implicitly populated with provided Configuration - || paramInfo.ParameterType == typeof(IConfiguration); + throw new InvalidOperationException("GetImplicitValueForNotSpecifiedKey() should only be called for parameters for which HasImplicitValueWhenNotSpecified() is true. " + + "This means something is wrong in the Serilog.Settings.Configuration code."); } - object GetImplicitValueForNotSpecifiedKey(ParameterInfo parameter, MethodInfo methodToInvoke) + if (parameter.ParameterType == typeof(IConfiguration)) { - if (!HasImplicitValueWhenNotSpecified(parameter)) + if (_resolutionContext.HasAppConfiguration) { - throw new InvalidOperationException("GetImplicitValueForNotSpecifiedKey() should only be called for parameters for which HasImplicitValueWhenNotSpecified() is true. " + - "This means something is wrong in the Serilog.Settings.Configuration code."); + return _resolutionContext.AppConfiguration; } - - if (parameter.ParameterType == typeof(IConfiguration)) + if (parameter.HasDefaultValue) { - if (_resolutionContext.HasAppConfiguration) - { - return _resolutionContext.AppConfiguration; - } - if (parameter.HasDefaultValue) - { - return parameter.DefaultValue; - } - - throw new InvalidOperationException("Trying to invoke a configuration method accepting a `IConfiguration` argument. " + - $"This is not supported when only a `IConfigSection` has been provided. (method '{methodToInvoke}')"); + return parameter.DefaultValue; } - return parameter.DefaultValue; + throw new InvalidOperationException("Trying to invoke a configuration method accepting a `IConfiguration` argument. " + + $"This is not supported when only a `IConfigSection` has been provided. (method '{methodToInvoke}')"); } - internal static MethodInfo SelectConfigurationMethod(IReadOnlyCollection candidateMethods, string name, IReadOnlyCollection suppliedArgumentNames) + return parameter.DefaultValue; + } + + internal static MethodInfo? SelectConfigurationMethod(IReadOnlyCollection candidateMethods, string name, IReadOnlyCollection suppliedArgumentNames) + { + // Per issue #111, it is safe to use case-insensitive matching on argument names. The CLR doesn't permit this type + // of overloading, and the Microsoft.Extensions.Configuration keys are case-insensitive (case is preserved with some + // config sources, but key-matching is case-insensitive and case-preservation does not appear to be guaranteed). + var selectedMethod = candidateMethods + .Where(m => m.Name == name) + .Where(m => m.GetParameters() + .Skip(1) + .All(p => HasImplicitValueWhenNotSpecified(p) || + ParameterNameMatches(p.Name, suppliedArgumentNames))) + .OrderByDescending(m => + { + var matchingArgs = m.GetParameters().Where(p => ParameterNameMatches(p.Name, suppliedArgumentNames)).ToList(); + + // Prefer the configuration method with most number of matching arguments and of those the ones with + // the most string type parameters to predict best match with least type casting + return new Tuple( + matchingArgs.Count, + matchingArgs.Count(p => p.ParameterType == typeof(string))); + }) + .FirstOrDefault(); + + if (selectedMethod == null) { - // Per issue #111, it is safe to use case-insensitive matching on argument names. The CLR doesn't permit this type - // of overloading, and the Microsoft.Extensions.Configuration keys are case-insensitive (case is preserved with some - // config sources, but key-matching is case-insensitive and case-preservation does not appear to be guaranteed). - var selectedMethod = candidateMethods + var methodsByName = candidateMethods .Where(m => m.Name == name) - .Where(m => m.GetParameters() - .Skip(1) - .All(p => HasImplicitValueWhenNotSpecified(p) || - ParameterNameMatches(p.Name, suppliedArgumentNames))) - .OrderByDescending(m => - { - var matchingArgs = m.GetParameters().Where(p => ParameterNameMatches(p.Name, suppliedArgumentNames)).ToList(); - - // Prefer the configuration method with most number of matching arguments and of those the ones with - // the most string type parameters to predict best match with least type casting - return new Tuple( - matchingArgs.Count, - matchingArgs.Count(p => p.ParameterType == typeof(string))); - }) - .FirstOrDefault(); + .Select(m => $"{m.Name}({string.Join(", ", m.GetParameters().Skip(1).Select(p => p.Name))})") + .ToList(); - if (selectedMethod == null) + if (!methodsByName.Any()) { - var methodsByName = candidateMethods - .Where(m => m.Name == name) - .Select(m => $"{m.Name}({string.Join(", ", m.GetParameters().Skip(1).Select(p => p.Name))})") - .ToList(); - - if (!methodsByName.Any()) + if (candidateMethods.Any()) + { SelfLog.WriteLine($"Unable to find a method called {name}. Candidate methods are:{Environment.NewLine}{string.Join(Environment.NewLine, candidateMethods)}"); + } else - SelfLog.WriteLine($"Unable to find a method called {name} " - + (suppliedArgumentNames.Any() - ? "for supplied arguments: " + string.Join(", ", suppliedArgumentNames) - : "with no supplied arguments") - + ". Candidate methods are:" - + Environment.NewLine - + string.Join(Environment.NewLine, methodsByName)); + { + SelfLog.WriteLine($"Unable to find a method called {name}. No candidates found."); + } + } + else + { + SelfLog.WriteLine($"Unable to find a method called {name} " + + (suppliedArgumentNames.Any() + ? "for supplied arguments: " + string.Join(", ", suppliedArgumentNames) + : "with no supplied arguments") + + ". Candidate methods are:" + + Environment.NewLine + + string.Join(Environment.NewLine, methodsByName)); } - - return selectedMethod; } + return selectedMethod; + } - static bool ParameterNameMatches(string actualParameterName, string suppliedName) - { - return suppliedName.Equals(actualParameterName, StringComparison.OrdinalIgnoreCase); - } + static bool ParameterNameMatches(string? actualParameterName, string suppliedName) + { + return suppliedName.Equals(actualParameterName, StringComparison.OrdinalIgnoreCase); + } - static bool ParameterNameMatches(string actualParameterName, IEnumerable suppliedNames) - { - return suppliedNames.Any(s => ParameterNameMatches(actualParameterName, s)); - } + static bool ParameterNameMatches(string? actualParameterName, IEnumerable suppliedNames) + { + return suppliedNames.Any(s => ParameterNameMatches(actualParameterName, s)); + } - static IReadOnlyCollection FindSinkConfigurationMethods(IReadOnlyCollection configurationAssemblies) - { - var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerSinkConfiguration)); - if (configurationAssemblies.Contains(typeof(LoggerSinkConfiguration).GetTypeInfo().Assembly)) - found.AddRange(SurrogateConfigurationMethods.WriteTo); + static IReadOnlyCollection FindSinkConfigurationMethods(IReadOnlyCollection configurationAssemblies, bool allowInternalTypes, bool allowInternalMethods) + { + var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerSinkConfiguration), allowInternalTypes, allowInternalMethods); + if (configurationAssemblies.Contains(typeof(LoggerSinkConfiguration).GetTypeInfo().Assembly)) + found.AddRange(SurrogateConfigurationMethods.WriteTo); - return found; - } + return found; + } - static IReadOnlyCollection FindAuditSinkConfigurationMethods(IReadOnlyCollection configurationAssemblies) - { - var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerAuditSinkConfiguration)); - if (configurationAssemblies.Contains(typeof(LoggerAuditSinkConfiguration).GetTypeInfo().Assembly)) - found.AddRange(SurrogateConfigurationMethods.AuditTo); - return found; - } + static IReadOnlyCollection FindAuditSinkConfigurationMethods(IReadOnlyCollection configurationAssemblies, bool allowInternalTypes, bool allowInternalMethods) + { + var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerAuditSinkConfiguration), allowInternalTypes, allowInternalMethods); + if (configurationAssemblies.Contains(typeof(LoggerAuditSinkConfiguration).GetTypeInfo().Assembly)) + found.AddRange(SurrogateConfigurationMethods.AuditTo); + return found; + } - static IReadOnlyCollection FindFilterConfigurationMethods(IReadOnlyCollection configurationAssemblies) - { - var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerFilterConfiguration)); - if (configurationAssemblies.Contains(typeof(LoggerFilterConfiguration).GetTypeInfo().Assembly)) - found.AddRange(SurrogateConfigurationMethods.Filter); + static IReadOnlyCollection FindFilterConfigurationMethods(IReadOnlyCollection configurationAssemblies, bool allowInternalTypes, bool allowInternalMethods) + { + var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerFilterConfiguration), allowInternalTypes, allowInternalMethods); + if (configurationAssemblies.Contains(typeof(LoggerFilterConfiguration).GetTypeInfo().Assembly)) + found.AddRange(SurrogateConfigurationMethods.Filter); - return found; - } + return found; + } - static IReadOnlyCollection FindDestructureConfigurationMethods(IReadOnlyCollection configurationAssemblies) - { - var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerDestructuringConfiguration)); - if (configurationAssemblies.Contains(typeof(LoggerDestructuringConfiguration).GetTypeInfo().Assembly)) - found.AddRange(SurrogateConfigurationMethods.Destructure); + static IReadOnlyCollection FindDestructureConfigurationMethods(IReadOnlyCollection configurationAssemblies, bool allowInternalTypes, bool allowInternalMethods) + { + var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerDestructuringConfiguration), allowInternalTypes, allowInternalMethods); + if (configurationAssemblies.Contains(typeof(LoggerDestructuringConfiguration).GetTypeInfo().Assembly)) + found.AddRange(SurrogateConfigurationMethods.Destructure); - return found; - } + return found; + } - static IReadOnlyCollection FindEventEnricherConfigurationMethods(IReadOnlyCollection configurationAssemblies) - { - var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerEnrichmentConfiguration)); - if (configurationAssemblies.Contains(typeof(LoggerEnrichmentConfiguration).GetTypeInfo().Assembly)) - found.AddRange(SurrogateConfigurationMethods.Enrich); + static IReadOnlyCollection FindEventEnricherConfigurationMethods(IReadOnlyCollection configurationAssemblies, bool allowInternalTypes, bool allowInternalMethods) + { + var found = FindConfigurationExtensionMethods(configurationAssemblies, typeof(LoggerEnrichmentConfiguration), allowInternalTypes, allowInternalMethods); + if (configurationAssemblies.Contains(typeof(LoggerEnrichmentConfiguration).GetTypeInfo().Assembly)) + found.AddRange(SurrogateConfigurationMethods.Enrich); - return found; - } + return found; + } - static List FindConfigurationExtensionMethods(IReadOnlyCollection configurationAssemblies, Type configType) + static List FindConfigurationExtensionMethods(IReadOnlyCollection configurationAssemblies, Type configType, bool allowInternalTypes, bool allowInternalMethods) + { + // ExtensionAttribute can be polyfilled to support extension methods + static bool HasCustomExtensionAttribute(MethodInfo m) { - // ExtensionAttribute can be polyfilled to support extension methods - bool HasExtensionAttribute(MethodInfo m) => - m.CustomAttributes.Any(a => a.AttributeType.FullName == "System.Runtime.CompilerServices.ExtensionAttribute"); - - return configurationAssemblies - .SelectMany(a => a.ExportedTypes - .Select(t => t.GetTypeInfo()) - .Where(t => t.IsSealed && t.IsAbstract && !t.IsNested)) - .SelectMany(t => t.DeclaredMethods) - .Where(m => m.IsStatic && m.IsPublic && HasExtensionAttribute(m)) - .Where(m => m.GetParameters()[0].ParameterType == configType) - .ToList(); + try + { + return m.CustomAttributes.Any(a => a.AttributeType.FullName == "System.Runtime.CompilerServices.ExtensionAttribute"); + } + catch (CustomAttributeFormatException) + { + return false; + } } - internal static bool IsValidSwitchName(string input) - { - return Regex.IsMatch(input, LevelSwitchNameRegex); - } + return configurationAssemblies + .SelectMany(a => (allowInternalTypes ? a.GetTypes() : a.ExportedTypes) + .Select(t => t.GetTypeInfo()) + .Where(t => t.IsSealed && t.IsAbstract && !t.IsNested && (t.IsPublic || allowInternalTypes && !t.IsVisible))) + .SelectMany(t => t.DeclaredMethods) + .Where(m => m.IsStatic && (m.IsPublic || allowInternalMethods && m.IsAssembly) && (m.IsDefined(typeof(ExtensionAttribute), false) || HasCustomExtensionAttribute(m))) + .Where(m => m.GetParameters()[0].ParameterType == configType) + .ToList(); + } - static LogEventLevel ParseLogEventLevel(string value) - { - if (!Enum.TryParse(value, out LogEventLevel parsedLevel)) - throw new InvalidOperationException($"The value {value} is not a valid Serilog level."); - return parsedLevel; - } + internal static bool IsValidSwitchName(string input) + { + return Regex.IsMatch(input, LevelSwitchNameRegex); + } + static LogEventLevel ParseLogEventLevel(string value) + { + if (!Enum.TryParse(value, ignoreCase: true, out LogEventLevel parsedLevel)) + throw new InvalidOperationException($"The value {value} is not a valid Serilog level."); + return parsedLevel; } } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReaderOptions.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReaderOptions.cs new file mode 100644 index 00000000..4510aa9f --- /dev/null +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReaderOptions.cs @@ -0,0 +1,88 @@ +using System.Globalization; +using System.Reflection; +using Microsoft.Extensions.DependencyModel; +using Serilog.Core; + +namespace Serilog.Settings.Configuration; + +/// +/// Options to adjust how the configuration object is processed. +/// +public sealed class ConfigurationReaderOptions +{ + /// + /// Initialize a new instance of the class. + /// + /// A collection of assemblies that contains sinks and other types. + /// The argument is null. + /// The argument is empty. + public ConfigurationReaderOptions(params Assembly[] assemblies) + { + Assemblies = assemblies ?? throw new ArgumentNullException(nameof(assemblies)); + if (assemblies.Length == 0) + throw new ArgumentException("The assemblies array must not be empty.", nameof(assemblies)); + } + + /// + /// Initialize a new instance of the class. + /// + /// Prefer the constructor taking explicit assemblies: . It's the only one supporting single-file publishing. + public ConfigurationReaderOptions() : this(dependencyContext: null) + { + } + + /// + /// Initialize a new instance of the class. + /// + /// + /// The dependency context from which sink/enricher packages can be located. If , the platform default will be used. + /// + /// Prefer the constructor taking explicit assemblies: . It's the only one supporting single-file publishing. + public ConfigurationReaderOptions(DependencyContext? dependencyContext) => DependencyContext = dependencyContext; + + /// + /// Initialize a new instance of the class. + /// + /// Defines how the package identifies assemblies to scan for sinks and other types. + /// Prefer the constructor taking explicit assemblies: . It's the only one supporting single-file publishing. + public ConfigurationReaderOptions(ConfigurationAssemblySource configurationAssemblySource) => ConfigurationAssemblySource = configurationAssemblySource; + + /// + /// The section name for section which contains a Serilog section. Defaults to Serilog. + /// + public string? SectionName { get; init; } = ConfigurationLoggerConfigurationExtensions.DefaultSectionName; + + /// + /// The used when converting strings to other object types. Defaults to the invariant culture. + /// + public IFormatProvider? FormatProvider { get; init; } = CultureInfo.InvariantCulture; + + /// + /// Allows to use internal types for extension methods for sink configuration. Defaults to . + /// + public bool AllowInternalTypes { get; init; } + + /// + /// Allows to use internal extension methods for sink configuration. Defaults to . + /// + public bool AllowInternalMethods { get; init; } + + /// + /// Called when a log level switch is created while reading the configuration. + /// Log level switches are created either from the Serilog:LevelSwitches section (declared switches) or the Serilog:MinimumLevel:Override section (minimum level override switches). + /// + /// For declared switches, the switch name includes the leading $ character. + /// For minimum level override switches, the switch name is the (partial) namespace or type name of the override. + /// + /// + public Action? OnLevelSwitchCreated { get; init; } + + /// + /// Called when a log filter switch is created while reading the Serilog:FilterSwitches section of the configuration. + /// + public Action? OnFilterSwitchCreated { get; init; } + + internal Assembly[]? Assemblies { get; } + internal DependencyContext? DependencyContext { get; } + internal ConfigurationAssemblySource? ConfigurationAssemblySource { get; } +} diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationArgumentValue.cs index e24e8e02..8a1e86fd 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationArgumentValue.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationArgumentValue.cs @@ -1,9 +1,6 @@ -using System; +namespace Serilog.Settings.Configuration; -namespace Serilog.Settings.Configuration +interface IConfigurationArgumentValue { - interface IConfigurationArgumentValue - { - object ConvertTo(Type toType, ResolutionContext resolutionContext); - } + object? ConvertTo(Type toType, ResolutionContext resolutionContext); } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationReader.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationReader.cs index af815af2..f3dd36ff 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationReader.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationReader.cs @@ -1,10 +1,9 @@ using Serilog.Configuration; -namespace Serilog.Settings.Configuration +namespace Serilog.Settings.Configuration; + +interface IConfigurationReader : ILoggerSettings { - interface IConfigurationReader : ILoggerSettings - { - void ApplySinks(LoggerSinkConfiguration loggerSinkConfiguration); - void ApplyEnrichment(LoggerEnrichmentConfiguration loggerEnrichmentConfiguration); - } + void ApplySinks(LoggerSinkConfiguration loggerSinkConfiguration); + void ApplyEnrichment(LoggerEnrichmentConfiguration loggerEnrichmentConfiguration); } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ILoggingFilterSwitch.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ILoggingFilterSwitch.cs new file mode 100644 index 00000000..321295bb --- /dev/null +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ILoggingFilterSwitch.cs @@ -0,0 +1,16 @@ +namespace Serilog.Settings.Configuration; + +/// +/// A log event filter that can be modified at runtime. +/// +/// +/// Under the hood, the logging filter switch is either a Serilog.Expressions.LoggingFilterSwitch or a Serilog.Filters.Expressions.LoggingFilterSwitch instance. +/// +public interface ILoggingFilterSwitch +{ + /// + /// A filter expression against which log events will be tested. + /// Only expressions that evaluate to true are included by the filter. A null expression will accept all events. + /// + public string? Expression { get; set; } +} diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/LoggingFilterSwitchProxy.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/LoggingFilterSwitchProxy.cs index 676a2dd8..8da70d07 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/LoggingFilterSwitchProxy.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/LoggingFilterSwitchProxy.cs @@ -1,50 +1,48 @@ -using System; +namespace Serilog.Settings.Configuration; -namespace Serilog.Settings.Configuration +class LoggingFilterSwitchProxy : ILoggingFilterSwitch { - class LoggingFilterSwitchProxy + readonly Action _setProxy; + readonly Func _getProxy; + + LoggingFilterSwitchProxy(object realSwitch) { - readonly Action _setProxy; - readonly Func _getProxy; + RealSwitch = realSwitch ?? throw new ArgumentNullException(nameof(realSwitch)); - LoggingFilterSwitchProxy(object realSwitch) - { - RealSwitch = realSwitch ?? throw new ArgumentNullException(nameof(realSwitch)); + var type = realSwitch.GetType(); + var expressionProperty = type.GetProperty("Expression") ?? throw new MissingMemberException(type.FullName, "Expression"); + + _setProxy = (Action)Delegate.CreateDelegate( + typeof(Action), + realSwitch, + expressionProperty.GetSetMethod() ?? throw new MissingMethodException(type.FullName, "set_Expression")); - var type = realSwitch.GetType(); - var expressionProperty = type.GetProperty("Expression") ?? throw new MissingMemberException(type.FullName, "Expression"); + _getProxy = (Func)Delegate.CreateDelegate( + typeof(Func), + realSwitch, + expressionProperty.GetGetMethod() ?? throw new MissingMethodException(type.FullName, "get_Expression")); + } - _setProxy = (Action)Delegate.CreateDelegate( - typeof(Action), - realSwitch, - expressionProperty.GetSetMethod()); + public object RealSwitch { get; } - _getProxy = (Func)Delegate.CreateDelegate( - typeof(Func), - realSwitch, - expressionProperty.GetGetMethod()); - } + public string? Expression + { + get => _getProxy(); + set => _setProxy(value); + } - public object RealSwitch { get; } + public static LoggingFilterSwitchProxy? Create(string? expression = null) + { + var filterSwitchType = + Type.GetType("Serilog.Expressions.LoggingFilterSwitch, Serilog.Expressions") ?? + Type.GetType("Serilog.Filters.Expressions.LoggingFilterSwitch, Serilog.Filters.Expressions"); - public string Expression + if (filterSwitchType is null) { - get => _getProxy(); - set => _setProxy(value); + return null; } - public static LoggingFilterSwitchProxy Create(string expression = null) - { - var filterSwitchType = - Type.GetType("Serilog.Expressions.LoggingFilterSwitch, Serilog.Expressions") ?? - Type.GetType("Serilog.Filters.Expressions.LoggingFilterSwitch, Serilog.Filters.Expressions"); - - if (filterSwitchType is null) - { - return null; - } - - return new LoggingFilterSwitchProxy(Activator.CreateInstance(filterSwitchType, expression)); - } + var realSwitch = Activator.CreateInstance(filterSwitchType, expression) ?? throw new InvalidOperationException($"Activator.CreateInstance returned null for {filterSwitchType}"); + return new LoggingFilterSwitchProxy(realSwitch); } } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs index d22f78b1..151ce5b5 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs @@ -1,6 +1,4 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Diagnostics.CodeAnalysis; using System.Linq.Expressions; using System.Reflection; @@ -8,234 +6,233 @@ using Serilog.Configuration; -namespace Serilog.Settings.Configuration +namespace Serilog.Settings.Configuration; + +class ObjectArgumentValue : IConfigurationArgumentValue { - class ObjectArgumentValue : IConfigurationArgumentValue + readonly IConfigurationSection _section; + readonly IReadOnlyCollection _configurationAssemblies; + + public ObjectArgumentValue(IConfigurationSection section, IReadOnlyCollection configurationAssemblies) { - readonly IConfigurationSection _section; - readonly IReadOnlyCollection _configurationAssemblies; + _section = section ?? throw new ArgumentNullException(nameof(section)); - public ObjectArgumentValue(IConfigurationSection section, IReadOnlyCollection configurationAssemblies) - { - _section = section ?? throw new ArgumentNullException(nameof(section)); + // used by nested logger configurations to feed a new pass by ConfigurationReader + _configurationAssemblies = configurationAssemblies ?? throw new ArgumentNullException(nameof(configurationAssemblies)); + } - // used by nested logger configurations to feed a new pass by ConfigurationReader - _configurationAssemblies = configurationAssemblies ?? throw new ArgumentNullException(nameof(configurationAssemblies)); - } + public object? ConvertTo(Type toType, ResolutionContext resolutionContext) + { + // return the entire section for internal processing + if (toType == typeof(IConfigurationSection)) return _section; - public object ConvertTo(Type toType, ResolutionContext resolutionContext) + // process a nested configuration to populate an Action<> logger/sink config parameter? + var typeInfo = toType.GetTypeInfo(); + if (typeInfo.IsGenericType && + typeInfo.GetGenericTypeDefinition() is Type genericType && genericType == typeof(Action<>)) { - // return the entire section for internal processing - if (toType == typeof(IConfigurationSection)) return _section; + var configType = typeInfo.GenericTypeArguments[0]; + IConfigurationReader configReader = new ConfigurationReader(_section, _configurationAssemblies, resolutionContext); - // process a nested configuration to populate an Action<> logger/sink config parameter? - var typeInfo = toType.GetTypeInfo(); - if (typeInfo.IsGenericType && - typeInfo.GetGenericTypeDefinition() is Type genericType && genericType == typeof(Action<>)) + return configType switch { - var configType = typeInfo.GenericTypeArguments[0]; - IConfigurationReader configReader = new ConfigurationReader(_section, _configurationAssemblies, resolutionContext); - - return configType switch - { - _ when configType == typeof(LoggerConfiguration) => 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.") - }; - } + _ when configType == typeof(LoggerConfiguration) => 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.") + }; + } - if (toType.IsArray) - return CreateArray(); + if (toType.IsArray) + return CreateArray(); - if (IsContainer(toType, out var elementType) && TryCreateContainer(out var container)) - return container; + 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(); - } + if (TryBuildCtorExpression(_section, toType, resolutionContext, out var ctorExpression)) + { + return Expression.Lambda>(ctorExpression).Compile().Invoke(); + } - // MS Config binding can work with a limited set of primitive types and collections - return _section.Get(toType); + // MS Config binding can work with a limited set of primitive types and collections + return _section.Get(toType); - object CreateArray() + 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) { - var arrayElementType = toType.GetElementType()!; - var configurationElements = _section.GetChildren().ToArray(); - var array = Array.CreateInstance(arrayElementType, configurationElements.Length); - for (int i = 0; i < configurationElements.Length; ++i) - { - var argumentValue = ConfigurationReader.GetArgumentValue(configurationElements[i], _configurationAssemblies); - var value = argumentValue.ConvertTo(arrayElementType, resolutionContext); - array.SetValue(value, i); - } - - return array; + var argumentValue = ConfigurationReader.GetArgumentValue(configurationElements[i], _configurationAssemblies); + var value = argumentValue.ConvertTo(arrayElementType, resolutionContext); + array.SetValue(value, i); } - bool TryCreateContainer(out object result) - { - result = null; + return array; + } - if (toType.GetConstructor(Type.EmptyTypes) == null) - return false; + bool TryCreateContainer([NotNullWhen(true)] out object? result) + { + result = null; - // 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 (toType.GetConstructor(Type.EmptyTypes) == null) + return false; - var configurationElements = _section.GetChildren().ToArray(); - result = Activator.CreateInstance(toType); + // 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; - for (int i = 0; i < configurationElements.Length; ++i) - { - var argumentValue = ConfigurationReader.GetArgumentValue(configurationElements[i], _configurationAssemblies); - var value = argumentValue.ConvertTo(elementType, resolutionContext); - addMethod.Invoke(result, new[] { value }); - } + var configurationElements = _section.GetChildren().ToArray(); + result = Activator.CreateInstance(toType) ?? throw new InvalidOperationException($"Activator.CreateInstance returned null for {toType}"); - return true; + for (int i = 0; i < configurationElements.Length; ++i) + { + var argumentValue = ConfigurationReader.GetArgumentValue(configurationElements[i], _configurationAssemblies); + var value = argumentValue.ConvertTo(elementType, resolutionContext); + addMethod.Invoke(result, new[] { value }); } + + return true; } + } - internal static bool TryBuildCtorExpression( - IConfigurationSection section, Type parameterType, ResolutionContext resolutionContext, out NewExpression ctorExpression) - { - ctorExpression = null; + internal static bool TryBuildCtorExpression( + IConfigurationSection section, Type parameterType, ResolutionContext resolutionContext, [NotNullWhen(true)] out NewExpression? ctorExpression) + { + ctorExpression = null; - var typeDirective = section.GetValue("$type") switch + var typeDirective = section.GetValue("$type") switch + { + not null => "$type", + null => section.GetValue("type") switch { - not null => "$type", - null => section.GetValue("type") switch - { - not null => "type", - null => null, - }, - }; + not null => "type", + null => null, + }, + }; - var type = typeDirective switch - { - not null => Type.GetType(section.GetValue(typeDirective), throwOnError: false), - null => parameterType, - }; + var type = typeDirective switch + { + not null => Type.GetType(section.GetValue(typeDirective)!, throwOnError: false), + null => parameterType, + }; - if (type is null or { IsAbstract: true }) - { - return false; - } + if (type is null or { IsAbstract: true }) + { + return false; + } - var suppliedArguments = section.GetChildren().Where(s => s.Key != typeDirective) - .ToDictionary(s => s.Key, StringComparer.OrdinalIgnoreCase); + var suppliedArguments = section.GetChildren().Where(s => s.Key != typeDirective) + .ToDictionary(s => s.Key, StringComparer.OrdinalIgnoreCase); - if (suppliedArguments.Count == 0 && - type.GetConstructor(Type.EmptyTypes) is ConstructorInfo parameterlessCtor) - { - ctorExpression = Expression.New(parameterlessCtor); - return true; - } + if (suppliedArguments.Count == 0 && + type.GetConstructor(Type.EmptyTypes) is ConstructorInfo parameterlessCtor) + { + ctorExpression = Expression.New(parameterlessCtor); + return true; + } - var ctor = - (from c in type.GetConstructors() - from p in c.GetParameters() - let argumentBindResult = suppliedArguments.TryGetValue(p.Name, out var argValue) switch - { - true => new { success = true, hasMatch = true, value = (object)argValue }, - false => p.HasDefaultValue switch - { - true => new { success = true, hasMatch = false, value = p.DefaultValue }, - false => new { success = false, hasMatch = false, value = (object)null }, - }, - } - group new { argumentBindResult, p.ParameterType } by c into gr - where gr.All(z => z.argumentBindResult.success) - let matchedArgs = gr.Where(z => z.argumentBindResult.hasMatch).ToList() - orderby matchedArgs.Count descending, - matchedArgs.Count(p => p.ParameterType == typeof(string)) descending - select new + 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 { - ConstructorInfo = gr.Key, - ArgumentValues = gr.Select(z => new { Value = z.argumentBindResult.value, Type = z.ParameterType }) - .ToList() - }).FirstOrDefault(); + 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) + { + return false; + } - if (ctor is null) + var ctorArguments = new List(); + foreach (var argumentValue in ctor.ArgumentValues) + { + if (TryBindToCtorArgument(argumentValue.Value, argumentValue.Type, resolutionContext, out var argumentExpression)) { - return false; + ctorArguments.Add(argumentExpression); } - - var ctorArguments = new List(); - foreach (var argumentValue in ctor.ArgumentValues) + else { - if (TryBindToCtorArgument(argumentValue.Value, argumentValue.Type, resolutionContext, out var argumentExpression)) - { - ctorArguments.Add(argumentExpression); - } - else - { - return false; - } + return false; } + } - ctorExpression = Expression.New(ctor.ConstructorInfo, ctorArguments); - return true; + ctorExpression = Expression.New(ctor.ConstructorInfo, ctorArguments); + return true; - static bool TryBindToCtorArgument(object value, Type type, ResolutionContext resolutionContext, out Expression argumentExpression) - { - argumentExpression = null; + static bool TryBindToCtorArgument(object value, Type type, ResolutionContext resolutionContext, [NotNullWhen(true)] out Expression? argumentExpression) + { + argumentExpression = null; - if (value is IConfigurationSection s) + if (value is IConfigurationSection s) + { + if (s.Value is string argValue) { - if (s.Value is string argValue) + var stringArgumentValue = new StringArgumentValue(argValue); + try { - var stringArgumentValue = new StringArgumentValue(argValue); - try - { - argumentExpression = Expression.Constant( - stringArgumentValue.ConvertTo(type, resolutionContext), - type); - - return true; - } - catch (Exception) - { - return false; - } + argumentExpression = Expression.Constant( + stringArgumentValue.ConvertTo(type, resolutionContext), + type); + + return true; } - else if (s.GetChildren().Any()) + catch (Exception) { - if (TryBuildCtorExpression(s, type, resolutionContext, out var ctorExpression)) - { - argumentExpression = ctorExpression; - return true; - } - return false; } } + else if (s.GetChildren().Any()) + { + if (TryBuildCtorExpression(s, type, resolutionContext, out var ctorExpression)) + { + argumentExpression = ctorExpression; + return true; + } - argumentExpression = Expression.Constant(value, type); - return true; + return false; + } } + + argumentExpression = Expression.Constant(value, type); + return true; } + } - static bool IsContainer(Type type, out Type elementType) + static bool IsContainer(Type type, [NotNullWhen(true)] out Type? elementType) + { + elementType = null; + foreach (var iface in type.GetInterfaces()) { - elementType = null; - foreach (var iface in type.GetInterfaces()) + if (iface.IsGenericType) { - if (iface.IsGenericType) + if (iface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) { - if (iface.GetGenericTypeDefinition() == typeof(IEnumerable<>)) - { - elementType = iface.GetGenericArguments()[0]; - return true; - } + elementType = iface.GetGenericArguments()[0]; + return true; } } - - return false; } + + return false; } } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ResolutionContext.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ResolutionContext.cs index 2963a64e..61d40d86 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ResolutionContext.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ResolutionContext.cs @@ -1,85 +1,77 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Serilog.Core; -namespace Serilog.Settings.Configuration +namespace Serilog.Settings.Configuration; + +/// +/// Keeps track of available elements that are useful when resolving values in the settings system. +/// +sealed class ResolutionContext { + readonly IDictionary _declaredLevelSwitches; + readonly IDictionary _declaredFilterSwitches; + readonly IConfiguration? _appConfiguration; + + public ResolutionContext(IConfiguration? appConfiguration = null, ConfigurationReaderOptions? readerOptions = null) + { + _declaredLevelSwitches = new Dictionary(); + _declaredFilterSwitches = new Dictionary(); + _appConfiguration = appConfiguration; + ReaderOptions = readerOptions ?? new ConfigurationReaderOptions(); + } + + public ConfigurationReaderOptions ReaderOptions { get; } + /// - /// Keeps track of available elements that are useful when resolving values in the settings system. + /// Looks up a switch in the declared LoggingLevelSwitches /// - sealed class ResolutionContext + /// the name of a switch to look up + /// the LoggingLevelSwitch registered with the name + /// if no switch has been registered with + public LoggingLevelSwitch LookUpLevelSwitchByName(string switchName) { - readonly IDictionary _declaredLevelSwitches; - readonly IDictionary _declaredFilterSwitches; - readonly IConfiguration _appConfiguration; - - public ResolutionContext(IConfiguration appConfiguration = null) + if (_declaredLevelSwitches.TryGetValue(switchName, out var levelSwitch)) { - _declaredLevelSwitches = new Dictionary(); - _declaredFilterSwitches = new Dictionary(); - _appConfiguration = appConfiguration; + return levelSwitch; } - /// - /// Looks up a switch in the declared LoggingLevelSwitches - /// - /// the name of a switch to look up - /// the LoggingLevelSwitch registered with the name - /// if no switch has been registered with - public LoggingLevelSwitch LookUpLevelSwitchByName(string switchName) - { - if (_declaredLevelSwitches.TryGetValue(switchName, out var levelSwitch)) - { - return levelSwitch; - } - - throw new InvalidOperationException($"No LoggingLevelSwitch has been declared with name \"{switchName}\". You might be missing a section \"LevelSwitches\":{{\"{switchName}\":\"InitialLevel\"}}"); - } + throw new InvalidOperationException($"No LoggingLevelSwitch has been declared with name \"{switchName}\". You might be missing a section \"LevelSwitches\":{{\"{switchName}\":\"InitialLevel\"}}"); + } - public LoggingFilterSwitchProxy LookUpFilterSwitchByName(string switchName) + public LoggingFilterSwitchProxy LookUpFilterSwitchByName(string switchName) + { + if (_declaredFilterSwitches.TryGetValue(switchName, out var filterSwitch)) { - if (_declaredFilterSwitches.TryGetValue(switchName, out var filterSwitch)) - { - return filterSwitch; - } - - throw new InvalidOperationException($"No LoggingFilterSwitch has been declared with name \"{switchName}\". You might be missing a section \"FilterSwitches\":{{\"{switchName}\":\"{{FilterExpression}}\"}}"); + return filterSwitch; } - public bool HasAppConfiguration => _appConfiguration != null; + throw new InvalidOperationException($"No LoggingFilterSwitch has been declared with name \"{switchName}\". You might be missing a section \"FilterSwitches\":{{\"{switchName}\":\"{{FilterExpression}}\"}}"); + } - public IConfiguration AppConfiguration - { - get - { - if (!HasAppConfiguration) - { - throw new InvalidOperationException("AppConfiguration is not available"); - } + public bool HasAppConfiguration => _appConfiguration != null; - return _appConfiguration; - } - } + public IConfiguration AppConfiguration => _appConfiguration ?? throw new InvalidOperationException("AppConfiguration is not available"); - public void AddLevelSwitch(string levelSwitchName, LoggingLevelSwitch levelSwitch) - { - if (levelSwitchName == null) throw new ArgumentNullException(nameof(levelSwitchName)); - if (levelSwitch == null) throw new ArgumentNullException(nameof(levelSwitch)); - _declaredLevelSwitches[ToSwitchReference(levelSwitchName)] = levelSwitch; - } + public string AddLevelSwitch(string levelSwitchName, LoggingLevelSwitch levelSwitch) + { + if (levelSwitchName == null) throw new ArgumentNullException(nameof(levelSwitchName)); + if (levelSwitch == null) throw new ArgumentNullException(nameof(levelSwitch)); + var referenceName = ToSwitchReference(levelSwitchName); + _declaredLevelSwitches[referenceName] = levelSwitch; + return referenceName; + } - public void AddFilterSwitch(string filterSwitchName, LoggingFilterSwitchProxy filterSwitch) - { - if (filterSwitchName == null) throw new ArgumentNullException(nameof(filterSwitchName)); - if (filterSwitch == null) throw new ArgumentNullException(nameof(filterSwitch)); - _declaredFilterSwitches[ToSwitchReference(filterSwitchName)] = filterSwitch; - } + public void AddFilterSwitch(string filterSwitchName, LoggingFilterSwitchProxy filterSwitch) + { + if (filterSwitchName == null) throw new ArgumentNullException(nameof(filterSwitchName)); + if (filterSwitch == null) throw new ArgumentNullException(nameof(filterSwitch)); + var referenceName = ToSwitchReference(filterSwitchName); + _declaredFilterSwitches[referenceName] = filterSwitch; + } - string ToSwitchReference(string switchName) - { - return switchName.StartsWith("$") ? switchName : $"${switchName}"; - } + string ToSwitchReference(string switchName) + { + return switchName.StartsWith("$") ? switchName : $"${switchName}"; } } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs index 69dceb3f..ebf3d2ea 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs @@ -1,165 +1,197 @@ -using System; -using System.Collections.Generic; -using System.Linq; +using System.Diagnostics.CodeAnalysis; using System.Reflection; using System.Text.RegularExpressions; using Serilog.Core; -namespace Serilog.Settings.Configuration +namespace Serilog.Settings.Configuration; + +class StringArgumentValue : IConfigurationArgumentValue { - class StringArgumentValue : IConfigurationArgumentValue - { - readonly string _providedValue; + readonly string _providedValue; - static readonly Regex StaticMemberAccessorRegex = new Regex("^(?[^:]+)::(?[A-Za-z][A-Za-z0-9]*)(?[^:]*)$"); + static readonly Regex StaticMemberAccessorRegex = new Regex("^(?[^:]+)::(?[A-Za-z][A-Za-z0-9]*)(?[^:]*)$"); - public StringArgumentValue(string providedValue) + public StringArgumentValue(string providedValue) + { + _providedValue = providedValue ?? throw new ArgumentNullException(nameof(providedValue)); + } + + static readonly Dictionary> ExtendedTypeConversions = new Dictionary> { - _providedValue = providedValue ?? throw new ArgumentNullException(nameof(providedValue)); - } + { typeof(Uri), s => new Uri(s) }, + { typeof(TimeSpan), s => TimeSpan.Parse(s) }, + { typeof(Type), s => Type.GetType(s, throwOnError:true)! }, + }; - static readonly Dictionary> ExtendedTypeConversions = new Dictionary> - { - { typeof(Uri), s => new Uri(s) }, - { typeof(TimeSpan), s => TimeSpan.Parse(s) }, - { typeof(Type), s => Type.GetType(s, throwOnError:true) }, - }; + public object? ConvertTo(Type toType, ResolutionContext resolutionContext) + { + var argumentValue = Environment.ExpandEnvironmentVariables(_providedValue); - public object ConvertTo(Type toType, ResolutionContext resolutionContext) + if (toType == typeof(LoggingLevelSwitch)) { - var argumentValue = Environment.ExpandEnvironmentVariables(_providedValue); - - if (toType == typeof(LoggingLevelSwitch)) - { - return resolutionContext.LookUpLevelSwitchByName(argumentValue); - } + return resolutionContext.LookUpLevelSwitchByName(argumentValue); + } - if (toType.FullName == "Serilog.Expressions.LoggingFilterSwitch" || - toType.FullName == "Serilog.Filters.Expressions.LoggingFilterSwitch") - { - return resolutionContext.LookUpFilterSwitchByName(argumentValue).RealSwitch; - } + if (toType.FullName == "Serilog.Expressions.LoggingFilterSwitch" || + toType.FullName == "Serilog.Filters.Expressions.LoggingFilterSwitch") + { + return resolutionContext.LookUpFilterSwitchByName(argumentValue).RealSwitch; + } - var toTypeInfo = toType.GetTypeInfo(); - if (toTypeInfo.IsGenericType && toType.GetGenericTypeDefinition() == typeof(Nullable<>)) - { - if (string.IsNullOrEmpty(argumentValue)) - return null; + var toTypeInfo = toType.GetTypeInfo(); + if (toTypeInfo.IsGenericType && toType.GetGenericTypeDefinition() == typeof(Nullable<>)) + { + if (string.IsNullOrEmpty(argumentValue)) + return null; - // unwrap Nullable<> type since we're not handling null situations - toType = toTypeInfo.GenericTypeArguments[0]; - toTypeInfo = toType.GetTypeInfo(); - } + // unwrap Nullable<> type since we're not handling null situations + toType = toTypeInfo.GenericTypeArguments[0]; + toTypeInfo = toType.GetTypeInfo(); + } - if (toTypeInfo.IsEnum) - return Enum.Parse(toType, argumentValue); + if (toTypeInfo.IsEnum) + return Enum.Parse(toType, argumentValue); - var convertor = ExtendedTypeConversions - .Where(t => t.Key.GetTypeInfo().IsAssignableFrom(toTypeInfo)) - .Select(t => t.Value) - .FirstOrDefault(); + var convertor = ExtendedTypeConversions + .Where(t => t.Key.GetTypeInfo().IsAssignableFrom(toTypeInfo)) + .Select(t => t.Value) + .FirstOrDefault(); - if (convertor != null) - return convertor(argumentValue); + if (convertor != null) + return convertor(argumentValue); - if (!string.IsNullOrWhiteSpace(argumentValue)) + if (!string.IsNullOrWhiteSpace(argumentValue)) + { + // check if value looks like a static property or field directive + // like "Namespace.TypeName::StaticProperty, AssemblyName" + if (toType != typeof(string) && + TryParseStaticMemberAccessor(argumentValue, out var accessorTypeName, out var memberName)) { - // check if value looks like a static property or field directive - // like "Namespace.TypeName::StaticProperty, AssemblyName" - if (toType != typeof(string) && - TryParseStaticMemberAccessor(argumentValue, out var accessorTypeName, out var memberName)) + var accessorType = Type.GetType(accessorTypeName, throwOnError: true)!; + + // if delegate, look for a method and then construct a delegate + if (typeof(Delegate).IsAssignableFrom(toType) || typeof(MethodInfo) == toType) { - var accessorType = Type.GetType(accessorTypeName, throwOnError: true); - // is there a public static property with that name ? - var publicStaticPropertyInfo = accessorType.GetTypeInfo().DeclaredProperties + var methodCandidates = accessorType.GetTypeInfo().DeclaredMethods .Where(x => x.Name == memberName) - .Where(x => x.GetMethod != null) - .Where(x => x.GetMethod.IsPublic) - .FirstOrDefault(x => x.GetMethod.IsStatic); + .Where(x => x.IsPublic) + .Where(x => !x.IsGenericMethod) + .Where(x => x.IsStatic) + .ToList(); - if (publicStaticPropertyInfo != null) + if (methodCandidates.Count > 1) { - return publicStaticPropertyInfo.GetValue(null); // static property, no instance to pass + // filter possible method overloads + + var delegateSig = toType.GetMethod("Invoke"); + var delegateParameters = delegateSig!.GetParameters().Select(x => x.ParameterType); + methodCandidates = methodCandidates + .Where(x => x.ReturnType == delegateSig.ReturnType && x.GetParameters().Select(y => y.ParameterType).SequenceEqual(delegateParameters)) + .ToList(); } - // no property ? look for a public static field - var publicStaticFieldInfo = accessorType.GetTypeInfo().DeclaredFields - .Where(x => x.Name == memberName) - .Where(x => x.IsPublic) - .FirstOrDefault(x => x.IsStatic); + var methodCandidate = methodCandidates.SingleOrDefault(); - if (publicStaticFieldInfo != null) + if (methodCandidate != null) { - return publicStaticFieldInfo.GetValue(null); // static field, no instance to pass + if (typeof(MethodInfo) == toType) + { + return methodCandidate; + } + else + { + return methodCandidate.CreateDelegate(toType); + } } - - throw new InvalidOperationException($"Could not find a public static property or field with name `{memberName}` on type `{accessorTypeName}`"); } - if (toTypeInfo.IsInterface || toTypeInfo.IsAbstract) - { - // maybe it's the assembly-qualified type name of a concrete implementation - // with a default constructor - var type = FindType(argumentValue.Trim()); - if (type == null) - { - throw new InvalidOperationException($"Type {argumentValue} was not found."); - } + // is there a public static property with that name ? + var publicStaticPropertyInfo = accessorType.GetTypeInfo().DeclaredProperties + .Where(x => x.Name == memberName) + .FirstOrDefault(x => x.GetMethod != null && x.GetMethod.IsPublic && x.GetMethod.IsStatic); - var ctor = type.GetTypeInfo().DeclaredConstructors.Where(ci => !ci.IsStatic).FirstOrDefault(ci => - { - var parameters = ci.GetParameters(); - return parameters.Length == 0 || parameters.All(pi => pi.HasDefaultValue); - }); + if (publicStaticPropertyInfo != null) + { + return publicStaticPropertyInfo.GetValue(null); // static property, no instance to pass + } - if (ctor == null) - throw new InvalidOperationException($"A default constructor was not found on {type.FullName}."); + // no property ? look for a public static field + var publicStaticFieldInfo = accessorType.GetTypeInfo().DeclaredFields + .Where(x => x.Name == memberName) + .Where(x => x.IsPublic) + .FirstOrDefault(x => x.IsStatic); - var call = ctor.GetParameters().Select(pi => pi.DefaultValue).ToArray(); - return ctor.Invoke(call); + if (publicStaticFieldInfo != null) + { + return publicStaticFieldInfo.GetValue(null); // static field, no instance to pass } - } - return Convert.ChangeType(argumentValue, toType); - } + throw new InvalidOperationException($"Could not find a public static property or field with name `{memberName}` on type `{accessorTypeName}`"); + } - internal static Type FindType(string typeName) - { - var type = Type.GetType(typeName); - if (type == null) + if (toTypeInfo.IsInterface || toTypeInfo.IsAbstract) { - if (!typeName.Contains(',')) + // maybe it's the assembly-qualified type name of a concrete implementation + // with a default constructor + var type = FindType(argumentValue.Trim()); + if (type == null) { - type = Type.GetType($"{typeName}, Serilog"); + throw new InvalidOperationException($"Type {argumentValue} was not found."); } - } - return type; + var ctor = type.GetTypeInfo().DeclaredConstructors.Where(ci => !ci.IsStatic).FirstOrDefault(ci => + { + var parameters = ci.GetParameters(); + return parameters.Length == 0 || parameters.All(pi => pi.HasDefaultValue); + }); + + if (ctor == null) + throw new InvalidOperationException($"A default constructor was not found on {type.FullName}."); + + var call = ctor.GetParameters().Select(pi => pi.DefaultValue).ToArray(); + return ctor.Invoke(call); + } } - internal static bool TryParseStaticMemberAccessor(string input, out string accessorTypeName, out string memberName) + return Convert.ChangeType(argumentValue, toType, resolutionContext.ReaderOptions.FormatProvider); + } + + internal static Type? FindType(string typeName) + { + var type = Type.GetType(typeName); + if (type == null) { - if (input == null) + if (!typeName.Contains(',')) { - accessorTypeName = null; - memberName = null; - return false; - } - if (StaticMemberAccessorRegex.IsMatch(input)) - { - var match = StaticMemberAccessorRegex.Match(input); - var shortAccessorTypeName = match.Groups["shortTypeName"].Value; - var rawMemberName = match.Groups["memberName"].Value; - var extraQualifiers = match.Groups["typeNameExtraQualifiers"].Value; - - memberName = rawMemberName.Trim(); - accessorTypeName = shortAccessorTypeName.Trim() + extraQualifiers.TrimEnd(); - return true; + type = Type.GetType($"{typeName}, Serilog"); } + } + + return type; + } + + internal static bool TryParseStaticMemberAccessor(string? input, [NotNullWhen(true)] out string? accessorTypeName, [NotNullWhen(true)] out string? memberName) + { + if (input == null) + { accessorTypeName = null; memberName = null; return false; } + if (StaticMemberAccessorRegex.IsMatch(input)) + { + var match = StaticMemberAccessorRegex.Match(input); + var shortAccessorTypeName = match.Groups["shortTypeName"].Value; + var rawMemberName = match.Groups["memberName"].Value; + var extraQualifiers = match.Groups["typeNameExtraQualifiers"].Value; + + memberName = rawMemberName.Trim(); + accessorTypeName = shortAccessorTypeName.Trim() + extraQualifiers.TrimEnd(); + return true; + } + accessorTypeName = null; + memberName = null; + return false; } } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs index 4b460b32..b758d7c3 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs @@ -1,124 +1,120 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Reflection; +using System.Reflection; using Serilog.Configuration; using Serilog.Core; using Serilog.Events; -namespace Serilog.Settings.Configuration +namespace Serilog.Settings.Configuration; + +/// +/// Contains "fake extension" methods for the Serilog configuration API. +/// By default the settings know how to find extension methods, but some configuration +/// are actually "regular" method calls and would not be found otherwise. +/// +/// This static class contains internal methods that can be used instead. +/// +/// +static class SurrogateConfigurationMethods { - /// - /// Contains "fake extension" methods for the Serilog configuration API. - /// By default the settings know how to find extension methods, but some configuration - /// are actually "regular" method calls and would not be found otherwise. - /// - /// This static class contains internal methods that can be used instead. - /// - /// - static class SurrogateConfigurationMethods - { - static readonly Dictionary SurrogateMethodCandidates = typeof(SurrogateConfigurationMethods) - .GetTypeInfo().DeclaredMethods - .GroupBy(m => m.GetParameters().First().ParameterType) - .ToDictionary(g => g.Key, g => g.ToArray()); - - - internal static readonly MethodInfo[] WriteTo = SurrogateMethodCandidates[typeof(LoggerSinkConfiguration)]; - internal static readonly MethodInfo[] AuditTo = SurrogateMethodCandidates[typeof(LoggerAuditSinkConfiguration)]; - internal static readonly MethodInfo[] Enrich = SurrogateMethodCandidates[typeof(LoggerEnrichmentConfiguration)]; - internal static readonly MethodInfo[] Destructure = SurrogateMethodCandidates[typeof(LoggerDestructuringConfiguration)]; - internal static readonly MethodInfo[] Filter = SurrogateMethodCandidates[typeof(LoggerFilterConfiguration)]; - - /* - Pass-through calls to various Serilog config methods which are - implemented as instance methods rather than extension methods. - ConfigurationReader adds those to the already discovered extension methods - so they can be invoked as well. - */ - - // ReSharper disable UnusedMember.Local - // those methods are discovered through reflection by `SurrogateMethodCandidates` - // ReSharper has no way to see that they are actually used ... - - // .WriteTo... - // ======== - static LoggerConfiguration Sink( - LoggerSinkConfiguration loggerSinkConfiguration, - ILogEventSink sink, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - LoggingLevelSwitch levelSwitch = null) - => loggerSinkConfiguration.Sink(sink, restrictedToMinimumLevel, levelSwitch); - - static LoggerConfiguration Logger( - LoggerSinkConfiguration loggerSinkConfiguration, - Action configureLogger, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - LoggingLevelSwitch levelSwitch = null) - => loggerSinkConfiguration.Logger(configureLogger, restrictedToMinimumLevel, levelSwitch); - - // .AuditTo... - // ======== - static LoggerConfiguration Sink( - LoggerAuditSinkConfiguration auditSinkConfiguration, - ILogEventSink sink, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - LoggingLevelSwitch levelSwitch = null) - => auditSinkConfiguration.Sink(sink, restrictedToMinimumLevel, levelSwitch); - - static LoggerConfiguration Logger( - LoggerAuditSinkConfiguration auditSinkConfiguration, - Action configureLogger, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - LoggingLevelSwitch levelSwitch = null) - => auditSinkConfiguration.Logger(configureLogger, restrictedToMinimumLevel, levelSwitch); - - // .Filter... - // ======= - // TODO: add overload for array argument (ILogEventEnricher[]) - // expose `With(params ILogEventFilter[] filters)` as if it was `With(ILogEventFilter filter)` - static LoggerConfiguration With(LoggerFilterConfiguration loggerFilterConfiguration, ILogEventFilter filter) - => loggerFilterConfiguration.With(filter); - - // .Destructure... - // ============ - // TODO: add overload for array argument (IDestructuringPolicy[]) - // expose `With(params IDestructuringPolicy[] destructuringPolicies)` as if it was `With(IDestructuringPolicy policy)` - static LoggerConfiguration With(LoggerDestructuringConfiguration loggerDestructuringConfiguration, IDestructuringPolicy policy) - => loggerDestructuringConfiguration.With(policy); - - static LoggerConfiguration ToMaximumDepth(LoggerDestructuringConfiguration loggerDestructuringConfiguration, int maximumDestructuringDepth) - => loggerDestructuringConfiguration.ToMaximumDepth(maximumDestructuringDepth); - - static LoggerConfiguration ToMaximumStringLength(LoggerDestructuringConfiguration loggerDestructuringConfiguration, int maximumStringLength) - => loggerDestructuringConfiguration.ToMaximumStringLength(maximumStringLength); - - static LoggerConfiguration ToMaximumCollectionCount(LoggerDestructuringConfiguration loggerDestructuringConfiguration, int maximumCollectionCount) - => loggerDestructuringConfiguration.ToMaximumCollectionCount(maximumCollectionCount); - - static LoggerConfiguration AsScalar(LoggerDestructuringConfiguration loggerDestructuringConfiguration, Type scalarType) - => loggerDestructuringConfiguration.AsScalar(scalarType); - - // .Enrich... - // ======= - // expose `With(params ILogEventEnricher[] enrichers)` as if it was `With(ILogEventEnricher enricher)` - static LoggerConfiguration With( - LoggerEnrichmentConfiguration loggerEnrichmentConfiguration, - ILogEventEnricher enricher) - => loggerEnrichmentConfiguration.With(enricher); - - static LoggerConfiguration AtLevel( - LoggerEnrichmentConfiguration loggerEnrichmentConfiguration, - Action configureEnricher, - LogEventLevel enrichFromLevel = LevelAlias.Minimum, - LoggingLevelSwitch levelSwitch = null) - => levelSwitch != null ? loggerEnrichmentConfiguration.AtLevel(levelSwitch, configureEnricher) - : loggerEnrichmentConfiguration.AtLevel(enrichFromLevel, configureEnricher); - - static LoggerConfiguration FromLogContext(LoggerEnrichmentConfiguration loggerEnrichmentConfiguration) - => loggerEnrichmentConfiguration.FromLogContext(); - - // ReSharper restore UnusedMember.Local - } + static readonly Dictionary SurrogateMethodCandidates = typeof(SurrogateConfigurationMethods) + .GetTypeInfo().DeclaredMethods + .GroupBy(m => m.GetParameters().First().ParameterType) + .ToDictionary(g => g.Key, g => g.ToArray()); + + + internal static readonly MethodInfo[] WriteTo = SurrogateMethodCandidates[typeof(LoggerSinkConfiguration)]; + internal static readonly MethodInfo[] AuditTo = SurrogateMethodCandidates[typeof(LoggerAuditSinkConfiguration)]; + internal static readonly MethodInfo[] Enrich = SurrogateMethodCandidates[typeof(LoggerEnrichmentConfiguration)]; + internal static readonly MethodInfo[] Destructure = SurrogateMethodCandidates[typeof(LoggerDestructuringConfiguration)]; + internal static readonly MethodInfo[] Filter = SurrogateMethodCandidates[typeof(LoggerFilterConfiguration)]; + + /* + Pass-through calls to various Serilog config methods which are + implemented as instance methods rather than extension methods. + ConfigurationReader adds those to the already discovered extension methods + so they can be invoked as well. + */ + + // ReSharper disable UnusedMember.Local + // those methods are discovered through reflection by `SurrogateMethodCandidates` + // ReSharper has no way to see that they are actually used ... + + // .WriteTo... + // ======== + static LoggerConfiguration Sink( + LoggerSinkConfiguration loggerSinkConfiguration, + ILogEventSink sink, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch? levelSwitch = null) + => loggerSinkConfiguration.Sink(sink, restrictedToMinimumLevel, levelSwitch); + + static LoggerConfiguration Logger( + LoggerSinkConfiguration loggerSinkConfiguration, + Action configureLogger, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch? levelSwitch = null) + => loggerSinkConfiguration.Logger(configureLogger, restrictedToMinimumLevel, levelSwitch); + + // .AuditTo... + // ======== + static LoggerConfiguration Sink( + LoggerAuditSinkConfiguration auditSinkConfiguration, + ILogEventSink sink, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch? levelSwitch = null) + => auditSinkConfiguration.Sink(sink, restrictedToMinimumLevel, levelSwitch); + + static LoggerConfiguration Logger( + LoggerAuditSinkConfiguration auditSinkConfiguration, + Action configureLogger, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch? levelSwitch = null) + => auditSinkConfiguration.Logger(configureLogger, restrictedToMinimumLevel, levelSwitch); + + // .Filter... + // ======= + // TODO: add overload for array argument (ILogEventEnricher[]) + // expose `With(params ILogEventFilter[] filters)` as if it was `With(ILogEventFilter filter)` + static LoggerConfiguration With(LoggerFilterConfiguration loggerFilterConfiguration, ILogEventFilter filter) + => loggerFilterConfiguration.With(filter); + + // .Destructure... + // ============ + // TODO: add overload for array argument (IDestructuringPolicy[]) + // expose `With(params IDestructuringPolicy[] destructuringPolicies)` as if it was `With(IDestructuringPolicy policy)` + static LoggerConfiguration With(LoggerDestructuringConfiguration loggerDestructuringConfiguration, IDestructuringPolicy policy) + => loggerDestructuringConfiguration.With(policy); + + static LoggerConfiguration ToMaximumDepth(LoggerDestructuringConfiguration loggerDestructuringConfiguration, int maximumDestructuringDepth) + => loggerDestructuringConfiguration.ToMaximumDepth(maximumDestructuringDepth); + + static LoggerConfiguration ToMaximumStringLength(LoggerDestructuringConfiguration loggerDestructuringConfiguration, int maximumStringLength) + => loggerDestructuringConfiguration.ToMaximumStringLength(maximumStringLength); + + static LoggerConfiguration ToMaximumCollectionCount(LoggerDestructuringConfiguration loggerDestructuringConfiguration, int maximumCollectionCount) + => loggerDestructuringConfiguration.ToMaximumCollectionCount(maximumCollectionCount); + + static LoggerConfiguration AsScalar(LoggerDestructuringConfiguration loggerDestructuringConfiguration, Type scalarType) + => loggerDestructuringConfiguration.AsScalar(scalarType); + + // .Enrich... + // ======= + // expose `With(params ILogEventEnricher[] enrichers)` as if it was `With(ILogEventEnricher enricher)` + static LoggerConfiguration With( + LoggerEnrichmentConfiguration loggerEnrichmentConfiguration, + ILogEventEnricher enricher) + => loggerEnrichmentConfiguration.With(enricher); + + static LoggerConfiguration AtLevel( + LoggerEnrichmentConfiguration loggerEnrichmentConfiguration, + Action configureEnricher, + LogEventLevel enrichFromLevel = LevelAlias.Minimum, + LoggingLevelSwitch? levelSwitch = null) + => levelSwitch != null ? loggerEnrichmentConfiguration.AtLevel(levelSwitch, configureEnricher) + : loggerEnrichmentConfiguration.AtLevel(enrichFromLevel, configureEnricher); + + static LoggerConfiguration FromLogContext(LoggerEnrichmentConfiguration loggerEnrichmentConfiguration) + => loggerEnrichmentConfiguration.FromLogContext(); + + // ReSharper restore UnusedMember.Local } diff --git a/test/Serilog.Settings.Configuration.Tests/ApiApprovalTests.cs b/test/Serilog.Settings.Configuration.Tests/ApiApprovalTests.cs new file mode 100644 index 00000000..08bf7f46 --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/ApiApprovalTests.cs @@ -0,0 +1,25 @@ +#if NET7_0 + +using PublicApiGenerator; +using Shouldly; + +namespace Serilog.Settings.Configuration.Tests; + +public class ApiApprovalTests +{ + [Fact] + public void PublicApi_Should_Not_Change_Unintentionally() + { + var assembly = typeof(ConfigurationReaderOptions).Assembly; + var publicApi = assembly.GeneratePublicApi( + new() + { + IncludeAssemblyAttributes = false, + ExcludeAttributes = new[] { "System.Diagnostics.DebuggerDisplayAttribute" }, + }); + + publicApi.ShouldMatchApproved(options => options.WithFilenameGenerator((_, _, fileType, fileExtension) => $"{assembly.GetName().Name!}.{fileType}.{fileExtension}")); + } +} + +#endif diff --git a/test/Serilog.Settings.Configuration.Tests/ConfigurationReaderTests.cs b/test/Serilog.Settings.Configuration.Tests/ConfigurationReaderTests.cs index f5c96930..d235e8f2 100644 --- a/test/Serilog.Settings.Configuration.Tests/ConfigurationReaderTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ConfigurationReaderTests.cs @@ -1,176 +1,300 @@ -using Xunit; using System.Reflection; -using System.Linq; +using Microsoft.Extensions.Configuration; +using Serilog.Events; using Serilog.Formatting; using Serilog.Settings.Configuration.Assemblies; using Serilog.Settings.Configuration.Tests.Support; +using static Serilog.Settings.Configuration.Tests.Support.ConfigurationReaderTestHelpers; -namespace Serilog.Settings.Configuration.Tests +namespace Serilog.Settings.Configuration.Tests; + +public class ConfigurationReaderTests { - public class ConfigurationReaderTests + readonly ConfigurationReader _configurationReader; + + public ConfigurationReaderTests() { - readonly ConfigurationReader _configurationReader; + _configurationReader = new ConfigurationReader( + JsonStringConfigSource.LoadSection("{ \"Serilog\": { } }", "Serilog"), + AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies), + new ConfigurationReaderOptions()); + } - public ConfigurationReaderTests() + [Fact] + public void WriteToSupportSimplifiedSyntax() + { + // language=json + var json = """ { - _configurationReader = new ConfigurationReader( - JsonStringConfigSource.LoadSection(@"{ 'Serilog': { } }", "Serilog"), - AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies)); + "WriteTo": [ "LiterateConsole", "DiagnosticTrace" ] } + """; - [Fact] - public void WriteToSupportSimplifiedSyntax() - { - var json = @" -{ - 'WriteTo': [ 'LiterateConsole', 'DiagnosticTrace' ] -}"; + var result = _configurationReader.GetMethodCalls(JsonStringConfigSource.LoadSection(json, "WriteTo")); + Assert.Equal(2, result.Count); + Assert.True(result.Contains("LiterateConsole")); + Assert.True(result.Contains("DiagnosticTrace")); - var result = _configurationReader.GetMethodCalls(JsonStringConfigSource.LoadSection(json, "WriteTo")); - Assert.Equal(2, result.Count); - Assert.True(result.Contains("LiterateConsole")); - Assert.True(result.Contains("DiagnosticTrace")); + Assert.Single(result["LiterateConsole"]); + Assert.Single(result["DiagnosticTrace"]); + } - Assert.Single(result["LiterateConsole"]); - Assert.Single(result["DiagnosticTrace"]); + [Fact] + public void WriteToSupportExpandedSyntaxWithoutArgs() + { + // language=json + var json = """ + { + "WriteTo": [ { + "Name": "LiterateConsole" + }] } + """; - [Fact] - public void WriteToSupportExpandedSyntaxWithoutArgs() - { - var json = @" -{ - 'WriteTo': [ { - 'Name': 'LiterateConsole' - }] -}"; + var result = _configurationReader.GetMethodCalls(JsonStringConfigSource.LoadSection(json, "WriteTo")); + Assert.Equal(1, result.Count); + Assert.True(result.Contains("LiterateConsole")); - var result = _configurationReader.GetMethodCalls(JsonStringConfigSource.LoadSection(json, "WriteTo")); - Assert.Equal(1, result.Count); - Assert.True(result.Contains("LiterateConsole")); + Assert.Single(result["LiterateConsole"]); + } - Assert.Single(result["LiterateConsole"]); + [Fact] + public void WriteToSupportExpandedSyntaxWithArgs() + { + // language=json + var json = """ + { + "WriteTo": [ { + "Name": "LiterateConsole", + "Args": { + "outputTemplate": "{Message}" + } + }] } + """; - [Fact] - public void WriteToSupportExpandedSyntaxWithArgs() - { - var json = @" -{ - 'WriteTo': [ { - 'Name': 'LiterateConsole', - 'Args': { - 'outputTemplate': '{Message}' - }, - }] -}"; + var result = _configurationReader.GetMethodCalls(JsonStringConfigSource.LoadSection(json, "WriteTo")); - var result = _configurationReader.GetMethodCalls(JsonStringConfigSource.LoadSection(json, "WriteTo")); + Assert.Single(result); + Assert.True(result.Contains("LiterateConsole")); - Assert.Single(result); - Assert.True(result.Contains("LiterateConsole")); + Assert.Single(result["LiterateConsole"]); - Assert.Single(result["LiterateConsole"]); + var args = result["LiterateConsole"].Single().ToArray(); - var args = result["LiterateConsole"].Single().ToArray(); + Assert.Single(args); + Assert.Equal("outputTemplate", args[0].Key); + Assert.Equal("{Message}", args[0].Value.ConvertTo(typeof(string), new ResolutionContext())); + } - Assert.Single(args); - Assert.Equal("outputTemplate", args[0].Key); - Assert.Equal("{Message}", args[0].Value.ConvertTo(typeof(string), new ResolutionContext())); + [Fact] + public void WriteToSupportMultipleSinksOfTheSameKind() + { + // language=json + var json = """ + { + "WriteTo": [ + { + "Name": "LiterateConsole", + "Args": { + "outputTemplate": "{Message}" + } + }, + "DiagnosticTrace" + ], + "WriteTo:File1": { + "Name": "File", + "Args": { + "outputTemplate": "{Message}" + } + }, + "WriteTo:File2": { + "Name": "File", + "Args": { + "outputTemplate": "{Message}" + } + } } + """; + + var result = _configurationReader.GetMethodCalls(JsonStringConfigSource.LoadSection(json, "WriteTo")); - [Fact] - public void WriteToSupportMultipleSinksOfTheSameKind() + Assert.Equal(3, result.Count); + Assert.True(result.Contains("LiterateConsole")); + Assert.True(result.Contains("DiagnosticTrace")); + Assert.True(result.Contains("File")); + + Assert.Single(result["LiterateConsole"]); + Assert.Single(result["DiagnosticTrace"]); + Assert.Equal(2, result["File"].Count()); + } + + [Fact] + public void Enrich_SupportSimplifiedSyntax() + { + // language=json + var json = """ { - var json = @" -{ - 'WriteTo': [ - { - 'Name': 'LiterateConsole', - 'Args': { - 'outputTemplate': '{Message}' - }, - }, - 'DiagnosticTrace' - ], - 'WriteTo:File1': { - 'Name': 'File', - 'Args': { - 'outputTemplate': '{Message}' - }, - }, - 'WriteTo:File2': { - 'Name': 'File', - 'Args': { - 'outputTemplate': '{Message}' - }, + "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ] + } + """; + + var result = _configurationReader.GetMethodCalls(JsonStringConfigSource.LoadSection(json, "Enrich")); + Assert.Equal(3, result.Count); + Assert.True(result.Contains("FromLogContext")); + Assert.True(result.Contains("WithMachineName")); + Assert.True(result.Contains("WithThreadId")); + + Assert.Single(result["FromLogContext"]); + Assert.Single(result["WithMachineName"]); + Assert.Single(result["WithThreadId"]); } -}"; - var result = _configurationReader.GetMethodCalls(JsonStringConfigSource.LoadSection(json, "WriteTo")); + [Fact] + public void CallableMethodsAreSelected() + { + var options = typeof(DummyLoggerConfigurationExtensions).GetTypeInfo().DeclaredMethods.ToList(); + Assert.Equal(2, options.Count(mi => mi.Name == "DummyRollingFile")); + var suppliedArgumentNames = new[] { "pathFormat" }; - Assert.Equal(3, result.Count); - Assert.True(result.Contains("LiterateConsole")); - Assert.True(result.Contains("DiagnosticTrace")); - Assert.True(result.Contains("File")); + var selected = ConfigurationReader.SelectConfigurationMethod(options, "DummyRollingFile", suppliedArgumentNames); + Assert.Equal(typeof(string), selected?.GetParameters()[1].ParameterType); + } - Assert.Single(result["LiterateConsole"]); - Assert.Single(result["DiagnosticTrace"]); - Assert.Equal(2, result["File"].Count()); - } + [Fact] + public void MethodsAreSelectedBasedOnCountOfMatchedArguments() + { + var options = typeof(DummyLoggerConfigurationExtensions).GetTypeInfo().DeclaredMethods.ToList(); + Assert.Equal(2, options.Count(mi => mi.Name == "DummyRollingFile")); - [Fact] - public void Enrich_SupportSimplifiedSyntax() - { - var json = @" -{ - 'Enrich': [ 'FromLogContext', 'WithMachineName', 'WithThreadId' ] -}"; - - var result = _configurationReader.GetMethodCalls(JsonStringConfigSource.LoadSection(json, "Enrich")); - Assert.Equal(3, result.Count); - Assert.True(result.Contains("FromLogContext")); - Assert.True(result.Contains("WithMachineName")); - Assert.True(result.Contains("WithThreadId")); - - Assert.Single(result["FromLogContext"]); - Assert.Single(result["WithMachineName"]); - Assert.Single(result["WithThreadId"]); - } + var suppliedArgumentNames = new[] { "pathFormat", "formatter" }; - [Fact] - public void CallableMethodsAreSelected() - { - var options = typeof(DummyLoggerConfigurationExtensions).GetTypeInfo().DeclaredMethods.ToList(); - Assert.Equal(2, options.Count(mi => mi.Name == "DummyRollingFile")); - var suppliedArgumentNames = new[] { "pathFormat" }; + var selected = ConfigurationReader.SelectConfigurationMethod(options, "DummyRollingFile", suppliedArgumentNames); + Assert.Equal(typeof(ITextFormatter), selected?.GetParameters()[1].ParameterType); + } - var selected = ConfigurationReader.SelectConfigurationMethod(options, "DummyRollingFile", suppliedArgumentNames); - Assert.Equal(typeof(string), selected.GetParameters()[1].ParameterType); - } + [Fact] + public void MethodsAreSelectedBasedOnCountOfMatchedArgumentsAndThenStringType() + { + var options = typeof(DummyLoggerConfigurationWithMultipleMethodsExtensions).GetTypeInfo().DeclaredMethods.ToList(); + Assert.Equal(3, options.Count(mi => mi.Name == "DummyRollingFile")); - [Fact] - public void MethodsAreSelectedBasedOnCountOfMatchedArguments() - { - var options = typeof(DummyLoggerConfigurationExtensions).GetTypeInfo().DeclaredMethods.ToList(); - Assert.Equal(2, options.Count(mi => mi.Name == "DummyRollingFile")); + var suppliedArgumentNames = new[] { "pathFormat", "formatter" }; - var suppliedArgumentNames = new[] { "pathFormat", "formatter" }; + var selected = ConfigurationReader.SelectConfigurationMethod(options, "DummyRollingFile", suppliedArgumentNames); + Assert.Equal(typeof(string), selected?.GetParameters()[2].ParameterType); + } - var selected = ConfigurationReader.SelectConfigurationMethod(options, "DummyRollingFile", suppliedArgumentNames); - Assert.Equal(typeof(ITextFormatter), selected.GetParameters()[1].ParameterType); + public static IEnumerable FlatMinimumLevel => new List + { + new object[] { GetConfigRoot(appsettingsJsonLevel: minimumLevelFlatTemplate.Format(LogEventLevel.Error)), LogEventLevel.Error }, + new object[] { GetConfigRoot(appsettingsDevelopmentJsonLevel: minimumLevelFlatTemplate.Format(LogEventLevel.Error)), LogEventLevel.Error }, + new object[] { GetConfigRoot(envVariables: new Dictionary {{minimumLevelFlatKey, LogEventLevel.Error.ToString()}}), LogEventLevel.Error}, + new object[] { GetConfigRoot( + appsettingsJsonLevel: minimumLevelFlatTemplate.Format(LogEventLevel.Debug), + envVariables: new Dictionary {{minimumLevelFlatKey, LogEventLevel.Error.ToString()}}), + LogEventLevel.Error } + }; + + [Theory] + [MemberData(nameof(FlatMinimumLevel))] + public void FlatMinimumLevelCorrectOneIsEnabledOnLogger(IConfigurationRoot root, LogEventLevel expectedMinimumLevel) + { + var reader = new ConfigurationReader(root.GetSection("Serilog"), AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies), new ConfigurationReaderOptions(), root); + var loggerConfig = new LoggerConfiguration(); + + reader.Configure(loggerConfig); + + AssertLogEventLevels(loggerConfig, expectedMinimumLevel); + } + + public static IEnumerable ObjectMinimumLevel => new List + { + new object[] { GetConfigRoot(appsettingsJsonLevel: minimumLevelObjectTemplate.Format(LogEventLevel.Error)), LogEventLevel.Error }, + new object[] { GetConfigRoot(appsettingsJsonLevel: minimumLevelObjectTemplate.Format(LogEventLevel.Error.ToString().ToUpper())), LogEventLevel.Error }, + new object[] { GetConfigRoot(appsettingsDevelopmentJsonLevel: minimumLevelObjectTemplate.Format(LogEventLevel.Error)), LogEventLevel.Error }, + new object[] { GetConfigRoot(envVariables: new Dictionary{{minimumLevelObjectKey, LogEventLevel.Error.ToString() } }), LogEventLevel.Error }, + new object[] { GetConfigRoot( + appsettingsJsonLevel: minimumLevelObjectTemplate.Format(LogEventLevel.Error), + appsettingsDevelopmentJsonLevel: minimumLevelObjectTemplate.Format(LogEventLevel.Debug)), + LogEventLevel.Debug } + }; + + [Theory] + [MemberData(nameof(ObjectMinimumLevel))] + public void ObjectMinimumLevelCorrectOneIsEnabledOnLogger(IConfigurationRoot root, LogEventLevel expectedMinimumLevel) + { + var reader = new ConfigurationReader(root.GetSection("Serilog"), AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies), new ConfigurationReaderOptions(), root); + var loggerConfig = new LoggerConfiguration(); - [Fact] - public void MethodsAreSelectedBasedOnCountOfMatchedArgumentsAndThenStringType() + reader.Configure(loggerConfig); + + AssertLogEventLevels(loggerConfig, expectedMinimumLevel); + } + + // currently only works in the .NET 4.6.1 and .NET Standard builds of Serilog.Settings.Configuration + public static IEnumerable MixedMinimumLevel => new List + { + new object[] { - var options = typeof(DummyLoggerConfigurationWithMultipleMethodsExtensions).GetTypeInfo().DeclaredMethods.ToList(); - Assert.Equal(3, options.Count(mi => mi.Name == "DummyRollingFile")); + GetConfigRoot( + appsettingsJsonLevel: minimumLevelObjectTemplate.Format(LogEventLevel.Error), + appsettingsDevelopmentJsonLevel: minimumLevelFlatTemplate.Format(LogEventLevel.Debug)), + LogEventLevel.Debug + }, + new object[] + { + GetConfigRoot( + appsettingsJsonLevel: minimumLevelFlatTemplate.Format(LogEventLevel.Error), + appsettingsDevelopmentJsonLevel: minimumLevelObjectTemplate.Format(LogEventLevel.Debug)), + LogEventLevel.Debug + }, + // precedence should be flat > object if from the same source + new object[] + { + GetConfigRoot( + envVariables: new Dictionary() + { + {minimumLevelObjectKey, LogEventLevel.Error.ToString()}, + {minimumLevelFlatKey, LogEventLevel.Debug.ToString()} + }), + LogEventLevel.Debug + } + }; - var suppliedArgumentNames = new[] { "pathFormat", "formatter" }; + [Theory] + [MemberData(nameof(MixedMinimumLevel))] + public void MixedMinimumLevelCorrectOneIsEnabledOnLogger(IConfigurationRoot root, LogEventLevel expectedMinimumLevel) + { + var reader = new ConfigurationReader(root.GetSection("Serilog"), AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies), new ConfigurationReaderOptions(), root); + var loggerConfig = new LoggerConfiguration(); + + reader.Configure(loggerConfig); - var selected = ConfigurationReader.SelectConfigurationMethod(options, "DummyRollingFile", suppliedArgumentNames); - Assert.Equal(typeof(string), selected.GetParameters()[2].ParameterType); + AssertLogEventLevels(loggerConfig, expectedMinimumLevel); + } + + [Fact] + public void NoConfigurationRootUsedStillValid() + { + // language=json + var json = """ + { + "Nest": { + "Serilog": { + "MinimumLevel": "Error" + } + } } + """; + + var section = JsonStringConfigSource.LoadSection(json, "Nest"); + var reader = new ConfigurationReader(section.GetSection("Serilog"), AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies), new ConfigurationReaderOptions(), section); + var loggerConfig = new LoggerConfiguration(); + + reader.Configure(loggerConfig); + + AssertLogEventLevels(loggerConfig, LogEventLevel.Error); } } diff --git a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs index b9f163a2..4916f352 100644 --- a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs @@ -1,4 +1,4 @@ -using System; +using System.Globalization; using Microsoft.Extensions.Configuration; using Serilog.Core; using Serilog.Events; @@ -6,1213 +6,1573 @@ using TestDummies; using TestDummies.Console; using TestDummies.Console.Themes; -using Xunit; -namespace Serilog.Settings.Configuration.Tests +namespace Serilog.Settings.Configuration.Tests; + +public class ConfigurationSettingsTests { - public class ConfigurationSettingsTests + static LoggerConfiguration ConfigFromJson(string jsonString, string? secondJsonSource = null, ConfigurationReaderOptions? options = null) { - static LoggerConfiguration ConfigFromJson(string jsonString, string secondJsonSource = null) - { - return ConfigFromJson(jsonString, secondJsonSource, out _); - } + return ConfigFromJson(jsonString, secondJsonSource, out _, options); + } - static LoggerConfiguration ConfigFromJson(string jsonString, out IConfiguration configuration) - { - return ConfigFromJson(jsonString, null, out configuration); - } + static LoggerConfiguration ConfigFromJson(string jsonString, out IConfiguration configuration, ConfigurationReaderOptions? options = null) + { + return ConfigFromJson(jsonString, null, out configuration, options); + } - static LoggerConfiguration ConfigFromJson(string jsonString, string secondJsonSource, out IConfiguration configuration) - { - var builder = new ConfigurationBuilder().AddJsonString(jsonString); - if (secondJsonSource != null) - builder.AddJsonString(secondJsonSource); - configuration = builder.Build(); - return new LoggerConfiguration() - .ReadFrom.Configuration(configuration); - } + static LoggerConfiguration ConfigFromJson(string jsonString, string? secondJsonSource, out IConfiguration configuration, ConfigurationReaderOptions? options) + { + var builder = new ConfigurationBuilder().AddJsonString(jsonString); + if (secondJsonSource != null) + builder.AddJsonString(secondJsonSource); + configuration = builder.Build(); + return new LoggerConfiguration() + .ReadFrom.Configuration(configuration, options); + } - [Fact] - public void PropertyEnrichmentIsApplied() - { - LogEvent evt = null; + [Fact] + public void PropertyEnrichmentIsApplied() + { + LogEvent? evt = null; - var json = @"{ - ""Serilog"": { - ""Properties"": { - ""App"": ""Test"" + // language=json + var json = """ + { + "Serilog": { + "Properties": { + "App": "Test" } } - }"; + } + """; - var log = ConfigFromJson(json) - .WriteTo.Sink(new DelegatingSink(e => evt = e)) - .CreateLogger(); + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); - log.Information("Has a test property"); + log.Information("Has a test property"); - Assert.NotNull(evt); - Assert.Equal("Test", evt.Properties["App"].LiteralValue()); - } + Assert.NotNull(evt); + Assert.Equal("Test", evt.Properties["App"].LiteralValue()); + } - [Theory] - [InlineData("extended syntax", - @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [ - { ""Name"": ""DummyConsole""}, - { ""Name"": ""DummyWithLevelSwitch""}, - ] + [Theory] + [InlineData(null)] + [InlineData("")] + public void CanReadWithoutSerilogSection(string sectionName) + { + LogEvent? evt = null; + + // language=json + var json = """ + { + "Properties": { + "App": "Test" } - }")] - [InlineData("simplified syntax", - @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [""DummyConsole"", ""DummyWithLevelSwitch"" ] + } + """; + + var log = ConfigFromJson(json, options: new ConfigurationReaderOptions { SectionName = sectionName }) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + log.Information("Has a test property"); + + Assert.NotNull(evt); + Assert.Equal("Test", evt.Properties["App"].LiteralValue()); + } + + [Theory] + [InlineData("extended syntax", """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": [ + { "Name": "DummyConsole"}, + { "Name": "DummyWithLevelSwitch"}, + ] + } + } + """)] + [InlineData("simplified syntax", """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": ["DummyConsole", "DummyWithLevelSwitch" ] + } + } + """)] + public void ParameterlessSinksAreConfigured(string syntax, string json) + { + _ = syntax; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyConsoleSink.Emitted.Clear(); + DummyWithLevelSwitchSink.Emitted.Clear(); + + log.Write(Some.InformationEvent()); + + Assert.Single(DummyConsoleSink.Emitted); + Assert.Single(DummyWithLevelSwitchSink.Emitted); + } + + [Fact] + public void ConfigurationAssembliesFromDllScanning() + { + // language=json + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": ["DummyConsole"] } - }")] - public void ParameterlessSinksAreConfigured(string syntax, string json) - { - _ = syntax; + } + """; + + var builder = new ConfigurationBuilder().AddJsonString(json); + var config = builder.Build(); + var log = new LoggerConfiguration() + .ReadFrom.Configuration( + configuration: config, + readerOptions: new ConfigurationReaderOptions(ConfigurationAssemblySource.AlwaysScanDllFiles)) + .CreateLogger(); - var log = ConfigFromJson(json) - .CreateLogger(); + DummyConsoleSink.Emitted.Clear(); - DummyConsoleSink.Emitted.Clear(); - DummyWithLevelSwitchSink.Emitted.Clear(); + log.Write(Some.InformationEvent()); - log.Write(Some.InformationEvent()); + Assert.Single(DummyConsoleSink.Emitted); + } + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ConfigurationAssembliesWithInternalMethodInPublicClass(bool allowInternalMethods) + { + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": ["DummyConsoleInternal"] + } + } + """; + + var builder = new ConfigurationBuilder().AddJsonString(json); + var config = builder.Build(); + var log = new LoggerConfiguration() + .ReadFrom.Configuration( + configuration: config, + readerOptions: new ConfigurationReaderOptions(ConfigurationAssemblySource.AlwaysScanDllFiles) { AllowInternalMethods = allowInternalMethods }) + .CreateLogger(); + + DummyConsoleSink.Emitted.Clear(); + + log.Write(Some.InformationEvent()); + + if (allowInternalMethods) Assert.Single(DummyConsoleSink.Emitted); - Assert.Single(DummyWithLevelSwitchSink.Emitted); - } + else + Assert.Empty(DummyConsoleSink.Emitted); + } - [Fact] - public void ConfigurationAssembliesFromDllScanning() - { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [""DummyConsole""] + [Theory] + [InlineData(false)] + [InlineData(true)] + public void ConfigurationAssembliesWithPublicMethodInInternalClass(bool allowInternalTypes) + { + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": ["DummyConsolePublicInInternal"] } - }"; + } + """; + + var builder = new ConfigurationBuilder().AddJsonString(json); + var config = builder.Build(); + var log = new LoggerConfiguration() + .ReadFrom.Configuration( + configuration: config, + readerOptions: new ConfigurationReaderOptions(ConfigurationAssemblySource.AlwaysScanDllFiles) { AllowInternalTypes = allowInternalTypes }) + .CreateLogger(); - var builder = new ConfigurationBuilder().AddJsonString(json); - var config = builder.Build(); - var log = new LoggerConfiguration() - .ReadFrom.Configuration( - configuration: config, - configurationAssemblySource: ConfigurationAssemblySource.AlwaysScanDllFiles) - .CreateLogger(); + DummyConsoleSink.Emitted.Clear(); - DummyConsoleSink.Emitted.Clear(); + log.Write(Some.InformationEvent()); - log.Write(Some.InformationEvent()); + if (allowInternalTypes) + Assert.Single(DummyConsoleSink.Emitted); + else + Assert.Empty(DummyConsoleSink.Emitted); + } + [Theory] + [InlineData(false, false)] + [InlineData(true, true)] + public void ConfigurationAssembliesWithInternalMethodInInternalClass(bool allowInternalTypes, bool allowInternalMethods) + { + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": ["DummyConsoleInternalInInternal"] + } + } + """; + + var builder = new ConfigurationBuilder().AddJsonString(json); + var config = builder.Build(); + var log = new LoggerConfiguration() + .ReadFrom.Configuration( + configuration: config, + readerOptions: new ConfigurationReaderOptions(ConfigurationAssemblySource.AlwaysScanDllFiles) { AllowInternalTypes = allowInternalTypes, AllowInternalMethods = allowInternalMethods }) + .CreateLogger(); + + DummyConsoleSink.Emitted.Clear(); + + log.Write(Some.InformationEvent()); + + if (allowInternalTypes && allowInternalMethods) Assert.Single(DummyConsoleSink.Emitted); - } + else + Assert.Empty(DummyConsoleSink.Emitted); + } - [Fact] - public void SinksAreConfigured() - { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [{ - ""Name"": ""DummyRollingFile"", - ""Args"": {""pathFormat"" : ""C:\\""} + [Fact] + public void SinksAreConfigured() + { + // language=json + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": [{ + "Name": "DummyRollingFile", + "Args": {"pathFormat" : "C:\\"} }] } - }"; + } + """; - var log = ConfigFromJson(json) - .CreateLogger(); + var log = ConfigFromJson(json) + .CreateLogger(); - DummyRollingFileSink.Reset(); - DummyRollingFileAuditSink.Reset(); + DummyRollingFileSink.Reset(); + DummyRollingFileAuditSink.Reset(); - log.Write(Some.InformationEvent()); + log.Write(Some.InformationEvent()); - Assert.Single(DummyRollingFileSink.Emitted); - Assert.Empty(DummyRollingFileAuditSink.Emitted); - } + Assert.Single(DummyRollingFileSink.Emitted); + Assert.Empty(DummyRollingFileAuditSink.Emitted); + } - [Fact] - public void AuditSinksAreConfigured() - { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""AuditTo"": [{ - ""Name"": ""DummyRollingFile"", - ""Args"": {""pathFormat"" : ""C:\\""} + [Fact] + public void AuditSinksAreConfigured() + { + // language=json + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "AuditTo": [{ + "Name": "DummyRollingFile", + "Args": {"pathFormat" : "C:\\"} }] } - }"; + } + """; - var log = ConfigFromJson(json) - .CreateLogger(); + var log = ConfigFromJson(json) + .CreateLogger(); - DummyRollingFileSink.Reset(); - DummyRollingFileAuditSink.Reset(); + DummyRollingFileSink.Reset(); + DummyRollingFileAuditSink.Reset(); - log.Write(Some.InformationEvent()); + log.Write(Some.InformationEvent()); - Assert.Empty(DummyRollingFileSink.Emitted); - Assert.Single(DummyRollingFileAuditSink.Emitted); - } + Assert.Empty(DummyRollingFileSink.Emitted); + Assert.Single(DummyRollingFileAuditSink.Emitted); + } - [Fact] - public void AuditToSubLoggersAreConfigured() - { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""AuditTo"": [{ - ""Name"": ""Logger"", - ""Args"": { - ""configureLogger"" : { - ""AuditTo"": [{ - ""Name"": ""DummyRollingFile"", - ""Args"": {""pathFormat"" : ""C:\\""} - }]} - } - }] + [Fact] + public void AuditToSubLoggersAreConfigured() + { + // language=json + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "AuditTo": [{ + "Name": "Logger", + "Args": { + "configureLogger" : { + "AuditTo": [{ + "Name": "DummyRollingFile", + "Args": {"pathFormat" : "C:\\"} + }]} + } + }] + } } - }"; + """; - var log = ConfigFromJson(json) - .CreateLogger(); + var log = ConfigFromJson(json) + .CreateLogger(); - DummyRollingFileSink.Reset(); - DummyRollingFileAuditSink.Reset(); + DummyRollingFileSink.Reset(); + DummyRollingFileAuditSink.Reset(); - log.Write(Some.InformationEvent()); + log.Write(Some.InformationEvent()); - Assert.Empty(DummyRollingFileSink.Emitted); - Assert.Single(DummyRollingFileAuditSink.Emitted); - } + Assert.Empty(DummyRollingFileSink.Emitted); + Assert.Single(DummyRollingFileAuditSink.Emitted); + } - [Fact] - public void TestMinimumLevelOverrides() - { - var json = @"{ - ""Serilog"": { - ""MinimumLevel"" : { - ""Override"" : { - ""System"" : ""Warning"" + [Fact] + public void TestMinimumLevelOverrides() + { + // language=json + var json = """ + { + "Serilog": { + "MinimumLevel" : { + "Override" : { + "System" : "Warning" } } } - }"; + } + """; - LogEvent evt = null; + LogEvent? evt = null; - var log = ConfigFromJson(json) - .WriteTo.Sink(new DelegatingSink(e => evt = e)) - .CreateLogger(); + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); - var systemLogger = log.ForContext(); - systemLogger.Write(Some.InformationEvent()); + var systemLogger = log.ForContext(); + systemLogger.Write(Some.InformationEvent()); - Assert.Null(evt); + Assert.Null(evt); - systemLogger.Warning("Bad things"); - Assert.NotNull(evt); + systemLogger.Warning("Bad things"); + Assert.NotNull(evt); - evt = null; - log.Write(Some.InformationEvent()); - Assert.NotNull(evt); - } + evt = null; + log.Write(Some.InformationEvent()); + Assert.NotNull(evt); + } - [Fact] - public void TestMinimumLevelOverridesForChildContext() - { - var json = @"{ - ""Serilog"": { - ""MinimumLevel"" : { - ""Default"" : ""Warning"", - ""Override"" : { - ""System"" : ""Warning"", - ""System.Threading"": ""Debug"" + [Fact] + public void TestMinimumLevelOverridesForChildContext() + { + // language=json + var json = """ + { + "Serilog": { + "MinimumLevel" : { + "Default" : "Warning", + "Override" : { + "System" : "Warning", + "System.Threading": "Debug" } } } - }"; + } + """; - LogEvent evt = null; + LogEvent? evt = null; - var log = ConfigFromJson(json) - .WriteTo.Sink(new DelegatingSink(e => evt = e)) - .CreateLogger(); + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); - log.Write(Some.DebugEvent()); - Assert.Null(evt); + log.Write(Some.DebugEvent()); + Assert.Null(evt); - var custom = log.ForContext(Constants.SourceContextPropertyName, typeof(System.Threading.Tasks.Task).FullName + "<42>"); - custom.Write(Some.DebugEvent()); - Assert.NotNull(evt); + var custom = log.ForContext(Constants.SourceContextPropertyName, typeof(System.Threading.Tasks.Task).FullName + "<42>"); + custom.Write(Some.DebugEvent()); + Assert.NotNull(evt); - evt = null; - var systemThreadingLogger = log.ForContext(); - systemThreadingLogger.Write(Some.DebugEvent()); - Assert.NotNull(evt); - } + evt = null; + var systemThreadingLogger = log.ForContext(); + systemThreadingLogger.Write(Some.DebugEvent()); + Assert.NotNull(evt); + } - [Fact] - public void SinksWithAbstractParamsAreConfiguredWithTypeName() - { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [{ - ""Name"": ""DummyConsole"", - ""Args"": {""theme"" : ""Serilog.Settings.Configuration.Tests.Support.CustomConsoleTheme, Serilog.Settings.Configuration.Tests""} + [Fact] + public void SinksWithAbstractParamsAreConfiguredWithTypeName() + { + // language=json + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": [{ + "Name": "DummyConsole", + "Args": {"theme" : "Serilog.Settings.Configuration.Tests.Support.CustomConsoleTheme, Serilog.Settings.Configuration.Tests"} }] } - }"; + } + """; - DummyConsoleSink.Theme = null; + DummyConsoleSink.Theme = null; - ConfigFromJson(json) - .CreateLogger(); + ConfigFromJson(json) + .CreateLogger(); - Assert.NotNull(DummyConsoleSink.Theme); - Assert.IsType(DummyConsoleSink.Theme); - } + Assert.NotNull(DummyConsoleSink.Theme); + Assert.IsType(DummyConsoleSink.Theme); + } - [Fact] - public void SinksAreConfiguredWithStaticMember() - { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [{ - ""Name"": ""DummyConsole"", - ""Args"": {""theme"" : ""TestDummies.Console.Themes.ConsoleThemes::Theme1, TestDummies""} + [Fact] + public void SinksAreConfiguredWithStaticMember() + { + // language=json + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": [{ + "Name": "DummyConsole", + "Args": {"theme" : "TestDummies.Console.Themes.ConsoleThemes::Theme1, TestDummies"} }] } - }"; + } + """; - DummyConsoleSink.Theme = null; + DummyConsoleSink.Theme = null; - ConfigFromJson(json) - .CreateLogger(); + ConfigFromJson(json) + .CreateLogger(); - Assert.Equal(ConsoleThemes.Theme1, DummyConsoleSink.Theme); - } + Assert.Equal(ConsoleThemes.Theme1, DummyConsoleSink.Theme); + } - [Theory] - [InlineData("$switchName", true)] - [InlineData("$SwitchName", true)] - [InlineData("SwitchName", true)] - [InlineData("$switch1", true)] - [InlineData("$sw1tch0", true)] - [InlineData("sw1tch0", true)] - [InlineData("$SWITCHNAME", true)] - [InlineData("$$switchname", false)] - [InlineData("$switchname$", false)] - [InlineData("switch$name", false)] - [InlineData("$", false)] - [InlineData("", false)] - [InlineData(" ", false)] - [InlineData("$1switch", false)] - [InlineData("$switch_name", false)] - public void LoggingLevelSwitchNameValidityScenarios(string switchName, bool expectedValid) - { - Assert.True(ConfigurationReader.IsValidSwitchName(switchName) == expectedValid, - $"expected IsValidSwitchName({switchName}) to return {expectedValid} "); - } + [Theory] + [InlineData("$switchName", true)] + [InlineData("$SwitchName", true)] + [InlineData("SwitchName", true)] + [InlineData("$switch1", true)] + [InlineData("$sw1tch0", true)] + [InlineData("sw1tch0", true)] + [InlineData("$SWITCHNAME", true)] + [InlineData("$$switchname", false)] + [InlineData("$switchname$", false)] + [InlineData("switch$name", false)] + [InlineData("$", false)] + [InlineData("", false)] + [InlineData(" ", false)] + [InlineData("$1switch", false)] + [InlineData("$switch_name", false)] + public void LoggingLevelSwitchNameValidityScenarios(string switchName, bool expectedValid) + { + Assert.True(ConfigurationReader.IsValidSwitchName(switchName) == expectedValid, + $"expected IsValidSwitchName({switchName}) to return {expectedValid} "); + } - [Fact] - public void LoggingLevelSwitchWithInvalidNameThrowsFormatException() - { - var json = @"{ - ""Serilog"": { - ""LevelSwitches"": {""1InvalidSwitchName"" : ""Warning"" } + [Fact] + public void LoggingLevelSwitchWithInvalidNameThrowsFormatException() + { + // language=json + var json = """ + { + "Serilog": { + "LevelSwitches": {"1InvalidSwitchName" : "Warning" } } - }"; + } + """; - var ex = Assert.Throws(() => ConfigFromJson(json)); + var ex = Assert.Throws(() => ConfigFromJson(json)); - Assert.Contains("\"1InvalidSwitchName\"", ex.Message); - Assert.Contains("'$' sign", ex.Message); - Assert.Contains("\"LevelSwitches\" : {\"$switchName\" :", ex.Message); - } + Assert.Contains("\"1InvalidSwitchName\"", ex.Message); + Assert.Contains("'$' sign", ex.Message); + Assert.Contains("\"LevelSwitches\" : {\"$switchName\" :", ex.Message); + } - [Theory] - [InlineData("$mySwitch")] - [InlineData("mySwitch")] - public void LoggingFilterSwitchIsConfigured(string switchName) - { - var json = $@"{{ - 'Serilog': {{ - 'FilterSwitches': {{ '{switchName}': 'Prop = 42' }}, - 'Filter:BySwitch': {{ - 'Name': 'ControlledBy', - 'Args': {{ - 'switch': '$mySwitch' - }} - }} - }} - }}"; - LogEvent evt = null; - - var log = ConfigFromJson(json) - .WriteTo.Sink(new DelegatingSink(e => evt = e)) - .CreateLogger(); - - log.Write(Some.InformationEvent()); - Assert.Null(evt); - - log.ForContext("Prop", 42).Write(Some.InformationEvent()); - Assert.NotNull(evt); - } + [Theory] + [InlineData("$mySwitch")] + [InlineData("mySwitch")] + public void LoggingFilterSwitchIsConfigured(string switchName) + { + // language=json + var json = $$""" + { + "Serilog": { + "FilterSwitches": { "{{switchName}}": "Prop = 42" }, + "Filter:BySwitch": { + "Name": "ControlledBy", + "Args": { + "switch": "$mySwitch" + } + } + } + } + """; + LogEvent? evt = null; - [Theory] - [InlineData("$switch1")] - [InlineData("switch1")] - public void LoggingLevelSwitchIsConfigured(string switchName) - { - var json = $@"{{ - 'Serilog': {{ - 'LevelSwitches': {{ '{switchName}' : 'Warning' }}, - 'MinimumLevel' : {{ - 'ControlledBy' : '$switch1' - }} - }} - }}"; - LogEvent evt = null; - - var log = ConfigFromJson(json) - .WriteTo.Sink(new DelegatingSink(e => evt = e)) - .CreateLogger(); - - log.Write(Some.DebugEvent()); - Assert.True(evt is null, "LoggingLevelSwitch initial level was Warning. It should not log Debug messages"); - log.Write(Some.InformationEvent()); - Assert.True(evt is null, "LoggingLevelSwitch initial level was Warning. It should not log Information messages"); - log.Write(Some.WarningEvent()); - Assert.True(evt != null, "LoggingLevelSwitch initial level was Warning. It should log Warning messages"); - } + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); - [Fact] - public void SettingMinimumLevelControlledByToAnUndeclaredSwitchThrows() - { - var json = @"{ - ""Serilog"": { - ""LevelSwitches"": {""$switch1"" : ""Warning"" }, - ""MinimumLevel"" : { - ""ControlledBy"" : ""$switch2"" + log.Write(Some.InformationEvent()); + Assert.Null(evt); + + log.ForContext("Prop", 42).Write(Some.InformationEvent()); + Assert.NotNull(evt); + } + + [Theory] + [InlineData("$switch1")] + [InlineData("switch1")] + public void LoggingLevelSwitchIsConfigured(string switchName) + { + // language=json + var json = $$""" + { + "Serilog": { + "LevelSwitches": { "{{switchName}}" : "Warning" }, + "MinimumLevel" : { + "ControlledBy" : "$switch1" } } - }"; + } + """; + LogEvent? evt = null; - var ex = Assert.Throws(() => - ConfigFromJson(json) - .CreateLogger()); + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); - Assert.Contains("$switch2", ex.Message); - Assert.Contains("\"LevelSwitches\":{\"$switch2\":", ex.Message); - } + log.Write(Some.DebugEvent()); + Assert.True(evt is null, "LoggingLevelSwitch initial level was Warning. It should not log Debug messages"); + log.Write(Some.InformationEvent()); + Assert.True(evt is null, "LoggingLevelSwitch initial level was Warning. It should not log Information messages"); + log.Write(Some.WarningEvent()); + Assert.True(evt != null, "LoggingLevelSwitch initial level was Warning. It should log Warning messages"); + } - [Fact] - public void LoggingLevelSwitchIsPassedToSinks() - { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""LevelSwitches"": {""$switch1"" : ""Information"" }, - ""MinimumLevel"" : { - ""ControlledBy"" : ""$switch1"" + [Fact] + public void SettingMinimumLevelControlledByToAnUndeclaredSwitchThrows() + { + // language=json + var json = """ + { + "Serilog": { + "LevelSwitches": {"$switch1" : "Warning" }, + "MinimumLevel" : { + "ControlledBy" : "$switch2" + } + } + } + """; + + var ex = Assert.Throws(() => + ConfigFromJson(json) + .CreateLogger()); + + Assert.Contains("$switch2", ex.Message); + Assert.Contains("\"LevelSwitches\":{\"$switch2\":", ex.Message); + } + + [Fact] + public void LoggingLevelSwitchIsPassedToSinks() + { + // language=json + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "LevelSwitches": {"$switch1" : "Information" }, + "MinimumLevel" : { + "ControlledBy" : "$switch1" }, - ""WriteTo"": [{ - ""Name"": ""DummyWithLevelSwitch"", - ""Args"": {""controlLevelSwitch"" : ""$switch1""} + "WriteTo": [{ + "Name": "DummyWithLevelSwitch", + "Args": {"controlLevelSwitch" : "$switch1"} }] } - }"; + } + """; - LogEvent evt = null; + LogEvent? evt = null; - var log = ConfigFromJson(json) - .WriteTo.Sink(new DelegatingSink(e => evt = e)) - .CreateLogger(); + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); - Assert.False(DummyWithLevelSwitchSink.ControlLevelSwitch == null, "Sink ControlLevelSwitch should have been initialized"); + Assert.False(DummyWithLevelSwitchSink.ControlLevelSwitch == null, "Sink ControlLevelSwitch should have been initialized"); - var controlSwitch = DummyWithLevelSwitchSink.ControlLevelSwitch; - Assert.NotNull(controlSwitch); + var controlSwitch = DummyWithLevelSwitchSink.ControlLevelSwitch; + Assert.NotNull(controlSwitch); - log.Write(Some.DebugEvent()); - Assert.True(evt is null, "LoggingLevelSwitch initial level was information. It should not log Debug messages"); + log.Write(Some.DebugEvent()); + Assert.True(evt is null, "LoggingLevelSwitch initial level was information. It should not log Debug messages"); - controlSwitch.MinimumLevel = LogEventLevel.Debug; - log.Write(Some.DebugEvent()); - Assert.True(evt != null, "LoggingLevelSwitch level was changed to Debug. It should log Debug messages"); - } + controlSwitch.MinimumLevel = LogEventLevel.Debug; + log.Write(Some.DebugEvent()); + Assert.True(evt != null, "LoggingLevelSwitch level was changed to Debug. It should log Debug messages"); + } - [Fact] - public void ReferencingAnUndeclaredSwitchInSinkThrows() - { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""LevelSwitches"": {""$switch1"" : ""Information"" }, - ""MinimumLevel"" : { - ""ControlledBy"" : ""$switch1"" + [Fact] + public void ReferencingAnUndeclaredSwitchInSinkThrows() + { + // language=json + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "LevelSwitches": {"$switch1" : "Information" }, + "MinimumLevel" : { + "ControlledBy" : "$switch1" }, - ""WriteTo"": [{ - ""Name"": ""DummyWithLevelSwitch"", - ""Args"": {""controlLevelSwitch"" : ""$switch2""} + "WriteTo": [{ + "Name": "DummyWithLevelSwitch", + "Args": {"controlLevelSwitch" : "$switch2"} }] } - }"; + } + """; - var ex = Assert.Throws(() => - ConfigFromJson(json) - .CreateLogger()); + var ex = Assert.Throws(() => + ConfigFromJson(json) + .CreateLogger()); - Assert.Contains("$switch2", ex.Message); - Assert.Contains("\"LevelSwitches\":{\"$switch2\":", ex.Message); - } + Assert.Contains("$switch2", ex.Message); + Assert.Contains("\"LevelSwitches\":{\"$switch2\":", ex.Message); + } - [Fact] - public void LoggingLevelSwitchCanBeUsedForMinimumLevelOverrides() - { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""LevelSwitches"": {""$specificSwitch"" : ""Warning"" }, - ""MinimumLevel"" : { - ""Default"" : ""Debug"", - ""Override"" : { - ""System"" : ""$specificSwitch"" + [Fact] + public void LoggingLevelSwitchCanBeUsedForMinimumLevelOverrides() + { + // language=json + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "LevelSwitches": {"$specificSwitch" : "Warning" }, + "MinimumLevel" : { + "Default" : "Debug", + "Override" : { + "System" : "$specificSwitch" } }, - ""WriteTo"": [{ - ""Name"": ""DummyWithLevelSwitch"", - ""Args"": {""controlLevelSwitch"" : ""$specificSwitch""} + "WriteTo": [{ + "Name": "DummyWithLevelSwitch", + "Args": {"controlLevelSwitch" : "$specificSwitch"} }] } - }"; + } + """; - LogEvent evt = null; + LogEvent? evt = null; - var log = ConfigFromJson(json) - .WriteTo.Sink(new DelegatingSink(e => evt = e)) - .CreateLogger(); + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); - var systemLogger = log.ForContext(Constants.SourceContextPropertyName, "System.Bar"); + var systemLogger = log.ForContext(Constants.SourceContextPropertyName, "System.Bar"); - log.Write(Some.InformationEvent()); - Assert.False(evt is null, "Minimum level is Debug. It should log Information messages"); + log.Write(Some.InformationEvent()); + Assert.False(evt is null, "Minimum level is Debug. It should log Information messages"); - evt = null; - // ReSharper disable HeuristicUnreachableCode - systemLogger.Write(Some.InformationEvent()); - Assert.True(evt is null, "LoggingLevelSwitch initial level was Warning for logger System.*. It should not log Information messages for SourceContext System.Bar"); + evt = null; + // ReSharper disable HeuristicUnreachableCode + systemLogger.Write(Some.InformationEvent()); + Assert.True(evt is null, "LoggingLevelSwitch initial level was Warning for logger System.*. It should not log Information messages for SourceContext System.Bar"); - systemLogger.Write(Some.WarningEvent()); - Assert.False(evt is null, "LoggingLevelSwitch initial level was Warning for logger System.*. It should log Warning messages for SourceContext System.Bar"); + systemLogger.Write(Some.WarningEvent()); + Assert.False(evt is null, "LoggingLevelSwitch initial level was Warning for logger System.*. It should log Warning messages for SourceContext System.Bar"); - evt = null; - var controlSwitch = DummyWithLevelSwitchSink.ControlLevelSwitch; + evt = null; + var controlSwitch = DummyWithLevelSwitchSink.ControlLevelSwitch; + Assert.NotNull(controlSwitch); - controlSwitch.MinimumLevel = LogEventLevel.Information; - systemLogger.Write(Some.InformationEvent()); - Assert.False(evt is null, "LoggingLevelSwitch level was changed to Information for logger System.*. It should now log Information events for SourceContext System.Bar."); - // ReSharper restore HeuristicUnreachableCode - } + controlSwitch.MinimumLevel = LogEventLevel.Information; + systemLogger.Write(Some.InformationEvent()); + Assert.False(evt is null, "LoggingLevelSwitch level was changed to Information for logger System.*. It should now log Information events for SourceContext System.Bar."); + // ReSharper restore HeuristicUnreachableCode + } - [Fact] + [Fact] - [Trait("BugFix", "https://github.com/serilog/serilog-settings-configuration/issues/142")] - public void SinkWithIConfigurationArguments() - { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [{ - ""Name"": ""DummyWithConfiguration"", - ""Args"": {} + [Trait("BugFix", "https://github.com/serilog/serilog-settings-configuration/issues/142")] + public void SinkWithIConfigurationArguments() + { + // language=json + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": [{ + "Name": "DummyWithConfiguration", + "Args": {} }] } - }"; + } + """; - DummyConfigurationSink.Reset(); - var log = ConfigFromJson(json, out var expectedConfig) - .CreateLogger(); + DummyConfigurationSink.Reset(); + var log = ConfigFromJson(json, out var expectedConfig) + .CreateLogger(); - log.Write(Some.InformationEvent()); + log.Write(Some.InformationEvent()); - Assert.NotNull(DummyConfigurationSink.Configuration); - Assert.Same(expectedConfig, DummyConfigurationSink.Configuration); - } + Assert.NotNull(DummyConfigurationSink.Configuration); + Assert.Same(expectedConfig, DummyConfigurationSink.Configuration); + } - [Fact] - [Trait("BugFix", "https://github.com/serilog/serilog-settings-configuration/issues/142")] - public void SinkWithOptionalIConfigurationArguments() - { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [{ - ""Name"": ""DummyWithOptionalConfiguration"", - ""Args"": {} + [Fact] + [Trait("BugFix", "https://github.com/serilog/serilog-settings-configuration/issues/142")] + public void SinkWithOptionalIConfigurationArguments() + { + // language=json + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": [{ + "Name": "DummyWithOptionalConfiguration", + "Args": {} }] } - }"; + } + """; - DummyConfigurationSink.Reset(); - var log = ConfigFromJson(json, out var expectedConfig) - .CreateLogger(); + DummyConfigurationSink.Reset(); + var log = ConfigFromJson(json, out var expectedConfig) + .CreateLogger(); - log.Write(Some.InformationEvent()); + log.Write(Some.InformationEvent()); - // null is the default value, but we have a configuration to provide - Assert.NotNull(DummyConfigurationSink.Configuration); - Assert.Same(expectedConfig, DummyConfigurationSink.Configuration); - } + // null is the default value, but we have a configuration to provide + Assert.NotNull(DummyConfigurationSink.Configuration); + Assert.Same(expectedConfig, DummyConfigurationSink.Configuration); + } - [Fact] - public void SinkWithIConfigSectionArguments() - { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [{ - ""Name"": ""DummyWithConfigSection"", - ""Args"": {""configurationSection"" : { ""foo"" : ""bar"" } } + [Fact] + public void SinkWithIConfigSectionArguments() + { + // language=json + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": [{ + "Name": "DummyWithConfigSection", + "Args": {"configurationSection" : { "foo" : "bar" } } }] } - }"; + } + """; - DummyConfigurationSink.Reset(); - var log = ConfigFromJson(json) - .CreateLogger(); + DummyConfigurationSink.Reset(); + var log = ConfigFromJson(json) + .CreateLogger(); - log.Write(Some.InformationEvent()); + log.Write(Some.InformationEvent()); - Assert.NotNull(DummyConfigurationSink.ConfigSection); - Assert.Equal("bar", DummyConfigurationSink.ConfigSection["foo"]); - } + Assert.NotNull(DummyConfigurationSink.ConfigSection); + Assert.Equal("bar", DummyConfigurationSink.ConfigSection["foo"]); + } - [Fact] - public void SinkWithConfigurationBindingArgument() - { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [{ - ""Name"": ""DummyRollingFile"", - ""Args"": {""pathFormat"" : ""C:\\"", - ""objectBinding"" : [ { ""foo"" : ""bar"" }, { ""abc"" : ""xyz"" } ] } + [Fact] + public void SinkWithConfigurationBindingArgument() + { + // language=json + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": [{ + "Name": "DummyRollingFile", + "Args": {"pathFormat" : "C:\\", + "objectBinding" : [ { "foo" : "bar" }, { "abc" : "xyz" } ] } }] } - }"; + } + """; - var log = ConfigFromJson(json) - .CreateLogger(); + var log = ConfigFromJson(json) + .CreateLogger(); - DummyRollingFileSink.Reset(); + DummyRollingFileSink.Reset(); - log.Write(Some.InformationEvent()); + log.Write(Some.InformationEvent()); - Assert.Single(DummyRollingFileSink.Emitted); - } + Assert.Single(DummyRollingFileSink.Emitted); + } - [Fact] - public void SinkWithStringArrayArgument() - { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [{ - ""Name"": ""DummyRollingFile"", - ""Args"": {""pathFormat"" : ""C:\\"", - ""stringArrayBinding"" : [ ""foo"", ""bar"", ""baz"" ] } + [Fact] + public void SinkWithStringArrayArgument() + { + // language=json + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": [{ + "Name": "DummyRollingFile", + "Args": {"pathFormat" : "C:\\", + "stringArrayBinding" : [ "foo", "bar", "baz" ] } }] } - }"; + } + """; - var log = ConfigFromJson(json) - .CreateLogger(); + var log = ConfigFromJson(json) + .CreateLogger(); - DummyRollingFileSink.Reset(); + DummyRollingFileSink.Reset(); - log.Write(Some.InformationEvent()); + log.Write(Some.InformationEvent()); - Assert.Single(DummyRollingFileSink.Emitted); - } + Assert.Single(DummyRollingFileSink.Emitted); + } + + [Theory] + [InlineData(".")] + [InlineData(",")] + [Trait("BugFix", "https://github.com/serilog/serilog-settings-configuration/issues/325")] + public void DestructureNumericNumbers(string numberDecimalSeparator) + { + var originalCulture = Thread.CurrentThread.CurrentCulture; + + var culture = (CultureInfo)CultureInfo.InvariantCulture.Clone(); + culture.NumberFormat.NumberDecimalSeparator = numberDecimalSeparator; + + Thread.CurrentThread.CurrentCulture = culture; - [Fact] - public void DestructureWithCollectionsOfTypeArgument() + try { - var json = @"{ - ""Serilog"": { - ""Using"": [ ""TestDummies"" ], - ""Destructure"": [{ - ""Name"": ""DummyArrayOfType"", - ""Args"": { - ""list"": [ - ""System.Byte"", - ""System.Int16"" - ], - ""array"" : [ - ""System.Int32"", - ""System.String"" - ], - ""type"" : ""System.TimeSpan"", - ""custom"" : [ - ""System.Int64"" - ], - ""customString"" : [ - ""System.UInt32"" - ] + // language=json + var json = """ + { + "Serilog": { + "Using": [ "TestDummies" ], + "Destructure": [{ + "Name": "DummyNumbers", + "Args": { + "floatValue": 0.1, + "doubleValue": 0.2, + "decimalValue": 0.3 } }] } - }"; + } + """; DummyPolicy.Current = null; ConfigFromJson(json); Assert.NotNull(DummyPolicy.Current); - Assert.Equal(typeof(TimeSpan), DummyPolicy.Current.Type); - Assert.Equal(new[] { typeof(int), typeof(string) }, DummyPolicy.Current.Array); - Assert.Equal(new[] { typeof(byte), typeof(short) }, DummyPolicy.Current.List); - Assert.Equal(typeof(long), DummyPolicy.Current.Custom.First); - Assert.Equal("System.UInt32", DummyPolicy.Current.CustomStrings.First); + Assert.Equal(0.1f, DummyPolicy.Current.Float); + Assert.Equal(0.2d, DummyPolicy.Current.Double); + Assert.Equal(0.3m, DummyPolicy.Current.Decimal); } - - [Fact] - public void SinkWithIntArrayArgument() + finally { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [{ - ""Name"": ""DummyRollingFile"", - ""Args"": {""pathFormat"" : ""C:\\"", - ""intArrayBinding"" : [ 1,2,3,4,5 ] } + Thread.CurrentThread.CurrentCulture = originalCulture; + } + } + + [Fact] + public void DestructureWithCollectionsOfTypeArgument() + { + // language=json + var json = """ + { + "Serilog": { + "Using": [ "TestDummies" ], + "Destructure": [{ + "Name": "DummyArrayOfType", + "Args": { + "list": [ + "System.Byte", + "System.Int16" + ], + "array" : [ + "System.Int32", + "System.String" + ], + "type" : "System.TimeSpan", + "custom" : [ + "System.Int64" + ], + "customString" : [ + "System.UInt32" + ] + } }] } - }"; + } + """; - var log = ConfigFromJson(json) - .CreateLogger(); + DummyPolicy.Current = null; - DummyRollingFileSink.Reset(); + ConfigFromJson(json); - log.Write(Some.InformationEvent()); + Assert.NotNull(DummyPolicy.Current); + Assert.Equal(typeof(TimeSpan), DummyPolicy.Current.Type); + Assert.Equal(new[] { typeof(int), typeof(string) }, DummyPolicy.Current.Array); + Assert.Equal(new[] { typeof(byte), typeof(short) }, DummyPolicy.Current.List!); + Assert.Equal(typeof(long), DummyPolicy.Current.Custom?.First); + Assert.Equal("System.UInt32", DummyPolicy.Current.CustomStrings?.First); + } - Assert.Single(DummyRollingFileSink.Emitted); - } + [Fact] + public void SinkWithIntArrayArgument() + { + // language=json + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": [{ + "Name": "DummyRollingFile", + "Args": {"pathFormat" : "C:\\", + "intArrayBinding" : [ 1,2,3,4,5 ] } + }] + } + } + """; - [Trait("Bugfix", "#111")] - [Fact] - public void CaseInsensitiveArgumentNameMatching() - { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [{ - ""Name"": ""DummyRollingFile"", - ""Args"": {""PATHFORMAT"" : ""C:\\""} + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + + log.Write(Some.InformationEvent()); + + Assert.Single(DummyRollingFileSink.Emitted); + } + + [Trait("Bugfix", "#111")] + [Fact] + public void CaseInsensitiveArgumentNameMatching() + { + // language=json + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": [{ + "Name": "DummyRollingFile", + "Args": {"PATHFORMAT" : "C:\\"} }] } - }"; + } + """; - var log = ConfigFromJson(json) - .CreateLogger(); + var log = ConfigFromJson(json) + .CreateLogger(); - DummyRollingFileSink.Reset(); + DummyRollingFileSink.Reset(); - log.Write(Some.InformationEvent()); + log.Write(Some.InformationEvent()); - Assert.Single(DummyRollingFileSink.Emitted); - } + Assert.Single(DummyRollingFileSink.Emitted); + } - [Trait("Bugfix", "#91")] - [Fact] - public void WriteToLoggerWithRestrictedToMinimumLevelIsSupported() + [Trait("Bugfix", "#91")] + [Fact] + public void WriteToLoggerWithRestrictedToMinimumLevelIsSupported() + { + // language=json + var json = """ { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [{ - ""Name"": ""Logger"", - ""Args"": { - ""configureLogger"" : { - ""WriteTo"": [{ - ""Name"": ""DummyRollingFile"", - ""Args"": {""pathFormat"" : ""C:\\""} + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": [{ + "Name": "Logger", + "Args": { + "configureLogger" : { + "WriteTo": [{ + "Name": "DummyRollingFile", + "Args": {"pathFormat" : "C:\\"} }]}, - ""restrictedToMinimumLevel"": ""Warning"" + "restrictedToMinimumLevel": "Warning" } }] } - }"; + } + """; - var log = ConfigFromJson(json) - .CreateLogger(); + var log = ConfigFromJson(json) + .CreateLogger(); - DummyRollingFileSink.Reset(); + DummyRollingFileSink.Reset(); - log.Write(Some.InformationEvent()); - log.Write(Some.WarningEvent()); + log.Write(Some.InformationEvent()); + log.Write(Some.WarningEvent()); - Assert.Single(DummyRollingFileSink.Emitted); - } + Assert.Single(DummyRollingFileSink.Emitted); + } - [Trait("Bugfix", "#91")] - [Fact] - public void WriteToSubLoggerWithLevelSwitchIsSupported() + [Trait("Bugfix", "#91")] + [Fact] + public void WriteToSubLoggerWithLevelSwitchIsSupported() + { + // language=json + var json = """ { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""LevelSwitches"": {""$switch1"" : ""Warning"" }, - ""MinimumLevel"" : { - ""ControlledBy"" : ""$switch1"" + "Serilog": { + "Using": ["TestDummies"], + "LevelSwitches": {"$switch1" : "Warning" }, + "MinimumLevel" : { + "ControlledBy" : "$switch1" }, - ""WriteTo"": [{ - ""Name"": ""Logger"", - ""Args"": { - ""configureLogger"" : { - ""WriteTo"": [{ - ""Name"": ""DummyRollingFile"", - ""Args"": {""pathFormat"" : ""C:\\""} + "WriteTo": [{ + "Name": "Logger", + "Args": { + "configureLogger" : { + "WriteTo": [{ + "Name": "DummyRollingFile", + "Args": {"pathFormat" : "C:\\"} }]} } }] } - }"; + } + """; - var log = ConfigFromJson(json) - .CreateLogger(); + var log = ConfigFromJson(json) + .CreateLogger(); - DummyRollingFileSink.Reset(); + DummyRollingFileSink.Reset(); - log.Write(Some.InformationEvent()); - log.Write(Some.WarningEvent()); + log.Write(Some.InformationEvent()); + log.Write(Some.WarningEvent()); - Assert.Single(DummyRollingFileSink.Emitted); - } + Assert.Single(DummyRollingFileSink.Emitted); + } - [Trait("Bugfix", "#103")] - [Fact] - public void InconsistentComplexVsScalarArgumentValuesThrowsIOE() - { - var jsonDiscreteValue = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [{ - ""Name"": ""DummyRollingFile"", - ""Args"": {""pathFormat"" : ""C:\\""} + [Trait("Bugfix", "#103")] + [Fact] + public void InconsistentComplexVsScalarArgumentValuesThrowsIOE() + { + // language=json + var jsonDiscreteValue = """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": [{ + "Name": "DummyRollingFile", + "Args": {"pathFormat" : "C:\\"} }] } - }"; - - var jsonComplexValue = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [{ - ""Name"": ""DummyRollingFile"", - ""Args"": {""pathFormat"" : { ""foo"" : ""bar"" } } + } + """; + + // language=json + var jsonComplexValue = """ + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": [{ + "Name": "DummyRollingFile", + "Args": {"pathFormat" : { "foo" : "bar" } } }] } - }"; + } + """; - // These will combine into a ConfigurationSection object that has both - // Value == "C:\" and GetChildren() == List. No configuration - // extension matching this exists (in theory an "object" argument could - // accept either value). ConfigurationReader should throw as soon as - // the multiple values are recognized; it will never attempt to locate - // a matching argument. + // These will combine into a ConfigurationSection object that has both + // Value == "C:\" and GetChildren() == List. No configuration + // extension matching this exists (in theory an "object" argument could + // accept either value). ConfigurationReader should throw as soon as + // the multiple values are recognized; it will never attempt to locate + // a matching argument. - var ex = Assert.Throws(() - => ConfigFromJson(jsonDiscreteValue, jsonComplexValue)); + var ex = Assert.Throws(() + => ConfigFromJson(jsonDiscreteValue, jsonComplexValue)); - Assert.Contains("The value for the argument", ex.Message); - Assert.Contains("'Serilog:WriteTo:0:Args:pathFormat'", ex.Message); - } + Assert.Contains("The value for the argument", ex.Message); + Assert.Contains("'Serilog:WriteTo:0:Args:pathFormat'", ex.Message); + } - [Fact] - public void DestructureLimitsNestingDepth() - { - var json = @"{ - ""Serilog"": { - ""Destructure"": [ + [Fact] + public void DestructureLimitsNestingDepth() + { + // language=json + var json = """ + { + "Serilog": { + "Destructure": [ { - ""Name"": ""ToMaximumDepth"", - ""Args"": { ""maximumDestructuringDepth"": 3 } + "Name": "ToMaximumDepth", + "Args": { "maximumDestructuringDepth": 3 } }] } - }"; + } + """; - var NestedObject = new + var NestedObject = new + { + A = new { - A = new + B = new { - B = new + C = new { - C = new - { - D = "F" - } + D = "F" } } - }; + } + }; - var msg = GetDestructuredProperty(NestedObject, json); + var msg = GetDestructuredProperty(NestedObject, json); - Assert.Contains("C", msg); - Assert.DoesNotContain("D", msg); - } + Assert.Contains("C", msg); + Assert.DoesNotContain("D", msg); + } - [Fact] - public void DestructureLimitsStringLength() - { - var json = @"{ - ""Serilog"": { - ""Destructure"": [ + [Fact] + public void DestructureLimitsStringLength() + { + // language=json + var json = """ + { + "Serilog": { + "Destructure": [ { - ""Name"": ""ToMaximumStringLength"", - ""Args"": { ""maximumStringLength"": 3 } + "Name": "ToMaximumStringLength", + "Args": { "maximumStringLength": 3 } }] } - }"; + } + """; - var inputString = "ABCDEFGH"; - var msg = GetDestructuredProperty(inputString, json); + var inputString = "ABCDEFGH"; + var msg = GetDestructuredProperty(inputString, json); - Assert.Equal("\"AB…\"", msg); - } + Assert.Equal("\"AB…\"", msg); + } - [Fact] - public void DestructureLimitsCollectionCount() - { - var json = @"{ - ""Serilog"": { - ""Destructure"": [ + [Fact] + public void DestructureLimitsCollectionCount() + { + // language=json + var json = """ + { + "Serilog": { + "Destructure": [ { - ""Name"": ""ToMaximumCollectionCount"", - ""Args"": { ""maximumCollectionCount"": 3 } + "Name": "ToMaximumCollectionCount", + "Args": { "maximumCollectionCount": 3 } }] } - }"; + } + """; - var collection = new[] { 1, 2, 3, 4, 5, 6 }; - var msg = GetDestructuredProperty(collection, json); + var collection = new[] { 1, 2, 3, 4, 5, 6 }; + var msg = GetDestructuredProperty(collection, json); - Assert.Contains("3", msg); - Assert.DoesNotContain("4", msg); - } + Assert.Contains("3", msg); + Assert.DoesNotContain("4", msg); + } - private static string GetDestructuredProperty(object x, string json) - { - LogEvent evt = null; - var log = ConfigFromJson(json) - .WriteTo.Sink(new DelegatingSink(e => evt = e)) - .CreateLogger(); - log.Information("{@X}", x); - var result = evt.Properties["X"].ToString(); - return result; - } + private static string GetDestructuredProperty(object x, string json) + { + LogEvent? evt = null; + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + log.Information("{@X}", x); + var result = evt!.Properties["X"].ToString(); + return result; + } - [Fact] - public void DestructuringWithCustomExtensionMethodIsApplied() - { - var json = @"{ - ""Serilog"": { - ""Using"": [""TestDummies""], - ""Destructure"": [ + [Fact] + public void DestructuringWithCustomExtensionMethodIsApplied() + { + // language=json + var json = """ + { + "Serilog": { + "Using": ["TestDummies"], + "Destructure": [ { - ""Name"": ""WithDummyHardCodedString"", - ""Args"": { ""hardCodedString"": ""hardcoded"" } + "Name": "WithDummyHardCodedString", + "Args": { "hardCodedString": "hardcoded" } }] } - }"; + } + """; - LogEvent evt = null; - var log = ConfigFromJson(json) - .WriteTo.Sink(new DelegatingSink(e => evt = e)) - .CreateLogger(); - log.Information("Destructuring with hard-coded policy {@Input}", new { Foo = "Bar" }); - var formattedProperty = evt.Properties["Input"].ToString(); + LogEvent? evt = null; + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + log.Information("Destructuring with hard-coded policy {@Input}", new { Foo = "Bar" }); + var formattedProperty = evt?.Properties["Input"].ToString(); - Assert.Equal("\"hardcoded\"", formattedProperty); - } + Assert.Equal("\"hardcoded\"", formattedProperty); + } - [Fact] - public void DestructuringAsScalarIsAppliedWithShortTypeName() - { - var json = @"{ - ""Serilog"": { - ""Destructure"": [ + [Fact] + public void DestructuringAsScalarIsAppliedWithShortTypeName() + { + // language=json + var json = """ + { + "Serilog": { + "Destructure": [ { - ""Name"": ""AsScalar"", - ""Args"": { ""scalarType"": ""System.Version"" } + "Name": "AsScalar", + "Args": { "scalarType": "System.Version" } }] } - }"; + } + """; - LogEvent evt = null; - var log = ConfigFromJson(json) - .WriteTo.Sink(new DelegatingSink(e => evt = e)) - .CreateLogger(); + LogEvent? evt = null; + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); - log.Information("Destructuring as scalar {@Scalarized}", new Version(2, 3)); - var prop = evt.Properties["Scalarized"]; + log.Information("Destructuring as scalar {@Scalarized}", new Version(2, 3)); + var prop = evt?.Properties["Scalarized"]; - Assert.IsType(prop); - } + Assert.IsType(prop); + } - [Fact] - public void DestructuringAsScalarIsAppliedWithAssemblyQualifiedName() - { - var json = $@"{{ - ""Serilog"": {{ - ""Destructure"": [ - {{ - ""Name"": ""AsScalar"", - ""Args"": {{ ""scalarType"": ""{typeof(Version).AssemblyQualifiedName}"" }} - }}] - }} - }}"; - - LogEvent evt = null; - var log = ConfigFromJson(json) - .WriteTo.Sink(new DelegatingSink(e => evt = e)) - .CreateLogger(); - - log.Information("Destructuring as scalar {@Scalarized}", new Version(2, 3)); - var prop = evt.Properties["Scalarized"]; - - Assert.IsType(prop); - } + [Fact] + public void DestructuringAsScalarIsAppliedWithAssemblyQualifiedName() + { + // language=json + var json = $$""" + { + "Serilog": { + "Destructure": [ + { + "Name": "AsScalar", + "Args": { "scalarType": "{{typeof(Version).AssemblyQualifiedName}}" } + }] + } + } + """; - [Fact] - public void WriteToSinkIsAppliedWithCustomSink() - { - var json = $@"{{ - ""Serilog"": {{ - ""Using"": [""TestDummies""], - ""WriteTo"": [ - {{ - ""Name"": ""Sink"", - ""Args"": {{ - ""sink"": ""{typeof(DummyRollingFileSink).AssemblyQualifiedName}"" - }} - }}] - }} - }}"; - - var log = ConfigFromJson(json) - .CreateLogger(); - - DummyRollingFileSink.Reset(); - log.Write(Some.InformationEvent()); - - Assert.Single(DummyRollingFileSink.Emitted); - } + LogEvent? evt = null; + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); - [Fact] - public void WriteToSinkIsAppliedWithCustomSinkAndMinimumLevel() - { - var json = $@"{{ - ""Serilog"": {{ - ""Using"": [""TestDummies""], - ""WriteTo"": [ - {{ - ""Name"": ""Sink"", - ""Args"": {{ - ""sink"": ""{typeof(DummyRollingFileSink).AssemblyQualifiedName}"", - ""restrictedToMinimumLevel"": ""Warning"" - }} - }}] - }} - }}"; - - var log = ConfigFromJson(json) - .CreateLogger(); - - DummyRollingFileSink.Reset(); - log.Write(Some.InformationEvent()); - log.Write(Some.WarningEvent()); - - Assert.Single(DummyRollingFileSink.Emitted); - } + log.Information("Destructuring as scalar {@Scalarized}", new Version(2, 3)); + var prop = evt?.Properties["Scalarized"]; - [Fact] - public void WriteToSinkIsAppliedWithCustomSinkAndLevelSwitch() - { - var json = $@"{{ - ""Serilog"": {{ - ""Using"": [""TestDummies""], - ""LevelSwitches"": {{""$switch1"": ""Warning"" }}, - ""WriteTo"": [ - {{ - ""Name"": ""Sink"", - ""Args"": {{ - ""sink"": ""{typeof(DummyRollingFileSink).AssemblyQualifiedName}"", - ""levelSwitch"": ""$switch1"" - }} - }}] - }} - }}"; - - var log = ConfigFromJson(json) - .CreateLogger(); - - DummyRollingFileSink.Reset(); - log.Write(Some.InformationEvent()); - log.Write(Some.WarningEvent()); - - Assert.Single(DummyRollingFileSink.Emitted); - } + Assert.IsType(prop); + } - [Fact] - public void AuditToSinkIsAppliedWithCustomSink() - { - var json = $@"{{ - ""Serilog"": {{ - ""Using"": [""TestDummies""], - ""AuditTo"": [ - {{ - ""Name"": ""Sink"", - ""Args"": {{ - ""sink"": ""{typeof(DummyRollingFileSink).AssemblyQualifiedName}"" - }} - }}] - }} - }}"; - - var log = ConfigFromJson(json) - .CreateLogger(); - - DummyRollingFileSink.Reset(); - log.Write(Some.InformationEvent()); - - Assert.Single(DummyRollingFileSink.Emitted); - } + [Fact] + public void WriteToSinkIsAppliedWithCustomSink() + { + // language=json + var json = $$""" + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": [ + { + "Name": "Sink", + "Args": { + "sink": "{{typeof(DummyRollingFileSink).AssemblyQualifiedName}}" + } + }] + } + } + """; - [Fact] - public void AuditToSinkIsAppliedWithCustomSinkAndMinimumLevel() - { - var json = $@"{{ - ""Serilog"": {{ - ""Using"": [""TestDummies""], - ""AuditTo"": [ - {{ - ""Name"": ""Sink"", - ""Args"": {{ - ""sink"": ""{typeof(DummyRollingFileSink).AssemblyQualifiedName}"", - ""restrictedToMinimumLevel"": ""Warning"" - }} - }}] - }} - }}"; - - var log = ConfigFromJson(json) - .CreateLogger(); - - DummyRollingFileSink.Reset(); - log.Write(Some.InformationEvent()); - log.Write(Some.WarningEvent()); - - Assert.Single(DummyRollingFileSink.Emitted); - } + var log = ConfigFromJson(json) + .CreateLogger(); - [Fact] - public void AuditToSinkIsAppliedWithCustomSinkAndLevelSwitch() - { - var json = $@"{{ - ""Serilog"": {{ - ""Using"": [""TestDummies""], - ""LevelSwitches"": {{""$switch1"": ""Warning"" }}, - ""AuditTo"": [ - {{ - ""Name"": ""Sink"", - ""Args"": {{ - ""sink"": ""{typeof(DummyRollingFileSink).AssemblyQualifiedName}"", - ""levelSwitch"": ""$switch1"" - }} - }}] - }} - }}"; - - var log = ConfigFromJson(json) - .CreateLogger(); - - DummyRollingFileSink.Reset(); - log.Write(Some.InformationEvent()); - log.Write(Some.WarningEvent()); - - Assert.Single(DummyRollingFileSink.Emitted); - } + DummyRollingFileSink.Reset(); + log.Write(Some.InformationEvent()); - [Fact] - public void EnrichWithIsAppliedWithCustomEnricher() - { - LogEvent evt = null; - - var json = $@"{{ - ""Serilog"": {{ - ""Using"": [""TestDummies""], - ""Enrich"": [ - {{ - ""Name"": ""With"", - ""Args"": {{ - ""enricher"": ""{typeof(DummyThreadIdEnricher).AssemblyQualifiedName}"" - }} - }}] - }} - }}"; - - var log = ConfigFromJson(json) - .WriteTo.Sink(new DelegatingSink(e => evt = e)) - .CreateLogger(); - - log.Write(Some.InformationEvent()); - - Assert.NotNull(evt); - Assert.True(evt.Properties.ContainsKey("ThreadId"), "Event should have enriched property ThreadId"); - } + Assert.Single(DummyRollingFileSink.Emitted); + } - [Fact] - public void FilterWithIsAppliedWithCustomFilter() - { - LogEvent evt = null; - - var json = $@"{{ - ""Serilog"": {{ - ""Using"": [""TestDummies""], - ""Filter"": [ - {{ - ""Name"": ""With"", - ""Args"": {{ - ""filter"": ""{typeof(DummyAnonymousUserFilter).AssemblyQualifiedName}"" - }} - }}] - }} - }}"; - - var log = ConfigFromJson(json) - .WriteTo.Sink(new DelegatingSink(e => evt = e)) - .CreateLogger(); - - log.ForContext("User", "anonymous").Write(Some.InformationEvent()); - Assert.Null(evt); - log.ForContext("User", "the user").Write(Some.InformationEvent()); - Assert.NotNull(evt); - } + [Fact] + public void WriteToSinkIsAppliedWithCustomSinkAndMinimumLevel() + { + // language=json + var json = $$""" + { + "Serilog": { + "Using": ["TestDummies"], + "WriteTo": [ + { + "Name": "Sink", + "Args": { + "sink": "{{typeof(DummyRollingFileSink).AssemblyQualifiedName}}", + "restrictedToMinimumLevel": "Warning" + } + }] + } + } + """; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + log.Write(Some.InformationEvent()); + log.Write(Some.WarningEvent()); + + Assert.Single(DummyRollingFileSink.Emitted); + } + + [Fact] + public void WriteToSinkIsAppliedWithCustomSinkAndLevelSwitch() + { + // language=json + var json = $$""" + { + "Serilog": { + "Using": ["TestDummies"], + "LevelSwitches": {"$switch1": "Warning" }, + "WriteTo": [ + { + "Name": "Sink", + "Args": { + "sink": "{{typeof(DummyRollingFileSink).AssemblyQualifiedName}}", + "levelSwitch": "$switch1" + } + }] + } + } + """; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + log.Write(Some.InformationEvent()); + log.Write(Some.WarningEvent()); + + Assert.Single(DummyRollingFileSink.Emitted); + } + + [Fact] + public void AuditToSinkIsAppliedWithCustomSink() + { + // language=json + var json = $$""" + { + "Serilog": { + "Using": ["TestDummies"], + "AuditTo": [ + { + "Name": "Sink", + "Args": { + "sink": "{{typeof(DummyRollingFileSink).AssemblyQualifiedName}}" + } + }] + } + } + """; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + log.Write(Some.InformationEvent()); + + Assert.Single(DummyRollingFileSink.Emitted); + } + + [Fact] + public void AuditToSinkIsAppliedWithCustomSinkAndMinimumLevel() + { + // language=json + var json = $$""" + { + "Serilog": { + "Using": ["TestDummies"], + "AuditTo": [ + { + "Name": "Sink", + "Args": { + "sink": "{{typeof(DummyRollingFileSink).AssemblyQualifiedName}}", + "restrictedToMinimumLevel": "Warning" + } + }] + } + } + """; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + log.Write(Some.InformationEvent()); + log.Write(Some.WarningEvent()); + + Assert.Single(DummyRollingFileSink.Emitted); + } + + [Fact] + public void AuditToSinkIsAppliedWithCustomSinkAndLevelSwitch() + { + // language=json + var json = $$""" + { + "Serilog": { + "Using": ["TestDummies"], + "LevelSwitches": {"$switch1": "Warning" }, + "AuditTo": [ + { + "Name": "Sink", + "Args": { + "sink": "{{typeof(DummyRollingFileSink).AssemblyQualifiedName}}", + "levelSwitch": "$switch1" + } + }] + } + } + """; + + var log = ConfigFromJson(json) + .CreateLogger(); + + DummyRollingFileSink.Reset(); + log.Write(Some.InformationEvent()); + log.Write(Some.WarningEvent()); + + Assert.Single(DummyRollingFileSink.Emitted); + } + + [Fact] + public void EnrichWithIsAppliedWithCustomEnricher() + { + LogEvent? evt = null; + + // language=json + var json = $$""" + { + "Serilog": { + "Using": ["TestDummies"], + "Enrich": [ + { + "Name": "With", + "Args": { + "enricher": "{{typeof(DummyThreadIdEnricher).AssemblyQualifiedName}}" + } + }] + } + } + """; + + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + log.Write(Some.InformationEvent()); + + Assert.NotNull(evt); + Assert.True(evt.Properties.ContainsKey("ThreadId"), "Event should have enriched property ThreadId"); + } + + [Fact] + public void FilterWithIsAppliedWithCustomFilter() + { + LogEvent? evt = null; + + // language=json + var json = $$""" + { + "Serilog": { + "Using": ["TestDummies"], + "Filter": [ + { + "Name": "With", + "Args": { + "filter": "{{typeof(DummyAnonymousUserFilter).AssemblyQualifiedName}}" + } + }] + } + } + """; + + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + log.ForContext("User", "anonymous").Write(Some.InformationEvent()); + Assert.Null(evt); + log.ForContext("User", "the user").Write(Some.InformationEvent()); + Assert.NotNull(evt); + } + + [Theory] + [InlineData("$switch1")] + [InlineData("switch1")] + public void TestLogLevelSwitchesCallback(string switchName) + { + // language=json + var json = $$""" + { + "Serilog": { + "LevelSwitches": { "{{switchName}}": "Information" }, + "MinimumLevel": { + "Override": { + "System": "Warning", + "System.Threading": "Debug" + } + } + } + } + """; + + IDictionary switches = new Dictionary(); + var readerOptions = new ConfigurationReaderOptions { OnLevelSwitchCreated = (name, levelSwitch) => switches[name] = levelSwitch }; + ConfigFromJson(json, options: readerOptions); + + Assert.Equal(3, switches.Count); + + var switch1 = Assert.Contains("$switch1", switches); + Assert.Equal(LogEventLevel.Information, switch1.MinimumLevel); + + var system = Assert.Contains("System", switches); + Assert.Equal(LogEventLevel.Warning, system.MinimumLevel); + + var systemThreading = Assert.Contains("System.Threading", switches); + Assert.Equal(LogEventLevel.Debug, systemThreading.MinimumLevel); + } + + [Fact] + public void TestLogFilterSwitchesCallback() + { + // language=json + var json = """ + { + "Serilog": { + "FilterSwitches": { + "switch1": "Prop = 1", + "$switch2": "Prop = 2" + } + } + } + """; + + IDictionary switches = new Dictionary(); + var readerOptions = new ConfigurationReaderOptions { OnFilterSwitchCreated = (name, filterSwitch) => switches[name] = filterSwitch }; + ConfigFromJson(json, options: readerOptions); + + Assert.Equal(2, switches.Count); + + var switch1 = Assert.Contains("switch1", switches); + Assert.Equal("Prop = 1", switch1.Expression); + + var switch2 = Assert.Contains("$switch2", switches); + Assert.Equal("Prop = 2", switch2.Expression); } } diff --git a/test/Serilog.Settings.Configuration.Tests/DllScanningAssemblyFinderTests.cs b/test/Serilog.Settings.Configuration.Tests/DllScanningAssemblyFinderTests.cs index 050bf540..120ac3c6 100644 --- a/test/Serilog.Settings.Configuration.Tests/DllScanningAssemblyFinderTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/DllScanningAssemblyFinderTests.cs @@ -3,65 +3,62 @@ using System.IO; #endif -using Xunit; - using Serilog.Settings.Configuration.Assemblies; -namespace Serilog.Settings.Configuration.Tests +namespace Serilog.Settings.Configuration.Tests; + +public class DllScanningAssemblyFinderTests { - public class DllScanningAssemblyFinderTests - { - const string BinDir1 = "bin1"; - const string BinDir2 = "bin2"; - const string BinDir3 = "bin3"; + const string BinDir1 = "bin1"; + const string BinDir2 = "bin2"; + const string BinDir3 = "bin3"; - [Fact] - public void ShouldProbeCurrentDirectory() - { - var assemblyNames = new DllScanningAssemblyFinder().FindAssembliesContainingName("TestDummies"); - Assert.Single(assemblyNames); - } + [Fact] + public void ShouldProbeCurrentDirectory() + { + var assemblyNames = new DllScanningAssemblyFinder().FindAssembliesContainingName("TestDummies"); + Assert.Single(assemblyNames); + } #if NETFRAMEWORK - [Fact] - public void ShouldProbePrivateBinPath() - { - var d1 = GetOrCreateDirectory(BinDir1); - var d2 = GetOrCreateDirectory(BinDir2); - var d3 = GetOrCreateDirectory(BinDir3); - - DirectoryInfo GetOrCreateDirectory(string name) - => Directory.Exists(name) ? new DirectoryInfo(name) : Directory.CreateDirectory(name); + [Fact] + public void ShouldProbePrivateBinPath() + { + var d1 = GetOrCreateDirectory(BinDir1); + var d2 = GetOrCreateDirectory(BinDir2); + var d3 = GetOrCreateDirectory(BinDir3); - File.Copy("TestDummies.dll", $"{BinDir1}/customSink1.dll", true); - File.Copy("TestDummies.dll", $"{BinDir2}/customSink2.dll", true); - File.Copy("TestDummies.dll", $"{BinDir3}/thirdpartydependency.dll", true); + DirectoryInfo GetOrCreateDirectory(string name) + => Directory.Exists(name) ? new DirectoryInfo(name) : Directory.CreateDirectory(name); - var ad = AppDomain.CreateDomain("serilog", null, - new AppDomainSetup - { - ApplicationBase = AppDomain.CurrentDomain.BaseDirectory, - PrivateBinPath = $"{d1.Name};{d2.FullName};{d3.Name}" - }); + File.Copy("TestDummies.dll", $"{BinDir1}/customSink1.dll", true); + File.Copy("TestDummies.dll", $"{BinDir2}/customSink2.dll", true); + File.Copy("TestDummies.dll", $"{BinDir3}/thirdpartydependency.dll", true); - try + var ad = AppDomain.CreateDomain("serilog", null, + new AppDomainSetup { - ad.DoCallBack(DoTestInner); - } - finally - { - AppDomain.Unload(ad); - Directory.Delete(BinDir1, true); - Directory.Delete(BinDir2, true); - Directory.Delete(BinDir3, true); - } + ApplicationBase = AppDomain.CurrentDomain.BaseDirectory, + PrivateBinPath = $"{d1.Name};{d2.FullName};{d3.Name}" + }); - static void DoTestInner() - { - var assemblyNames = new DllScanningAssemblyFinder().FindAssembliesContainingName("customSink"); - Assert.Equal(2, assemblyNames.Count); - } + try + { + ad.DoCallBack(DoTestInner); + } + finally + { + AppDomain.Unload(ad); + Directory.Delete(BinDir1, true); + Directory.Delete(BinDir2, true); + Directory.Delete(BinDir3, true); + } + + static void DoTestInner() + { + var assemblyNames = new DllScanningAssemblyFinder().FindAssembliesContainingName("customSink"); + Assert.Equal(2, assemblyNames.Count); } -#endif } +#endif } diff --git a/test/Serilog.Settings.Configuration.Tests/DummyLoggerConfigurationExtensions.cs b/test/Serilog.Settings.Configuration.Tests/DummyLoggerConfigurationExtensions.cs index 96754464..c8a00d66 100644 --- a/test/Serilog.Settings.Configuration.Tests/DummyLoggerConfigurationExtensions.cs +++ b/test/Serilog.Settings.Configuration.Tests/DummyLoggerConfigurationExtensions.cs @@ -1,29 +1,27 @@ -using System; -using Serilog.Configuration; +using Serilog.Configuration; using Serilog.Events; using Serilog.Formatting; -namespace Serilog.Settings.Configuration.Tests +namespace Serilog.Settings.Configuration.Tests; + +static class DummyLoggerConfigurationExtensions { - static class DummyLoggerConfigurationExtensions + public static LoggerConfiguration? DummyRollingFile( + LoggerSinkConfiguration loggerSinkConfiguration, + string pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + string? outputTemplate = null, + IFormatProvider? formatProvider = null) { - public static LoggerConfiguration DummyRollingFile( - LoggerSinkConfiguration loggerSinkConfiguration, - string pathFormat, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - string outputTemplate = null, - IFormatProvider formatProvider = null) - { - return null; - } + return null; + } - public static LoggerConfiguration DummyRollingFile( - LoggerSinkConfiguration loggerSinkConfiguration, - ITextFormatter formatter, - string pathFormat, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) - { - return null; - } + public static LoggerConfiguration? DummyRollingFile( + LoggerSinkConfiguration loggerSinkConfiguration, + ITextFormatter formatter, + string pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) + { + return null; } } diff --git a/test/Serilog.Settings.Configuration.Tests/DummyLoggerConfigurationWithMultipleMethodsExtensions.cs b/test/Serilog.Settings.Configuration.Tests/DummyLoggerConfigurationWithMultipleMethodsExtensions.cs index b6be5706..1ad5c222 100644 --- a/test/Serilog.Settings.Configuration.Tests/DummyLoggerConfigurationWithMultipleMethodsExtensions.cs +++ b/test/Serilog.Settings.Configuration.Tests/DummyLoggerConfigurationWithMultipleMethodsExtensions.cs @@ -1,42 +1,40 @@ -using System; -using Serilog.Configuration; +using Serilog.Configuration; using Serilog.Events; using Serilog.Formatting; -namespace Serilog.Settings.Configuration.Tests -{ - using System.Collections.Generic; +namespace Serilog.Settings.Configuration.Tests; + +using System.Collections.Generic; - static class DummyLoggerConfigurationWithMultipleMethodsExtensions +static class DummyLoggerConfigurationWithMultipleMethodsExtensions +{ + public static LoggerConfiguration? DummyRollingFile( + LoggerSinkConfiguration loggerSinkConfiguration, + ITextFormatter formatter, + IEnumerable pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + string? outputTemplate = null, + IFormatProvider? formatProvider = null) { - public static LoggerConfiguration DummyRollingFile( - LoggerSinkConfiguration loggerSinkConfiguration, - ITextFormatter formatter, - IEnumerable pathFormat, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - string outputTemplate = null, - IFormatProvider formatProvider = null) - { - return null; - } + return null; + } - public static LoggerConfiguration DummyRollingFile( - LoggerSinkConfiguration loggerSinkConfiguration, - ITextFormatter formatter, - string pathFormat, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - string outputTemplate = null, - IFormatProvider formatProvider = null) - { - return null; - } + public static LoggerConfiguration? DummyRollingFile( + LoggerSinkConfiguration loggerSinkConfiguration, + ITextFormatter formatter, + string pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + string? outputTemplate = null, + IFormatProvider? formatProvider = null) + { + return null; + } - public static LoggerConfiguration DummyRollingFile( - LoggerSinkConfiguration loggerSinkConfiguration, - string pathFormat, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) - { - return null; - } + public static LoggerConfiguration? DummyRollingFile( + LoggerSinkConfiguration loggerSinkConfiguration, + string pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) + { + return null; } } diff --git a/test/Serilog.Settings.Configuration.Tests/DynamicLevelChangeTests.cs b/test/Serilog.Settings.Configuration.Tests/DynamicLevelChangeTests.cs index 6b439f6e..4cc743a5 100644 --- a/test/Serilog.Settings.Configuration.Tests/DynamicLevelChangeTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/DynamicLevelChangeTests.cs @@ -1,114 +1,113 @@ using Serilog.Core; using Serilog.Events; using Serilog.Settings.Configuration.Tests.Support; - -using Xunit; using Microsoft.Extensions.Configuration; using TestDummies.Console; -namespace Serilog.Settings.Configuration.Tests +namespace Serilog.Settings.Configuration.Tests; + +public class DynamicLevelChangeTests { - public class DynamicLevelChangeTests - { - const string DefaultConfig = @"{ - 'Serilog': { - 'Using': [ 'TestDummies' ], - 'MinimumLevel': { - 'Default': 'Information', - 'Override': { - 'Root.Test': 'Information' + // language=json + const string DefaultConfig = """ + { + "Serilog": { + "Using": [ "TestDummies" ], + "MinimumLevel": { + "Default": "Information", + "Override": { + "Root.Test": "Information" } }, - 'LevelSwitches': { '$mySwitch': 'Information' }, - 'FilterSwitches': { '$myFilter': null }, - 'Filter:Dummy': { - 'Name': 'ControlledBy', - 'Args': { - 'switch': '$myFilter' + "LevelSwitches": { "$mySwitch": "Information" }, + "FilterSwitches": { "$myFilter": null }, + "Filter:Dummy": { + "Name": "ControlledBy", + "Args": { + "switch": "$myFilter" } }, - 'WriteTo:Dummy': { - 'Name': 'DummyConsole', - 'Args': { - 'levelSwitch': '$mySwitch' + "WriteTo:Dummy": { + "Name": "DummyConsole", + "Args": { + "levelSwitch": "$mySwitch" } } } - }"; + } + """; + + readonly ReloadableConfigurationSource _configSource; - readonly ReloadableConfigurationSource _configSource; + public DynamicLevelChangeTests() + { + _configSource = new ReloadableConfigurationSource(JsonStringConfigSource.LoadData(DefaultConfig)); + } + + [Fact] + public void ShouldRespectDynamicLevelChanges() + { + using var logger = new LoggerConfiguration() + .ReadFrom + .Configuration(new ConfigurationBuilder().Add(_configSource).Build()) + .CreateLogger(); + + DummyConsoleSink.Emitted.Clear(); + logger.Write(Some.DebugEvent()); + Assert.Empty(DummyConsoleSink.Emitted); + + DummyConsoleSink.Emitted.Clear(); + UpdateConfig(minimumLevel: LogEventLevel.Debug); + logger.Write(Some.DebugEvent()); + Assert.Empty(DummyConsoleSink.Emitted); + + DummyConsoleSink.Emitted.Clear(); + UpdateConfig(switchLevel: LogEventLevel.Debug); + logger.Write(Some.DebugEvent()); + logger.ForContext(Constants.SourceContextPropertyName, "Root.Test").Write(Some.DebugEvent()); + Assert.Single(DummyConsoleSink.Emitted); + + DummyConsoleSink.Emitted.Clear(); + UpdateConfig(overrideLevel: LogEventLevel.Debug); + logger.ForContext(Constants.SourceContextPropertyName, "Root.Test").Write(Some.DebugEvent()); + Assert.Single(DummyConsoleSink.Emitted); + + DummyConsoleSink.Emitted.Clear(); + UpdateConfig(filterExpression: "Prop = 'Val_1'"); + logger.Write(Some.DebugEvent()); + logger.ForContext("Prop", "Val_1").Write(Some.DebugEvent()); + Assert.Single(DummyConsoleSink.Emitted); + + DummyConsoleSink.Emitted.Clear(); + UpdateConfig(filterExpression: "Prop = 'Val_2'"); + logger.Write(Some.DebugEvent()); + logger.ForContext("Prop", "Val_1").Write(Some.DebugEvent()); + Assert.Empty(DummyConsoleSink.Emitted); + } - public DynamicLevelChangeTests() + void UpdateConfig(LogEventLevel? minimumLevel = null, LogEventLevel? switchLevel = null, LogEventLevel? overrideLevel = null, string? filterExpression = null) + { + if (minimumLevel.HasValue) { - _configSource = new ReloadableConfigurationSource(JsonStringConfigSource.LoadData(DefaultConfig)); + _configSource.Set("Serilog:MinimumLevel:Default", minimumLevel.Value.ToString()); } - [Fact] - public void ShouldRespectDynamicLevelChanges() + if (switchLevel.HasValue) { - using (var logger = new LoggerConfiguration() - .ReadFrom - .Configuration(new ConfigurationBuilder().Add(_configSource).Build()) - .CreateLogger()) - { - DummyConsoleSink.Emitted.Clear(); - logger.Write(Some.DebugEvent()); - Assert.Empty(DummyConsoleSink.Emitted); - - DummyConsoleSink.Emitted.Clear(); - UpdateConfig(minimumLevel: LogEventLevel.Debug); - logger.Write(Some.DebugEvent()); - Assert.Empty(DummyConsoleSink.Emitted); - - DummyConsoleSink.Emitted.Clear(); - UpdateConfig(switchLevel: LogEventLevel.Debug); - logger.Write(Some.DebugEvent()); - logger.ForContext(Constants.SourceContextPropertyName, "Root.Test").Write(Some.DebugEvent()); - Assert.Single(DummyConsoleSink.Emitted); - - DummyConsoleSink.Emitted.Clear(); - UpdateConfig(overrideLevel: LogEventLevel.Debug); - logger.ForContext(Constants.SourceContextPropertyName, "Root.Test").Write(Some.DebugEvent()); - Assert.Single(DummyConsoleSink.Emitted); - - DummyConsoleSink.Emitted.Clear(); - UpdateConfig(filterExpression: "Prop = 'Val_1'"); - logger.Write(Some.DebugEvent()); - logger.ForContext("Prop", "Val_1").Write(Some.DebugEvent()); - Assert.Single(DummyConsoleSink.Emitted); - - DummyConsoleSink.Emitted.Clear(); - UpdateConfig(filterExpression: "Prop = 'Val_2'"); - logger.Write(Some.DebugEvent()); - logger.ForContext("Prop", "Val_1").Write(Some.DebugEvent()); - Assert.Empty(DummyConsoleSink.Emitted); - } + _configSource.Set("Serilog:LevelSwitches:$mySwitch", switchLevel.Value.ToString()); } - void UpdateConfig(LogEventLevel? minimumLevel = null, LogEventLevel? switchLevel = null, LogEventLevel? overrideLevel = null, string filterExpression = null) + if (overrideLevel.HasValue) { - if (minimumLevel.HasValue) - { - _configSource.Set("Serilog:MinimumLevel:Default", minimumLevel.Value.ToString()); - } - - if (switchLevel.HasValue) - { - _configSource.Set("Serilog:LevelSwitches:$mySwitch", switchLevel.Value.ToString()); - } - - if (overrideLevel.HasValue) - { - _configSource.Set("Serilog:MinimumLevel:Override:Root.Test", overrideLevel.Value.ToString()); - } - - if (filterExpression != null) - { - _configSource.Set("Serilog:FilterSwitches:$myFilter", filterExpression); - } + _configSource.Set("Serilog:MinimumLevel:Override:Root.Test", overrideLevel.Value.ToString()); + } - _configSource.Reload(); + if (filterExpression != null) + { + _configSource.Set("Serilog:FilterSwitches:$myFilter", filterExpression); } + + _configSource.Reload(); } } diff --git a/test/Serilog.Settings.Configuration.Tests/LoggerConfigurationExtensionsTests.cs b/test/Serilog.Settings.Configuration.Tests/LoggerConfigurationExtensionsTests.cs index f7eee44f..39a0df9c 100644 --- a/test/Serilog.Settings.Configuration.Tests/LoggerConfigurationExtensionsTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/LoggerConfigurationExtensionsTests.cs @@ -1,133 +1,142 @@ -using System; using Microsoft.Extensions.Configuration; using Serilog.Events; using Serilog.Settings.Configuration.Tests.Support; -using Xunit; -namespace Serilog.Settings.Configuration.Tests +namespace Serilog.Settings.Configuration.Tests; + +public class LoggerConfigurationExtensionsTests { - public class LoggerConfigurationExtensionsTests + [Fact] + public void ReadFromConfigurationShouldNotThrowOnEmptyConfiguration() + { + Action act = () => new LoggerConfiguration().ReadFrom.Configuration(new ConfigurationBuilder().Build()); + + // should not throw + act(); + } + + [Fact] + [Trait("BugFix", "https://github.com/serilog/serilog-settings-configuration/issues/143")] + public void ReadFromConfigurationSectionReadsFromAnArbitrarySection() { - [Fact] - public void ReadFromConfigurationShouldNotThrowOnEmptyConfiguration() - { - Action act = () => new LoggerConfiguration().ReadFrom.Configuration(new ConfigurationBuilder().Build()); - - // should not throw - act(); - } - - [Fact] - [Trait("BugFix", "https://github.com/serilog/serilog-settings-configuration/issues/143")] - public void ReadFromConfigurationSectionReadsFromAnArbitrarySection() - { - LogEvent evt = null; - - var json = @"{ - ""NotSerilog"": { - ""Properties"": { - ""App"": ""Test"" - } - } - }"; - - var config = new ConfigurationBuilder() - .AddJsonString(json) - .Build(); + LogEvent? evt = null; + + // language=json + var json = """ + { + "NotSerilog": { + "Properties": { + "App": "Test" + } + } + } + """; + + var config = new ConfigurationBuilder() + .AddJsonString(json) + .Build(); #pragma warning disable CS0618 // Type or member is obsolete - var log = new LoggerConfiguration() - .ReadFrom.ConfigurationSection(config.GetSection("NotSerilog")) + var log = new LoggerConfiguration() + .ReadFrom.ConfigurationSection(config.GetSection("NotSerilog")) #pragma warning restore CS0618 // Type or member is obsolete - .WriteTo.Sink(new DelegatingSink(e => evt = e)) - .CreateLogger(); - - log.Information("Has a test property"); - - Assert.NotNull(evt); - Assert.Equal("Test", evt.Properties["App"].LiteralValue()); - } - - [Fact] - [Trait("BugFix", "https://github.com/serilog/serilog-settings-configuration/issues/143")] - public void ReadFromConfigurationSectionThrowsWhenTryingToCallConfigurationMethodWithIConfigurationParam() - { - var json = @"{ - ""NotSerilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [{ - ""Name"": ""DummyWithConfiguration"", - ""Args"": {} + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + log.Information("Has a test property"); + + Assert.NotNull(evt); + Assert.Equal("Test", evt?.Properties["App"].LiteralValue()); + } + + [Fact] + [Trait("BugFix", "https://github.com/serilog/serilog-settings-configuration/issues/143")] + public void ReadFromConfigurationSectionThrowsWhenTryingToCallConfigurationMethodWithIConfigurationParam() + { + // language=json + var json = """ + { + "NotSerilog": { + "Using": ["TestDummies"], + "WriteTo": [{ + "Name": "DummyWithConfiguration", + "Args": {} }] } - }"; + } + """; - var config = new ConfigurationBuilder() - .AddJsonString(json) - .Build(); + var config = new ConfigurationBuilder() + .AddJsonString(json) + .Build(); - var exception = Assert.Throws(() => + var exception = Assert.Throws(() => #pragma warning disable CS0618 // Type or member is obsolete - new LoggerConfiguration() - .ReadFrom.ConfigurationSection(config.GetSection("NotSerilog")) + new LoggerConfiguration() + .ReadFrom.ConfigurationSection(config.GetSection("NotSerilog")) #pragma warning restore CS0618 // Type or member is obsolete - .CreateLogger()); - - Assert.Equal("Trying to invoke a configuration method accepting a `IConfiguration` argument. " + - "This is not supported when only a `IConfigSection` has been provided. " + - "(method 'Serilog.LoggerConfiguration DummyWithConfiguration(Serilog.Configuration.LoggerSinkConfiguration, Microsoft.Extensions.Configuration.IConfiguration, Serilog.Events.LogEventLevel)')", - exception.Message); - - } - - [Fact] - public void ReadFromConfigurationDoesNotThrowWhenTryingToCallConfigurationMethodWithIConfigurationParam() - { - var json = @"{ - ""NotSerilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [{ - ""Name"": ""DummyWithConfiguration"", - ""Args"": {} + .CreateLogger()); + + Assert.Equal("Trying to invoke a configuration method accepting a `IConfiguration` argument. " + + "This is not supported when only a `IConfigSection` has been provided. " + + "(method 'Serilog.LoggerConfiguration DummyWithConfiguration(Serilog.Configuration.LoggerSinkConfiguration, Microsoft.Extensions.Configuration.IConfiguration, Serilog.Events.LogEventLevel)')", + exception.Message); + + } + + [Fact] + public void ReadFromConfigurationDoesNotThrowWhenTryingToCallConfigurationMethodWithIConfigurationParam() + { + // language=json + var json = """ + { + "NotSerilog": { + "Using": ["TestDummies"], + "WriteTo": [{ + "Name": "DummyWithConfiguration", + "Args": {} }] } - }"; - - var config = new ConfigurationBuilder() - .AddJsonString(json) - .Build(); - - _ = new LoggerConfiguration() - .ReadFrom.Configuration(config, "NotSerilog") - .CreateLogger(); - - } - - [Fact] - [Trait("BugFix", "https://github.com/serilog/serilog-settings-configuration/issues/143")] - public void ReadFromConfigurationSectionDoesNotThrowWhenTryingToCallConfigurationMethodWithOptionalIConfigurationParam() - { - var json = @"{ - ""NotSerilog"": { - ""Using"": [""TestDummies""], - ""WriteTo"": [{ - ""Name"": ""DummyWithOptionalConfiguration"", - ""Args"": {} + } + """; + + var config = new ConfigurationBuilder() + .AddJsonString(json) + .Build(); + + _ = new LoggerConfiguration() + .ReadFrom.Configuration(config, new ConfigurationReaderOptions { SectionName = "NotSerilog" }) + .CreateLogger(); + + } + + [Fact] + [Trait("BugFix", "https://github.com/serilog/serilog-settings-configuration/issues/143")] + public void ReadFromConfigurationSectionDoesNotThrowWhenTryingToCallConfigurationMethodWithOptionalIConfigurationParam() + { + // language=json + var json = """ + { + "NotSerilog": { + "Using": ["TestDummies"], + "WriteTo": [{ + "Name": "DummyWithOptionalConfiguration", + "Args": {} }] } - }"; + } + """; - var config = new ConfigurationBuilder() - .AddJsonString(json) - .Build(); + var config = new ConfigurationBuilder() + .AddJsonString(json) + .Build(); - // this should not throw because DummyWithOptionalConfiguration accepts an optional config + // this should not throw because DummyWithOptionalConfiguration accepts an optional config #pragma warning disable CS0618 // Type or member is obsolete - new LoggerConfiguration() - .ReadFrom.ConfigurationSection(config.GetSection("NotSerilog")) + new LoggerConfiguration() + .ReadFrom.ConfigurationSection(config.GetSection("NotSerilog")) #pragma warning restore CS0618 // Type or member is obsolete - .CreateLogger(); + .CreateLogger(); - } } } diff --git a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs index 9772424e..ee775e4e 100644 --- a/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ObjectArgumentValueTests.cs @@ -1,72 +1,67 @@ -using System; - -using Microsoft.Extensions.Configuration; - -using Xunit; +using Microsoft.Extensions.Configuration; // ReSharper disable UnusedMember.Local // ReSharper disable UnusedParameter.Local // ReSharper disable UnusedType.Local -namespace Serilog.Settings.Configuration.Tests +namespace Serilog.Settings.Configuration.Tests; + +public class ObjectArgumentValueTests { - public class ObjectArgumentValueTests - { - readonly IConfigurationRoot _config; + readonly IConfigurationRoot _config; - public ObjectArgumentValueTests() - { - _config = new ConfigurationBuilder() - .AddJsonFile("ObjectArgumentValueTests.json") - .Build(); - } + public ObjectArgumentValueTests() + { + _config = new ConfigurationBuilder() + .AddJsonFile("ObjectArgumentValueTests.json") + .Build(); + } - [Theory] - [InlineData("case_1", typeof(A), "new A(1, 23:59:59, http://dot.com/, \"d\")")] - [InlineData("case_2", typeof(B), "new B(2, new A(3, new D()), null)")] - [InlineData("case_3", typeof(E), "new E(\"1\", \"2\", \"3\")")] - [InlineData("case_4", typeof(F), "new F(\"paramType\", new E(1, 2, 3, 4))")] - [InlineData("case_5", typeof(G), "new G()")] - [InlineData("case_6", typeof(G), "new G(3, 4)")] - public void ShouldBindToConstructorArguments(string caseSection, Type targetType, string expectedExpression) - { - var testSection = _config.GetSection(caseSection); + [Theory] + [InlineData("case_1", typeof(A), "new A(1, 23:59:59, http://dot.com/, \"d\")")] + [InlineData("case_2", typeof(B), "new B(2, new A(3, new D()), null)")] + [InlineData("case_3", typeof(E), "new E(\"1\", \"2\", \"3\")")] + [InlineData("case_4", typeof(F), "new F(\"paramType\", new E(1, 2, 3, 4))")] + [InlineData("case_5", typeof(G), "new G()")] + [InlineData("case_6", typeof(G), "new G(3, 4)")] + public void ShouldBindToConstructorArguments(string caseSection, Type targetType, string expectedExpression) + { + var testSection = _config.GetSection(caseSection); - Assert.True(ObjectArgumentValue.TryBuildCtorExpression(testSection, targetType, new(), out var ctorExpression)); - Assert.Equal(expectedExpression, ctorExpression.ToString()); - } + Assert.True(ObjectArgumentValue.TryBuildCtorExpression(testSection, targetType, new(), out var ctorExpression)); + Assert.Equal(expectedExpression, ctorExpression.ToString()); + } - class A - { - public A(int a, TimeSpan b, Uri c, string d = "d") { } - public A(int a, C c) { } - } + class A + { + public A(int a, TimeSpan b, Uri c, string d = "d") { } + public A(int a, C c) { } + } - class B - { - public B(int b, A a, long? c = null) { } - } + class B + { + public B(int b, A a, long? c = null) { } + } - interface C { } + interface C { } - class D : C { } + class D : C { } - class E - { - public E(int a, int b, int c, int d = 4) { } - public E(int a, string b, string c) { } - public E(string a, string b, string c) { } - } + class E + { + public E(int a, int b, int c, int d = 4) { } + public E(int a, string b, string c) { } + public E(string a, string b, string c) { } + } - class F - { - public F(string type, E e) { } - } + class F + { + public F(string type, E e) { } + } - class G - { - public G() { } - public G(int a = 1, int b = 2) { } - } + class G + { + public G() { } + public G(int a = 1, int b = 2) { } } } diff --git a/test/Serilog.Settings.Configuration.Tests/PublishSingleFileTests.cs b/test/Serilog.Settings.Configuration.Tests/PublishSingleFileTests.cs new file mode 100644 index 00000000..2197e78c --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/PublishSingleFileTests.cs @@ -0,0 +1,158 @@ +using System.Diagnostics; +using System.Text; +using CliWrap; +using CliWrap.Exceptions; +using FluentAssertions; +using FluentAssertions.Execution; +using Serilog.Settings.Configuration.Tests.Support; +using Xunit.Abstractions; + +namespace Serilog.Settings.Configuration.Tests; + +[Trait("Category", "Integration")] +public sealed class PublishSingleFileTests : IDisposable, IClassFixture +{ + readonly ITestOutputHelper _outputHelper; + readonly TestApp _testApp; + readonly AssertionScope _scope; + + public PublishSingleFileTests(ITestOutputHelper outputHelper, TestApp testApp) + { + _outputHelper = outputHelper; + _testApp = testApp; + _scope = new AssertionScope(); + } + + public void Dispose() + { + _scope.Dispose(); + } + + [Theory] + [ClassData(typeof(PublishModeTheoryData))] + public async Task RunTestApp_NoUsingAndNoAssembly(PublishMode publishMode) + { + var (isSingleFile, stdOut, stdErr) = await RunTestAppAsync(publishMode); + stdOut.Should().Be(isSingleFile ? "Expected exception" : "(Main thread) [Information] Expected success"); + stdErr.Should().BeEmpty(); + } + + [Theory] + [ClassData(typeof(PublishModeTheoryData))] + public async Task RunTestApp_UsingConsole(PublishMode publishMode) + { + var (isSingleFile, stdOut, stdErr) = await RunTestAppAsync(publishMode, "--using-console"); + stdOut.Should().Be(isSingleFile ? "() [Information] Expected success" : "(Main thread) [Information] Expected success"); + if (isSingleFile) + stdErr.Should().Contain("Unable to find a method called WithThreadName"); + else + stdErr.Should().BeEmpty(); + } + + [Theory] + [ClassData(typeof(PublishModeTheoryData))] + public async Task RunTestApp_UsingThread(PublishMode publishMode) + { + var (isSingleFile, stdOut, stdErr) = await RunTestAppAsync(publishMode, "--using-thread"); + stdOut.Should().Be(isSingleFile ? "" : "(Main thread) [Information] Expected success"); + if (isSingleFile) + stdErr.Should().Contain("Unable to find a method called Console"); + else + stdErr.Should().BeEmpty(); + } + + [Theory] + [ClassData(typeof(PublishModeTheoryData))] + public async Task RunTestApp_AssemblyThread(PublishMode publishMode) + { + var (_, stdOut, stdErr) = await RunTestAppAsync(publishMode, "--assembly-thread"); + stdOut.Should().BeEmpty(); + stdErr.Should().Contain("Unable to find a method called Console"); + } + + [Theory] + [ClassData(typeof(PublishModeTheoryData))] + public async Task RunTestApp_AssemblyConsole(PublishMode publishMode) + { + var (_, stdOut, stdErr) = await RunTestAppAsync(publishMode, "--assembly-console"); + stdOut.Should().Be("() [Information] Expected success"); + stdErr.Should().Contain("Unable to find a method called WithThreadName"); + } + + [Theory] + [ClassData(typeof(PublishModeAndStrategyTheoryData))] + public async Task RunTestApp_ConsoleAndThread(PublishMode publishMode, string strategy) + { + var (_, stdOut, stdErr) = await RunTestAppAsync(publishMode, $"--{strategy}-console", $"--{strategy}-thread"); + stdOut.Should().Be("(Main thread) [Information] Expected success"); + stdErr.Should().BeEmpty(); + } + + async Task<(bool IsSingleFile, string StdOut, string StdErr)> RunTestAppAsync(PublishMode publishMode, params string[] args) + { + // Determine whether the app is a _true_ single file, i.e. not a .NET Core 3.x version which + // [extracts bundled files to disk][1] and thus can find dlls. + // [1]: https://github.com/dotnet/designs/blob/main/accepted/2020/single-file/extract.md + var (isSingleFile, _) = await RunTestAppInternalAsync(publishMode, "is-single-file"); + var (stdOut, stdErr) = await RunTestAppInternalAsync(publishMode, args); + return (bool.Parse(isSingleFile), stdOut, stdErr); + } + + async Task<(string StdOut, string StdErr)> RunTestAppInternalAsync(PublishMode publishMode, params string[] args) + { + var stdOutBuilder = new StringBuilder(); + var stdErrBuilder = new StringBuilder(); + + var command = Cli.Wrap(_testApp.GetExecutablePath(publishMode)) + .WithArguments(args) + .WithValidation(CommandResultValidation.None) + .WithStandardOutputPipe(PipeTarget.ToStringBuilder(stdOutBuilder)) + .WithStandardErrorPipe(PipeTarget.ToStringBuilder(stdErrBuilder)); + + _outputHelper.WriteLine(command.ToString()); + + var stopwatch = Stopwatch.StartNew(); + var result = await command.ExecuteAsync(); + var executionTime = stopwatch.ElapsedMilliseconds; + + var stdOut = stdOutBuilder.ToString().Trim(); + var stdErr = stdErrBuilder.ToString().Trim(); + + _outputHelper.WriteLine($"Executed in {executionTime} ms"); + _outputHelper.WriteLine(stdOut.Length > 0 ? $"stdout: {stdOut}" : "nothing on stdout"); + _outputHelper.WriteLine(stdErr.Length > 0 ? $"stderr: {stdErr}" : "nothing on stderr"); + _outputHelper.WriteLine(""); + + if (result.ExitCode != 0) + { + throw new CommandExecutionException(command, result.ExitCode, $"An unexpected exception has occurred while running {command}{Environment.NewLine}{stdErr}".Trim()); + } + + return (stdOut, stdErr); + } + + class PublishModeTheoryData : TheoryData + { + public PublishModeTheoryData() + { + foreach (var publishMode in PublishModeExtensions.GetPublishModes()) + { + Add(publishMode); + } + } + } + + class PublishModeAndStrategyTheoryData : TheoryData + { + public PublishModeAndStrategyTheoryData() + { + foreach (var publishMode in PublishModeExtensions.GetPublishModes()) + { + foreach (var strategy in new[] { "using", "assembly" }) + { + Add(publishMode, strategy); + } + } + } + } +} 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 1453c836..f1915c1c 100644 --- a/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj +++ b/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj @@ -1,12 +1,8 @@ - + - net6.0;netcoreapp3.1;net462 - latest - Serilog.Settings.Configuration.Tests - ../../assets/Serilog.snk - true - true + net48 + $(TargetFrameworks);net7.0;net6.0 @@ -21,11 +17,20 @@ - - - + + + + + + - + + + + + + + diff --git a/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.approved.txt b/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.approved.txt new file mode 100644 index 00000000..ca43b43a --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.approved.txt @@ -0,0 +1,57 @@ +namespace Serilog +{ + public static class ConfigurationLoggerConfigurationExtensions + { + public const string DefaultSectionName = "Serilog"; + [System.Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, ConfigurationReaderOptio" + + "ns readerOptions) instead.")] + public static Serilog.LoggerConfiguration Configuration(this Serilog.Configuration.LoggerSettingsConfiguration settingConfiguration, Microsoft.Extensions.Configuration.IConfiguration configuration, Microsoft.Extensions.DependencyModel.DependencyContext dependencyContext) { } + [System.Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, ConfigurationReaderOptio" + + "ns readerOptions) instead.")] + public static Serilog.LoggerConfiguration Configuration(this Serilog.Configuration.LoggerSettingsConfiguration settingConfiguration, Microsoft.Extensions.Configuration.IConfiguration configuration, Serilog.Settings.Configuration.ConfigurationAssemblySource configurationAssemblySource) { } + public static Serilog.LoggerConfiguration Configuration(this Serilog.Configuration.LoggerSettingsConfiguration settingConfiguration, Microsoft.Extensions.Configuration.IConfiguration configuration, Serilog.Settings.Configuration.ConfigurationReaderOptions? readerOptions = null) { } + [System.Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, ConfigurationReaderOptio" + + "ns readerOptions) instead.")] + public static Serilog.LoggerConfiguration Configuration(this Serilog.Configuration.LoggerSettingsConfiguration settingConfiguration, Microsoft.Extensions.Configuration.IConfiguration configuration, params System.Reflection.Assembly[] assemblies) { } + [System.Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, ConfigurationReaderOptio" + + "ns readerOptions) instead.")] + public static Serilog.LoggerConfiguration Configuration(this Serilog.Configuration.LoggerSettingsConfiguration settingConfiguration, Microsoft.Extensions.Configuration.IConfiguration configuration, string sectionName, Microsoft.Extensions.DependencyModel.DependencyContext? dependencyContext = null) { } + [System.Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, ConfigurationReaderOptio" + + "ns readerOptions) instead.")] + public static Serilog.LoggerConfiguration Configuration(this Serilog.Configuration.LoggerSettingsConfiguration settingConfiguration, Microsoft.Extensions.Configuration.IConfiguration configuration, string sectionName, Serilog.Settings.Configuration.ConfigurationAssemblySource configurationAssemblySource) { } + [System.Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, ConfigurationReaderOptio" + + "ns readerOptions) instead.")] + public static Serilog.LoggerConfiguration Configuration(this Serilog.Configuration.LoggerSettingsConfiguration settingConfiguration, Microsoft.Extensions.Configuration.IConfiguration configuration, string sectionName, params System.Reflection.Assembly[] assemblies) { } + [System.Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, string sectionName, Depe" + + "ndencyContext dependencyContext) instead.")] + public static Serilog.LoggerConfiguration ConfigurationSection(this Serilog.Configuration.LoggerSettingsConfiguration settingConfiguration, Microsoft.Extensions.Configuration.IConfigurationSection configSection, Microsoft.Extensions.DependencyModel.DependencyContext? dependencyContext = null) { } + [System.Obsolete("Use ReadFrom.Configuration(IConfiguration configuration, string sectionName, Conf" + + "igurationAssemblySource configurationAssemblySource) instead.")] + public static Serilog.LoggerConfiguration ConfigurationSection(this Serilog.Configuration.LoggerSettingsConfiguration settingConfiguration, Microsoft.Extensions.Configuration.IConfigurationSection configSection, Serilog.Settings.Configuration.ConfigurationAssemblySource configurationAssemblySource) { } + } +} +namespace Serilog.Settings.Configuration +{ + public enum ConfigurationAssemblySource + { + UseLoadedAssemblies = 0, + AlwaysScanDllFiles = 1, + } + public sealed class ConfigurationReaderOptions + { + public ConfigurationReaderOptions() { } + public ConfigurationReaderOptions(Microsoft.Extensions.DependencyModel.DependencyContext? dependencyContext) { } + public ConfigurationReaderOptions(Serilog.Settings.Configuration.ConfigurationAssemblySource configurationAssemblySource) { } + public ConfigurationReaderOptions(params System.Reflection.Assembly[] assemblies) { } + public bool AllowInternalMethods { get; init; } + public bool AllowInternalTypes { get; init; } + public System.IFormatProvider? FormatProvider { get; init; } + public System.Action? OnFilterSwitchCreated { get; init; } + public System.Action? OnLevelSwitchCreated { get; init; } + public string? SectionName { get; init; } + } + public interface ILoggingFilterSwitch + { + string? Expression { get; set; } + } +} \ No newline at end of file diff --git a/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs b/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs index 8b455d55..c28b1bd2 100644 --- a/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs @@ -1,220 +1,234 @@ -using System; -using Serilog.Core; +using Serilog.Core; using Serilog.Events; using Serilog.Formatting; using Serilog.Formatting.Json; using Serilog.Settings.Configuration.Tests.Support; -using Xunit; +namespace Serilog.Settings.Configuration.Tests; -namespace Serilog.Settings.Configuration.Tests +public class StringArgumentValueTests { - public class StringArgumentValueTests + [Fact] + public void StringValuesConvertToDefaultInstancesIfTargetIsInterface() { - [Fact] - public void StringValuesConvertToDefaultInstancesIfTargetIsInterface() - { - var stringArgumentValue = new StringArgumentValue("Serilog.Formatting.Json.JsonFormatter, Serilog"); + var stringArgumentValue = new StringArgumentValue("Serilog.Formatting.Json.JsonFormatter, Serilog"); - var result = stringArgumentValue.ConvertTo(typeof(ITextFormatter), new ResolutionContext()); + var result = stringArgumentValue.ConvertTo(typeof(ITextFormatter), new ResolutionContext()); - Assert.IsType(result); - } + Assert.IsType(result); + } - [Fact] - public void StringValuesConvertToDefaultInstancesIfTargetIsAbstractClass() - { - var stringArgumentValue = new StringArgumentValue("Serilog.Settings.Configuration.Tests.Support.ConcreteClass, Serilog.Settings.Configuration.Tests"); + [Fact] + public void StringValuesConvertToDefaultInstancesIfTargetIsAbstractClass() + { + var stringArgumentValue = new StringArgumentValue("Serilog.Settings.Configuration.Tests.Support.ConcreteClass, Serilog.Settings.Configuration.Tests"); - var result = stringArgumentValue.ConvertTo(typeof(AbstractClass), new ResolutionContext()); + var result = stringArgumentValue.ConvertTo(typeof(AbstractClass), new ResolutionContext()); - Assert.IsType(result); - } + Assert.IsType(result); + } - [Theory] - [InlineData("My.NameSpace.Class+InnerClass::Member", - "My.NameSpace.Class+InnerClass", "Member")] - [InlineData(" TrimMe.NameSpace.Class::NeedsTrimming ", - "TrimMe.NameSpace.Class", "NeedsTrimming")] - [InlineData("My.NameSpace.Class::Member", - "My.NameSpace.Class", "Member")] - [InlineData("My.NameSpace.Class::Member, MyAssembly", - "My.NameSpace.Class, MyAssembly", "Member")] - [InlineData("My.NameSpace.Class::Member, MyAssembly, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", - "My.NameSpace.Class, MyAssembly, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "Member")] - [InlineData("Just a random string with :: in it", - null, null)] - [InlineData("Its::a::trapWithColonsAppearingTwice", - null, null)] - [InlineData("ThereIsNoMemberHere::", - null, null)] - [InlineData(null, - null, null)] - [InlineData(" ", - null, null)] - // 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) + [Theory] + [InlineData("My.NameSpace.Class+InnerClass::Member", + "My.NameSpace.Class+InnerClass", "Member")] + [InlineData(" TrimMe.NameSpace.Class::NeedsTrimming ", + "TrimMe.NameSpace.Class", "NeedsTrimming")] + [InlineData("My.NameSpace.Class::Member", + "My.NameSpace.Class", "Member")] + [InlineData("My.NameSpace.Class::Member, MyAssembly", + "My.NameSpace.Class, MyAssembly", "Member")] + [InlineData("My.NameSpace.Class::Member, MyAssembly, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", + "My.NameSpace.Class, MyAssembly, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "Member")] + [InlineData("Just a random string with :: in it", + null, null)] + [InlineData("Its::a::trapWithColonsAppearingTwice", + null, null)] + [InlineData("ThereIsNoMemberHere::", + null, null)] + [InlineData(null, + null, null)] + [InlineData(" ", + null, null)] + // 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) + { + var actual = StringArgumentValue.TryParseStaticMemberAccessor(input, + out var actualAccessorType, + out var actualMemberName); + + if (expectedAccessorType == null) { - var actual = StringArgumentValue.TryParseStaticMemberAccessor(input, - out var actualAccessorType, - out var actualMemberName); - - if (expectedAccessorType == null) - { - Assert.False(actual, $"Should not parse {input}"); - } - else - { - Assert.True(actual, $"should successfully parse {input}"); - Assert.Equal(expectedAccessorType, actualAccessorType); - Assert.Equal(expectedPropertyName, actualMemberName); - } + Assert.False(actual, $"Should not parse {input}"); } - - [Theory] - [InlineData("Serilog.Formatting.Json.JsonFormatter", typeof(JsonFormatter))] - [InlineData("Serilog.Formatting.Json.JsonFormatter, Serilog", typeof(JsonFormatter))] - [InlineData("Serilog.ConfigurationLoggerConfigurationExtensions", typeof(ConfigurationLoggerConfigurationExtensions))] - public void FindTypeSupportsSimpleNamesForSerilogTypes(string input, Type targetType) + else { - var type = StringArgumentValue.FindType(input); - Assert.Equal(targetType, type); + Assert.True(actual, $"should successfully parse {input}"); + Assert.Equal(expectedAccessorType, actualAccessorType); + Assert.Equal(expectedPropertyName, actualMemberName); } + } - [Theory] - [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InterfaceProperty, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] - [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::AbstractProperty, Serilog.Settings.Configuration.Tests", typeof(AnAbstractClass))] - [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InterfaceField, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] - [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::AbstractField, Serilog.Settings.Configuration.Tests", typeof(AnAbstractClass))] - public void StaticMembersAccessorsCanBeUsedForAbstractTypes(string input, Type targetType) - { - var stringArgumentValue = new StringArgumentValue(input); + [Theory] + [InlineData("Serilog.Formatting.Json.JsonFormatter", typeof(JsonFormatter))] + [InlineData("Serilog.Formatting.Json.JsonFormatter, Serilog", typeof(JsonFormatter))] + [InlineData("Serilog.ConfigurationLoggerConfigurationExtensions", typeof(ConfigurationLoggerConfigurationExtensions))] + public void FindTypeSupportsSimpleNamesForSerilogTypes(string input, Type targetType) + { + var type = StringArgumentValue.FindType(input); + Assert.Equal(targetType, type); + } - var actual = stringArgumentValue.ConvertTo(targetType, new ResolutionContext()); + [Theory] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InterfaceProperty, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::AbstractProperty, Serilog.Settings.Configuration.Tests", typeof(AnAbstractClass))] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InterfaceField, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::AbstractField, Serilog.Settings.Configuration.Tests", typeof(AnAbstractClass))] + public void StaticMembersAccessorsCanBeUsedForAbstractTypes(string input, Type targetType) + { + var stringArgumentValue = new StringArgumentValue(input); - Assert.IsAssignableFrom(targetType, actual); - Assert.Equal(ConcreteImpl.Instance, actual); - } + var actual = stringArgumentValue.ConvertTo(targetType, new ResolutionContext()); - [Theory] - [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::ConcreteClassProperty, Serilog.Settings.Configuration.Tests", typeof(AConcreteClass))] - [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::ConcreteClassField, Serilog.Settings.Configuration.Tests", typeof(AConcreteClass))] - public void StaticMembersAccessorsCanBeUsedForConcreteReferenceTypes(string input, Type targetType) - { - var stringArgumentValue = new StringArgumentValue(input); + Assert.IsAssignableFrom(targetType, actual); + Assert.Equal(ConcreteImpl.Instance, actual); + } - var actual = stringArgumentValue.ConvertTo(targetType, new ResolutionContext()); + [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))] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::FuncIntParseProperty, Serilog.Settings.Configuration.Tests", typeof(Func))] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::NamedIntParseProperty, Serilog.Settings.Configuration.Tests", typeof(NamedIntParse))] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::IntParseMethod, Serilog.Settings.Configuration.Tests", typeof(NamedIntParse))] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::IntParseMethod, Serilog.Settings.Configuration.Tests", typeof(Func))] + public void StaticMembersAccessorsCanBeUsedForDelegateTypes(string input, Type targetType) + { + var stringArgumentValue = new StringArgumentValue(input); - Assert.IsAssignableFrom(targetType, actual); - Assert.Equal(ConcreteImplOfConcreteClass.Instance, actual); - } + var actual = stringArgumentValue.ConvertTo(targetType, new ResolutionContext()); - [Theory] - [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::IntProperty, Serilog.Settings.Configuration.Tests", typeof(int), 42)] - [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::StringProperty, Serilog.Settings.Configuration.Tests", typeof(string), - "Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::StringProperty, Serilog.Settings.Configuration.Tests")] - public void StaticMembersAccessorsCanBeUsedForBuiltInTypes(string input, Type targetType, object expected) - { - var stringArgumentValue = new StringArgumentValue(input); + Assert.IsAssignableFrom(targetType, actual); + var parser = (Delegate?)actual; + Assert.Equal(100, parser?.DynamicInvoke("100")); + } - var actual = stringArgumentValue.ConvertTo(targetType, new ResolutionContext()); + [Theory] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::ConcreteClassProperty, Serilog.Settings.Configuration.Tests", typeof(AConcreteClass))] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::ConcreteClassField, Serilog.Settings.Configuration.Tests", typeof(AConcreteClass))] + public void StaticMembersAccessorsCanBeUsedForConcreteReferenceTypes(string input, Type targetType) + { + var stringArgumentValue = new StringArgumentValue(input); - Assert.Equal(expected, actual); - } + var actual = stringArgumentValue.ConvertTo(targetType, new ResolutionContext()); - [Theory] - // unknown type - [InlineData("Namespace.ThisIsNotAKnownType::InterfaceProperty, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] - // good type name, but wrong namespace - [InlineData("Random.Namespace.ClassWithStaticAccessors::InterfaceProperty, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] - // good full type name, but missing or wrong assembly - [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InterfaceProperty", typeof(IAmAnInterface))] - public void StaticAccessorOnUnknownTypeThrowsTypeLoadException(string input, Type targetType) - { - var stringArgumentValue = new StringArgumentValue($"{input}"); - Assert.Throws(() => - stringArgumentValue.ConvertTo(targetType, new ResolutionContext()) - ); - } + Assert.IsAssignableFrom(targetType, actual); + Assert.Equal(ConcreteImplOfConcreteClass.Instance, actual); + } - [Theory] - // unknown member - [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::UnknownMember, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] - // static property exists but it's private - [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::PrivateInterfaceProperty, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] - // static field exists but it's private - [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::PrivateInterfaceField, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] - // public property exists but it's not static - [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InstanceInterfaceProperty, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] - // public field exists but it's not static - [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InstanceInterfaceField, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] - public void StaticAccessorWithInvalidMemberThrowsInvalidOperationException(string input, Type targetType) - { - var stringArgumentValue = new StringArgumentValue($"{input}"); - var exception = Assert.Throws(() => - stringArgumentValue.ConvertTo(targetType, new ResolutionContext()) - ); + [Theory] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::IntProperty, Serilog.Settings.Configuration.Tests", typeof(int), 42)] + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::StringProperty, Serilog.Settings.Configuration.Tests", typeof(string), + "Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::StringProperty, Serilog.Settings.Configuration.Tests")] + public void StaticMembersAccessorsCanBeUsedForBuiltInTypes(string input, Type targetType, object expected) + { + var stringArgumentValue = new StringArgumentValue(input); - Assert.Contains("Could not find a public static property or field ", exception.Message); - Assert.Contains("on type `Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors, Serilog.Settings.Configuration.Tests`", exception.Message); - } + var actual = stringArgumentValue.ConvertTo(targetType, new ResolutionContext()); - [Fact] - public void LevelSwitchesCanBeLookedUpByName() - { - var @switch = new LoggingLevelSwitch(LogEventLevel.Verbose); - var switchName = "$theSwitch"; - var resolutionContext = new ResolutionContext(); - resolutionContext.AddLevelSwitch(switchName, @switch); + Assert.Equal(expected, actual); + } - var stringArgumentValue = new StringArgumentValue(switchName); + [Theory] + // unknown type + [InlineData("Namespace.ThisIsNotAKnownType::InterfaceProperty, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] + // good type name, but wrong namespace + [InlineData("Random.Namespace.ClassWithStaticAccessors::InterfaceProperty, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] + // good full type name, but missing or wrong assembly + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InterfaceProperty", typeof(IAmAnInterface))] + public void StaticAccessorOnUnknownTypeThrowsTypeLoadException(string input, Type targetType) + { + var stringArgumentValue = new StringArgumentValue($"{input}"); + Assert.Throws(() => + stringArgumentValue.ConvertTo(targetType, new ResolutionContext()) + ); + } - var resolvedSwitch = stringArgumentValue.ConvertTo(typeof(LoggingLevelSwitch), resolutionContext); + [Theory] + // unknown member + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::UnknownMember, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] + // static property exists but it's private + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::PrivateInterfaceProperty, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] + // static field exists but it's private + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::PrivateInterfaceField, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] + // public property exists but it's not static + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InstanceInterfaceProperty, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] + // public field exists but it's not static + [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InstanceInterfaceField, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))] + public void StaticAccessorWithInvalidMemberThrowsInvalidOperationException(string input, Type targetType) + { + var stringArgumentValue = new StringArgumentValue($"{input}"); + var exception = Assert.Throws(() => + stringArgumentValue.ConvertTo(targetType, new ResolutionContext()) + ); - Assert.IsType(resolvedSwitch); - Assert.Same(@switch, resolvedSwitch); - } + Assert.Contains("Could not find a public static property or field ", exception.Message); + Assert.Contains("on type `Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors, Serilog.Settings.Configuration.Tests`", exception.Message); + } + [Fact] + public void LevelSwitchesCanBeLookedUpByName() + { + var @switch = new LoggingLevelSwitch(LogEventLevel.Verbose); + var switchName = "$theSwitch"; + var resolutionContext = new ResolutionContext(); + resolutionContext.AddLevelSwitch(switchName, @switch); - [Fact] - public void ReferencingUndeclaredLevelSwitchThrows() - { - var resolutionContext = new ResolutionContext(); - resolutionContext.AddLevelSwitch("$anotherSwitch", new LoggingLevelSwitch(LogEventLevel.Verbose)); + var stringArgumentValue = new StringArgumentValue(switchName); - var stringArgumentValue = new StringArgumentValue("$mySwitch"); + var resolvedSwitch = stringArgumentValue.ConvertTo(typeof(LoggingLevelSwitch), resolutionContext); - var ex = Assert.Throws(() => - stringArgumentValue.ConvertTo(typeof(LoggingLevelSwitch), resolutionContext) - ); + Assert.IsType(resolvedSwitch); + Assert.Same(@switch, resolvedSwitch); + } - Assert.Contains("$mySwitch", ex.Message); - Assert.Contains("\"LevelSwitches\":{\"$mySwitch\":", ex.Message); - } - [Fact] - public void StringValuesConvertToTypeFromShortTypeName() - { - var shortTypeName = "System.Version"; - var stringArgumentValue = new StringArgumentValue(shortTypeName); + [Fact] + public void ReferencingUndeclaredLevelSwitchThrows() + { + var resolutionContext = new ResolutionContext(); + resolutionContext.AddLevelSwitch("$anotherSwitch", new LoggingLevelSwitch(LogEventLevel.Verbose)); - var actual = (Type)stringArgumentValue.ConvertTo(typeof(Type), new ResolutionContext()); + var stringArgumentValue = new StringArgumentValue("$mySwitch"); - Assert.Equal(typeof(Version), actual); - } + var ex = Assert.Throws(() => + stringArgumentValue.ConvertTo(typeof(LoggingLevelSwitch), resolutionContext) + ); - [Fact] - public void StringValuesConvertToTypeFromAssemblyQualifiedName() - { - var assemblyQualifiedName = typeof(Version).AssemblyQualifiedName; - var stringArgumentValue = new StringArgumentValue(assemblyQualifiedName); + Assert.Contains("$mySwitch", ex.Message); + Assert.Contains("\"LevelSwitches\":{\"$mySwitch\":", ex.Message); + } - var actual = (Type)stringArgumentValue.ConvertTo(typeof(Type), new ResolutionContext()); + [Fact] + public void StringValuesConvertToTypeFromShortTypeName() + { + var shortTypeName = "System.Version"; + var stringArgumentValue = new StringArgumentValue(shortTypeName); - Assert.Equal(typeof(Version), actual); - } + var actual = (Type?)stringArgumentValue.ConvertTo(typeof(Type), new ResolutionContext()); + + Assert.Equal(typeof(Version), actual); + } + + [Fact] + public void StringValuesConvertToTypeFromAssemblyQualifiedName() + { + var assemblyQualifiedName = typeof(Version).AssemblyQualifiedName!; + var stringArgumentValue = new StringArgumentValue(assemblyQualifiedName); + + var actual = (Type?)stringArgumentValue.ConvertTo(typeof(Type), new ResolutionContext()); + + Assert.Equal(typeof(Version), actual); } } diff --git a/test/Serilog.Settings.Configuration.Tests/Support/AbstractClass.cs b/test/Serilog.Settings.Configuration.Tests/Support/AbstractClass.cs index a894668e..695f02e4 100644 --- a/test/Serilog.Settings.Configuration.Tests/Support/AbstractClass.cs +++ b/test/Serilog.Settings.Configuration.Tests/Support/AbstractClass.cs @@ -1,6 +1,5 @@ -namespace Serilog.Settings.Configuration.Tests.Support -{ - public abstract class AbstractClass { } +namespace Serilog.Settings.Configuration.Tests.Support; - public class ConcreteClass : AbstractClass { } -} +public abstract class AbstractClass { } + +public class ConcreteClass : AbstractClass { } diff --git a/test/Serilog.Settings.Configuration.Tests/Support/ConfigurationBuilderExtensions.cs b/test/Serilog.Settings.Configuration.Tests/Support/ConfigurationBuilderExtensions.cs index 212cd0db..d63b3c77 100644 --- a/test/Serilog.Settings.Configuration.Tests/Support/ConfigurationBuilderExtensions.cs +++ b/test/Serilog.Settings.Configuration.Tests/Support/ConfigurationBuilderExtensions.cs @@ -1,12 +1,11 @@ using Microsoft.Extensions.Configuration; -namespace Serilog.Settings.Configuration.Tests.Support +namespace Serilog.Settings.Configuration.Tests.Support; + +public static class ConfigurationBuilderExtensions { - public static class ConfigurationBuilderExtensions + public static IConfigurationBuilder AddJsonString(this IConfigurationBuilder builder, string json) { - public static IConfigurationBuilder AddJsonString(this IConfigurationBuilder builder, string json) - { - return builder.Add(new JsonStringConfigSource(json)); - } + return builder.Add(new JsonStringConfigSource(json)); } -} \ No newline at end of file +} diff --git a/test/Serilog.Settings.Configuration.Tests/Support/ConfigurationReaderTestHelpers.cs b/test/Serilog.Settings.Configuration.Tests/Support/ConfigurationReaderTestHelpers.cs new file mode 100644 index 00000000..ae76b43f --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/Support/ConfigurationReaderTestHelpers.cs @@ -0,0 +1,62 @@ +using Microsoft.Extensions.Configuration; +using Serilog.Events; + +namespace Serilog.Settings.Configuration.Tests.Support; + +static class ConfigurationReaderTestHelpers +{ + public const string minimumLevelFlatTemplate = """ + {{ + "Serilog": {{ + "MinimumLevel": "{0}" + }} + }} + """; + public const string minimumLevelObjectTemplate = """ + {{ + "Serilog": {{ + "MinimumLevel": {{ + "Default": "{0}" + }} + }} + }} + """; + public const string minimumLevelFlatKey = "Serilog:MinimumLevel"; + public const string minimumLevelObjectKey = "Serilog:MinimumLevel:Default"; + + public static void AssertLogEventLevels(LoggerConfiguration loggerConfig, LogEventLevel expectedMinimumLevel) + { + var logger = loggerConfig.CreateLogger(); + + var logEventValues = Enum.GetValues(typeof(LogEventLevel)).Cast(); + + foreach (var logEvent in logEventValues) + { + if (logEvent < expectedMinimumLevel) + { + Assert.False(logger.IsEnabled(logEvent), + $"The log level {logEvent} should be disabled as it's lower priority than the minimum level of {expectedMinimumLevel}."); + } + else + { + Assert.True(logger.IsEnabled(logEvent), + $"The log level {logEvent} should be enabled as it's {(logEvent == expectedMinimumLevel ? "the same" : "higher")} priority {(logEvent == expectedMinimumLevel ? "as" : "than")} the minimum level of {expectedMinimumLevel}."); + } + } + } + + // the naming is only to show priority as providers + public static IConfigurationRoot GetConfigRoot( + string? appsettingsJsonLevel = null, + string? appsettingsDevelopmentJsonLevel = null, + Dictionary? envVariables = null) + { + var configBuilder = new ConfigurationBuilder(); + + configBuilder.AddJsonString(appsettingsJsonLevel ?? "{}"); + configBuilder.AddJsonString(appsettingsDevelopmentJsonLevel ?? "{}"); + configBuilder.Add(new ReloadableConfigurationSource(envVariables ?? new Dictionary())); + + return configBuilder.Build(); + } +} diff --git a/test/Serilog.Settings.Configuration.Tests/Support/CustomConsoleTheme.cs b/test/Serilog.Settings.Configuration.Tests/Support/CustomConsoleTheme.cs index 0e3030a7..79050ce9 100644 --- a/test/Serilog.Settings.Configuration.Tests/Support/CustomConsoleTheme.cs +++ b/test/Serilog.Settings.Configuration.Tests/Support/CustomConsoleTheme.cs @@ -1,8 +1,7 @@ using TestDummies.Console.Themes; -namespace Serilog.Settings.Configuration.Tests.Support +namespace Serilog.Settings.Configuration.Tests.Support; + +class CustomConsoleTheme : ConsoleTheme { - class CustomConsoleTheme : ConsoleTheme - { - } } diff --git a/test/Serilog.Settings.Configuration.Tests/Support/DelegatingSink.cs b/test/Serilog.Settings.Configuration.Tests/Support/DelegatingSink.cs index 07a848bd..0c1bf815 100644 --- a/test/Serilog.Settings.Configuration.Tests/Support/DelegatingSink.cs +++ b/test/Serilog.Settings.Configuration.Tests/Support/DelegatingSink.cs @@ -1,33 +1,31 @@ -using System; -using Serilog.Core; +using Serilog.Core; using Serilog.Events; -namespace Serilog.Settings.Configuration.Tests.Support +namespace Serilog.Settings.Configuration.Tests.Support; + +public class DelegatingSink : ILogEventSink { - public class DelegatingSink : ILogEventSink - { - readonly Action _write; + readonly Action _write; - public DelegatingSink(Action write) - { - _write = write ?? throw new ArgumentNullException(nameof(write)); - } + public DelegatingSink(Action write) + { + _write = write ?? throw new ArgumentNullException(nameof(write)); + } - public void Emit(LogEvent logEvent) - { - _write(logEvent); - } + public void Emit(LogEvent logEvent) + { + _write(logEvent); + } - public static LogEvent GetLogEvent(Action writeAction) - { - LogEvent result = null; - var l = new LoggerConfiguration() - .MinimumLevel.Verbose() - .WriteTo.Sink(new DelegatingSink(le => result = le)) - .CreateLogger(); + public static LogEvent? GetLogEvent(Action writeAction) + { + LogEvent? result = null; + var l = new LoggerConfiguration() + .MinimumLevel.Verbose() + .WriteTo.Sink(new DelegatingSink(le => result = le)) + .CreateLogger(); - writeAction(l); - return result; - } + writeAction(l); + return result; } } diff --git a/test/Serilog.Settings.Configuration.Tests/Support/DirectoryInfoExtensions.cs b/test/Serilog.Settings.Configuration.Tests/Support/DirectoryInfoExtensions.cs new file mode 100644 index 00000000..2d2e214f --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/Support/DirectoryInfoExtensions.cs @@ -0,0 +1,10 @@ +namespace Serilog.Settings.Configuration.Tests; + +static class DirectoryInfoExtensions +{ + public static DirectoryInfo SubDirectory(this DirectoryInfo directory, params string[] paths) + => new(Path.GetFullPath(Path.Combine(paths.Prepend(directory.FullName).ToArray()))); + + public static FileInfo File(this DirectoryInfo directory, params string[] paths) + => new(Path.GetFullPath(Path.Combine(paths.Prepend(directory.FullName).ToArray()))); +} diff --git a/test/Serilog.Settings.Configuration.Tests/Support/Extensions.cs b/test/Serilog.Settings.Configuration.Tests/Support/Extensions.cs index 6ef5d96f..3bf62ffa 100644 --- a/test/Serilog.Settings.Configuration.Tests/Support/Extensions.cs +++ b/test/Serilog.Settings.Configuration.Tests/Support/Extensions.cs @@ -1,18 +1,13 @@ using Serilog.Events; -namespace Serilog.Settings.Configuration.Tests.Support +namespace Serilog.Settings.Configuration.Tests.Support; + +public static class Extensions { - public static class Extensions + public static object? LiteralValue(this LogEventPropertyValue @this) { - public static object LiteralValue(this LogEventPropertyValue @this) - { - return ((ScalarValue)@this).Value; - } - - public static string ToValidJson(this string str) - { - str = str.Replace('\'', '"'); - return str; - } + return ((ScalarValue)@this).Value; } + + public static string Format(this string template, params object[] paramObjects) => string.Format(template, paramObjects); } diff --git a/test/Serilog.Settings.Configuration.Tests/Support/JsonStringConfigSource.cs b/test/Serilog.Settings.Configuration.Tests/Support/JsonStringConfigSource.cs index 1175ee4e..eb26e60d 100644 --- a/test/Serilog.Settings.Configuration.Tests/Support/JsonStringConfigSource.cs +++ b/test/Serilog.Settings.Configuration.Tests/Support/JsonStringConfigSource.cs @@ -1,63 +1,59 @@ -using System.Collections.Generic; -using System.IO; - -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration.Json; -namespace Serilog.Settings.Configuration.Tests.Support +namespace Serilog.Settings.Configuration.Tests.Support; + +class JsonStringConfigSource : IConfigurationSource { - class JsonStringConfigSource : IConfigurationSource + readonly string _json; + + public JsonStringConfigSource(string json) + { + _json = json; + } + + public IConfigurationProvider Build(IConfigurationBuilder builder) + { + return new JsonStringConfigProvider(_json); + } + + public static IConfigurationSection LoadSection(string json, string section) + { + return new ConfigurationBuilder().Add(new JsonStringConfigSource(json)).Build().GetSection(section); + } + + public static IDictionary LoadData(string json) + { + var provider = new JsonStringConfigProvider(json); + provider.Load(); + return provider.Data; + } + + class JsonStringConfigProvider : JsonConfigurationProvider { readonly string _json; - public JsonStringConfigSource(string json) + public JsonStringConfigProvider(string json) : base(new JsonConfigurationSource { Optional = true }) { _json = json; } - public IConfigurationProvider Build(IConfigurationBuilder builder) - { - return new JsonStringConfigProvider(_json); - } + public new IDictionary Data => base.Data; - public static IConfigurationSection LoadSection(string json, string section) + public override void Load() { - return new ConfigurationBuilder().Add(new JsonStringConfigSource(json)).Build().GetSection(section); + Load(StringToStream(_json)); } - public static IDictionary LoadData(string json) + static Stream StringToStream(string str) { - var provider = new JsonStringConfigProvider(json); - provider.Load(); - return provider.Data; - } + var memStream = new MemoryStream(); + var textWriter = new StreamWriter(memStream); + textWriter.Write(str); + textWriter.Flush(); + memStream.Seek(0, SeekOrigin.Begin); - class JsonStringConfigProvider : JsonConfigurationProvider - { - readonly string _json; - - public JsonStringConfigProvider(string json) : base(new JsonConfigurationSource { Optional = true }) - { - _json = json; - } - - public new IDictionary Data => base.Data; - - public override void Load() - { - Load(StringToStream(_json.ToValidJson())); - } - - static Stream StringToStream(string str) - { - var memStream = new MemoryStream(); - var textWriter = new StreamWriter(memStream); - textWriter.Write(str); - textWriter.Flush(); - memStream.Seek(0, SeekOrigin.Begin); - - return memStream; - } + return memStream; } } } diff --git a/test/Serilog.Settings.Configuration.Tests/Support/PublishMode.cs b/test/Serilog.Settings.Configuration.Tests/Support/PublishMode.cs new file mode 100644 index 00000000..082d1dfc --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/Support/PublishMode.cs @@ -0,0 +1,25 @@ +namespace Serilog.Settings.Configuration.Tests.Support; + +/// +/// The possible application publish modes for the TestApp. +/// See also the .NET application publishing overview documentation. +/// +public enum PublishMode +{ + /// + /// Standard app publish, all dlls and related files are copied along the main executable. + /// + Standard, + + /// + /// Publish a single file as a framework-dependent binary. + /// + /// On .NET Framework, Costura is used to publish as a single file. + SingleFile, + + /// + /// Publish a single file as a self contained binary, i.e. including the .NET libraries and target runtime. + /// + /// This mode is ignored on .NET Framework as it doesn't make sense. + SelfContained, +} diff --git a/test/Serilog.Settings.Configuration.Tests/Support/PublishModeExtensions.cs b/test/Serilog.Settings.Configuration.Tests/Support/PublishModeExtensions.cs new file mode 100644 index 00000000..d3f49825 --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/Support/PublishModeExtensions.cs @@ -0,0 +1,31 @@ +using System.Reflection; +using System.Runtime.Versioning; +using NuGet.Frameworks; + +namespace Serilog.Settings.Configuration.Tests.Support; + +public static class PublishModeExtensions +{ + static PublishModeExtensions() + { + var targetFrameworkAttribute = typeof(TestApp).Assembly.GetCustomAttribute(); + if (targetFrameworkAttribute == null) + { + throw new Exception($"Assembly {typeof(TestApp).Assembly} does not have a {nameof(TargetFrameworkAttribute)}"); + } + + var framework = NuGetFramework.Parse(targetFrameworkAttribute.FrameworkName); + + TargetFramework = framework.GetShortFolderName(); + IsDesktop = framework.IsDesktop(); + } + + public static bool IsDesktop { get; } + + public static string TargetFramework { get; } + + public static IEnumerable GetPublishModes() + { + return IsDesktop ? new[] { PublishMode.Standard, PublishMode.SingleFile } : Enum.GetValues(typeof(PublishMode)).Cast(); + } +} diff --git a/test/Serilog.Settings.Configuration.Tests/Support/ReloadableConfigurationSource.cs b/test/Serilog.Settings.Configuration.Tests/Support/ReloadableConfigurationSource.cs index 8f90b348..4e8b8011 100644 --- a/test/Serilog.Settings.Configuration.Tests/Support/ReloadableConfigurationSource.cs +++ b/test/Serilog.Settings.Configuration.Tests/Support/ReloadableConfigurationSource.cs @@ -1,37 +1,35 @@ -using System.Collections.Generic; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; -namespace Serilog.Settings.Configuration.Tests.Support +namespace Serilog.Settings.Configuration.Tests.Support; + +class ReloadableConfigurationSource : IConfigurationSource { - class ReloadableConfigurationSource : IConfigurationSource + readonly ReloadableConfigurationProvider _configProvider; + readonly IDictionary _source; + + public ReloadableConfigurationSource(IDictionary source) { - readonly ReloadableConfigurationProvider _configProvider; - readonly IDictionary _source; + _source = source; + _configProvider = new ReloadableConfigurationProvider(source); + } - public ReloadableConfigurationSource(IDictionary source) - { - _source = source; - _configProvider = new ReloadableConfigurationProvider(source); - } + public IConfigurationProvider Build(IConfigurationBuilder builder) => _configProvider; - public IConfigurationProvider Build(IConfigurationBuilder builder) => _configProvider; + public void Reload() => _configProvider.Reload(); - public void Reload() => _configProvider.Reload(); + public void Set(string key, string value) => _source[key] = value; - public void Set(string key, string value) => _source[key] = value; + class ReloadableConfigurationProvider : ConfigurationProvider + { + readonly IDictionary _source; - class ReloadableConfigurationProvider : ConfigurationProvider + public ReloadableConfigurationProvider(IDictionary source) { - readonly IDictionary _source; - - public ReloadableConfigurationProvider(IDictionary source) - { - _source = source; - } + _source = source; + } - public override void Load() => Data = _source; + public override void Load() => Data = _source; - public void Reload() => OnReload(); - } + public void Reload() => OnReload(); } } diff --git a/test/Serilog.Settings.Configuration.Tests/Support/Some.cs b/test/Serilog.Settings.Configuration.Tests/Support/Some.cs index 9131b6d9..ba4f6af4 100644 --- a/test/Serilog.Settings.Configuration.Tests/Support/Some.cs +++ b/test/Serilog.Settings.Configuration.Tests/Support/Some.cs @@ -1,101 +1,95 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Threading; -using Serilog.Core; +using Serilog.Core; using Serilog.Events; using Serilog.Parsing; -namespace Serilog.Settings.Configuration.Tests.Support +namespace Serilog.Settings.Configuration.Tests.Support; + +static class Some { - static class Some - { - static int Counter; - - public static int Int() - { - return Interlocked.Increment(ref Counter); - } - - public static decimal Decimal() - { - return Int() + 0.123m; - } - - public static string String(string tag = null) - { - return (tag ?? "") + "__" + Int(); - } - - public static TimeSpan TimeSpan() - { - return System.TimeSpan.FromMinutes(Int()); - } - - public static DateTime Instant() - { - return new DateTime(2012, 10, 28) + TimeSpan(); - } - - public static DateTimeOffset OffsetInstant() - { - return new DateTimeOffset(Instant()); - } - - public static LogEvent LogEvent(string sourceContext, DateTimeOffset? timestamp = null, LogEventLevel level = LogEventLevel.Information) - { - return new LogEvent(timestamp ?? OffsetInstant(), level, - null, MessageTemplate(), - new List { new LogEventProperty(Constants.SourceContextPropertyName, new ScalarValue(sourceContext)) }); - } - - public static LogEvent LogEvent(DateTimeOffset? timestamp = null, LogEventLevel level = LogEventLevel.Information) - { - return new LogEvent(timestamp ?? OffsetInstant(), level, - null, MessageTemplate(), Enumerable.Empty()); - } - - public static LogEvent InformationEvent(DateTimeOffset? timestamp = null) - { - return LogEvent(timestamp, LogEventLevel.Information); - } - - public static LogEvent DebugEvent(DateTimeOffset? timestamp = null) - { - return LogEvent(timestamp, LogEventLevel.Debug); - } - - public static LogEvent WarningEvent(DateTimeOffset? timestamp = null) - { - return LogEvent(timestamp, LogEventLevel.Warning); - } - - public static LogEventProperty LogEventProperty() - { - return new LogEventProperty(String(), new ScalarValue(Int())); - } - - public static string NonexistentTempFilePath() - { - return Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".txt"); - } - - public static string TempFilePath() - { - return Path.GetTempFileName(); - } - - public static string TempFolderPath() - { - var dir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); - Directory.CreateDirectory(dir); - return dir; - } - - public static MessageTemplate MessageTemplate() - { - return new MessageTemplateParser().Parse(String()); - } + static int Counter; + + public static int Int() + { + return Interlocked.Increment(ref Counter); + } + + public static decimal Decimal() + { + return Int() + 0.123m; + } + + public static string String(string? tag = null) + { + return (tag ?? "") + "__" + Int(); + } + + public static TimeSpan TimeSpan() + { + return System.TimeSpan.FromMinutes(Int()); + } + + public static DateTime Instant() + { + return new DateTime(2012, 10, 28) + TimeSpan(); + } + + public static DateTimeOffset OffsetInstant() + { + return new DateTimeOffset(Instant()); + } + + public static LogEvent LogEvent(string sourceContext, DateTimeOffset? timestamp = null, LogEventLevel level = LogEventLevel.Information) + { + return new LogEvent(timestamp ?? OffsetInstant(), level, + null, MessageTemplate(), + new List { new LogEventProperty(Constants.SourceContextPropertyName, new ScalarValue(sourceContext)) }); + } + + public static LogEvent LogEvent(DateTimeOffset? timestamp = null, LogEventLevel level = LogEventLevel.Information) + { + return new LogEvent(timestamp ?? OffsetInstant(), level, + null, MessageTemplate(), Enumerable.Empty()); + } + + public static LogEvent InformationEvent(DateTimeOffset? timestamp = null) + { + return LogEvent(timestamp, LogEventLevel.Information); + } + + public static LogEvent DebugEvent(DateTimeOffset? timestamp = null) + { + return LogEvent(timestamp, LogEventLevel.Debug); + } + + public static LogEvent WarningEvent(DateTimeOffset? timestamp = null) + { + return LogEvent(timestamp, LogEventLevel.Warning); + } + + public static LogEventProperty LogEventProperty() + { + return new LogEventProperty(String(), new ScalarValue(Int())); + } + + public static string NonexistentTempFilePath() + { + return Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".txt"); + } + + public static string TempFilePath() + { + return Path.GetTempFileName(); + } + + public static string TempFolderPath() + { + var dir = Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString()); + Directory.CreateDirectory(dir); + return dir; + } + + public static MessageTemplate MessageTemplate() + { + return new MessageTemplateParser().Parse(String()); } } diff --git a/test/Serilog.Settings.Configuration.Tests/Support/StaticAccessorClasses.cs b/test/Serilog.Settings.Configuration.Tests/Support/StaticAccessorClasses.cs index 03b83574..babd2d69 100644 --- a/test/Serilog.Settings.Configuration.Tests/Support/StaticAccessorClasses.cs +++ b/test/Serilog.Settings.Configuration.Tests/Support/StaticAccessorClasses.cs @@ -1,50 +1,59 @@ -namespace Serilog.Settings.Configuration.Tests.Support -{ - public interface IAmAnInterface - { - } +namespace Serilog.Settings.Configuration.Tests.Support; - public abstract class AnAbstractClass - { - } +public delegate int NamedIntParse(string value); - class ConcreteImpl : AnAbstractClass, IAmAnInterface - { - private ConcreteImpl() - { - } +public interface IAmAnInterface +{ +} - public static ConcreteImpl Instance { get; } = new ConcreteImpl(); - } +public abstract class AnAbstractClass +{ +} - public class AConcreteClass +class ConcreteImpl : AnAbstractClass, IAmAnInterface +{ + private ConcreteImpl() { } - class ConcreteImplOfConcreteClass : AConcreteClass - { - public static ConcreteImplOfConcreteClass Instance { get; } = new ConcreteImplOfConcreteClass(); - } + public static ConcreteImpl Instance { get; } = new ConcreteImpl(); +} - public class ClassWithStaticAccessors - { - public static IAmAnInterface InterfaceProperty => ConcreteImpl.Instance; - public static AnAbstractClass AbstractProperty => ConcreteImpl.Instance; - public static AConcreteClass ConcreteClassProperty => ConcreteImplOfConcreteClass.Instance; - public static int IntProperty => 42; - public static string StringProperty => "don't see me"; +public class AConcreteClass +{ +} - public static IAmAnInterface InterfaceField = ConcreteImpl.Instance; - public static AnAbstractClass AbstractField = ConcreteImpl.Instance; - public static AConcreteClass ConcreteClassField = ConcreteImplOfConcreteClass.Instance; +class ConcreteImplOfConcreteClass : AConcreteClass +{ + public static ConcreteImplOfConcreteClass Instance { get; } = new ConcreteImplOfConcreteClass(); +} + +public class ClassWithStaticAccessors +{ + public static IAmAnInterface InterfaceProperty => ConcreteImpl.Instance; + public static AnAbstractClass AbstractProperty => ConcreteImpl.Instance; + public static AConcreteClass ConcreteClassProperty => ConcreteImplOfConcreteClass.Instance; + public static int IntProperty => 42; + public static string StringProperty => "don't see me"; + + public static IAmAnInterface InterfaceField = ConcreteImpl.Instance; + public static AnAbstractClass AbstractField = ConcreteImpl.Instance; + public static AConcreteClass ConcreteClassField = ConcreteImplOfConcreteClass.Instance; - // ReSharper disable once UnusedMember.Local - private static IAmAnInterface PrivateInterfaceProperty => ConcreteImpl.Instance; + // ReSharper disable once UnusedMember.Local + private static IAmAnInterface PrivateInterfaceProperty => ConcreteImpl.Instance; #pragma warning disable 169 - private static IAmAnInterface PrivateInterfaceField = ConcreteImpl.Instance; + private static IAmAnInterface PrivateInterfaceField = ConcreteImpl.Instance; #pragma warning restore 169 - public IAmAnInterface InstanceInterfaceProperty => ConcreteImpl.Instance; - public IAmAnInterface InstanceInterfaceField = ConcreteImpl.Instance; - } + public IAmAnInterface InstanceInterfaceProperty => ConcreteImpl.Instance; + public IAmAnInterface InstanceInterfaceField = ConcreteImpl.Instance; + + public static Func FuncIntParseField = int.Parse; + public static NamedIntParse NamedIntParseField = int.Parse; + public static Func FuncIntParseProperty => int.Parse; + public static NamedIntParse NamedIntParseProperty => int.Parse; + 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 } diff --git a/test/Serilog.Settings.Configuration.Tests/Support/TestApp.cs b/test/Serilog.Settings.Configuration.Tests/Support/TestApp.cs new file mode 100644 index 00000000..9263a66a --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/Support/TestApp.cs @@ -0,0 +1,161 @@ +using System.Runtime.CompilerServices; +using System.Runtime.InteropServices; +using System.Text; +using CliWrap; +using CliWrap.Exceptions; +using FluentAssertions; +using Polly; +using Xunit.Abstractions; +using Xunit.Sdk; +using static Serilog.Settings.Configuration.Tests.Support.PublishModeExtensions; + +namespace Serilog.Settings.Configuration.Tests.Support; + +public class TestApp : IAsyncLifetime +{ + readonly IMessageSink _messageSink; + readonly DirectoryInfo _workingDirectory; + readonly Dictionary _executables; + + public TestApp(IMessageSink messageSink) + { + _messageSink = messageSink; + _workingDirectory = GetDirectory("test", $"TestApp-{TargetFramework}"); + _workingDirectory.Create(); + foreach (var file in GetDirectory("test", "TestApp").EnumerateFiles()) + { + file.CopyTo(_workingDirectory.File(file.Name).FullName, overwrite: true); + } + _executables = new Dictionary(); + } + + async Task IAsyncLifetime.InitializeAsync() + { + // Retry 3 times because pack / restore / publish may try to access the same files across different target frameworks and fail with System.IO.IOException: + // The process cannot access the file [Serilog.Settings.Configuration.deps.json or Serilog.Settings.Configuration.dll] because it is being used by another process. + var retryPolicy = Policy.Handle().RetryAsync(3); + await retryPolicy.ExecuteAsync(CreateTestAppAsync); + } + + Task IAsyncLifetime.DisposeAsync() + { + _workingDirectory.Delete(recursive: true); + return Task.CompletedTask; + } + + public string GetExecutablePath(PublishMode publishMode) => _executables[publishMode].FullName; + + async Task CreateTestAppAsync() + { + // It might be tempting to do pack -> restore -> build --no-restore -> publish --no-build (and parallelize over publish modes) + // But this would fail because of https://github.com/dotnet/sdk/issues/17526 and probably because of other unforeseen bugs + // preventing from running multiple `dotnet publish` commands with different parameters. + + await PackAsync(); + await RestoreAsync(); + foreach (var publishMode in GetPublishModes()) + { + await PublishAsync(publishMode); + } + } + + async Task PackAsync() + { + var projectFile = GetFile("src", "Serilog.Settings.Configuration", "Serilog.Settings.Configuration.csproj"); + var packArgs = new[] { + "pack", projectFile.FullName, + "--configuration", "Release", + "--output", _workingDirectory.FullName, + "-p:Version=0.0.0-IntegrationTest.0", + }; + await RunDotnetAsync(_workingDirectory, packArgs); + } + + async Task RestoreAsync() + { + // Can't use "--source . --source https://api.nuget.org/v3/index.json" because of https://github.com/dotnet/sdk/issues/27202 => a nuget.config file is used instead. + // It also has the benefit of using settings _only_ from the specified config file, ignoring the global nuget.config where package source mapping could interfere with the local source. + var restoreArgs = new[] { + "restore", + "--configfile", "nuget.config", + "-p:Configuration=Release", + $"-p:TargetFramework={TargetFramework}", + }; + await RunDotnetAsync(_workingDirectory, restoreArgs); + } + + async Task PublishAsync(PublishMode publishMode) + { + var publishDirectory = _workingDirectory.SubDirectory("publish"); + var fodyWeaversXml = _workingDirectory.File("FodyWeavers.xml"); + + var outputDirectory = publishDirectory.SubDirectory(publishMode.ToString()); + + File.WriteAllText(fodyWeaversXml.FullName, publishMode == PublishMode.SingleFile && IsDesktop ? "" : ""); + + var publishArgsBase = new[] { + "publish", + "--no-restore", + "--configuration", "Release", + "--output", outputDirectory.FullName, + $"-p:TargetFramework={TargetFramework}" + }; + var autoGenerateBindingRedirects = $"-p:AutoGenerateBindingRedirects={publishMode is PublishMode.Standard}"; + var publishSingleFile = $"-p:PublishSingleFile={publishMode is PublishMode.SingleFile or PublishMode.SelfContained}"; + var selfContained = $"-p:SelfContained={publishMode is PublishMode.SelfContained}"; + var publishArgs = (IsDesktop ? publishArgsBase.Append(autoGenerateBindingRedirects) : publishArgsBase.Append(publishSingleFile).Append(selfContained)).ToArray(); + await RunDotnetAsync(_workingDirectory, publishArgs); + + var executableFileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? "TestApp.exe" : "TestApp"; + var executableFile = new FileInfo(Path.Combine(outputDirectory.FullName, executableFileName)); + executableFile.Exists.Should().BeTrue(); + var dlls = executableFile.Directory!.EnumerateFiles("*.dll"); + if (publishMode == PublishMode.Standard) + { + dlls.Should().NotBeEmpty(because: $"the test app was _not_ published as single-file ({publishMode})"); + } + else + { + dlls.Should().BeEmpty(because: $"the test app was published as single-file ({publishMode})"); + executableFile.Directory.EnumerateFiles().Should().ContainSingle().Which.FullName.Should().Be(executableFile.FullName); + } + + _executables[publishMode] = executableFile; + } + + async Task RunDotnetAsync(DirectoryInfo workingDirectory, params string[] arguments) + { + _messageSink.OnMessage(new DiagnosticMessage($"cd {workingDirectory}")); + _messageSink.OnMessage(new DiagnosticMessage($"dotnet {string.Join(" ", arguments)}")); + var outBuilder = new StringBuilder(); + var errBuilder = new StringBuilder(); + var command = Cli.Wrap("dotnet") + .WithValidation(CommandResultValidation.None) + .WithWorkingDirectory(workingDirectory.FullName) + .WithArguments(arguments) + .WithStandardOutputPipe(PipeTarget.ToDelegate(line => + { + outBuilder.AppendLine(line); + _messageSink.OnMessage(new DiagnosticMessage($"==> out: {line}")); + })) + .WithStandardErrorPipe(PipeTarget.ToDelegate(line => + { + errBuilder.AppendLine(line); + _messageSink.OnMessage(new DiagnosticMessage($"==> err: {line}")); + })); + + var result = await command.ExecuteAsync(); + if (result.ExitCode != 0) + { + throw new CommandExecutionException(command, result.ExitCode, $"An unexpected exception has occurred while running {command}{Environment.NewLine}{errBuilder}{outBuilder}".Trim()); + } + } + + static DirectoryInfo GetDirectory(params string[] paths) => new(GetFullPath(paths)); + + static FileInfo GetFile(params string[] paths) => new(GetFullPath(paths)); + + static string GetFullPath(params string[] paths) => Path.GetFullPath(Path.Combine(new[] { GetThisDirectory(), "..", "..", ".." }.Concat(paths).ToArray())); + + static string GetThisDirectory([CallerFilePath] string path = "") => Path.GetDirectoryName(path)!; +} diff --git a/test/Serilog.Settings.Configuration.Tests/xunit.runner.json b/test/Serilog.Settings.Configuration.Tests/xunit.runner.json new file mode 100644 index 00000000..6c0d1e49 --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/xunit.runner.json @@ -0,0 +1,4 @@ +{ + "$schema": "https://xunit.net/schema/current/xunit.runner.schema.json", + "diagnosticMessages": true +} diff --git a/test/TestApp/.gitignore b/test/TestApp/.gitignore new file mode 100644 index 00000000..1cc29f16 --- /dev/null +++ b/test/TestApp/.gitignore @@ -0,0 +1,2 @@ +FodyWeavers.xml +FodyWeavers.xsd diff --git a/test/TestApp/Program.cs b/test/TestApp/Program.cs new file mode 100644 index 00000000..b376df4b --- /dev/null +++ b/test/TestApp/Program.cs @@ -0,0 +1,56 @@ +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"; + +var configurationValues = new Dictionary +{ + ["Serilog:Enrich:0"] = "WithThreadName", + ["Serilog:WriteTo:0:Name"] = "Console", + ["Serilog:WriteTo:0:Args:outputTemplate"] = "({ThreadName}) [{Level}] {Message}{NewLine}", +}; + +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 logger = new LoggerConfiguration().ReadFrom.Configuration(configuration, options).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/TestApp.csproj b/test/TestApp/TestApp.csproj new file mode 100644 index 00000000..e60f2b73 --- /dev/null +++ b/test/TestApp/TestApp.csproj @@ -0,0 +1,29 @@ + + + + Exe + net48 + embedded + false + false + false + none + true + + + + + + + + + + + + + + + + + + diff --git a/test/TestApp/nuget.config b/test/TestApp/nuget.config new file mode 100644 index 00000000..cfec8fcc --- /dev/null +++ b/test/TestApp/nuget.config @@ -0,0 +1,11 @@ + + + + + + + + + + + diff --git a/test/TestDummies/Console/DummyConsoleSink.cs b/test/TestDummies/Console/DummyConsoleSink.cs index 79816b89..0486eaef 100644 --- a/test/TestDummies/Console/DummyConsoleSink.cs +++ b/test/TestDummies/Console/DummyConsoleSink.cs @@ -1,31 +1,27 @@ -using System; -using System.Collections.Generic; -using Serilog.Core; +using Serilog.Core; using Serilog.Events; using TestDummies.Console.Themes; -namespace TestDummies.Console +namespace TestDummies.Console; + +public class DummyConsoleSink : ILogEventSink { - public class DummyConsoleSink : ILogEventSink + public DummyConsoleSink(ConsoleTheme? theme = null) { - public DummyConsoleSink(ConsoleTheme theme = null) - { - Theme = theme ?? ConsoleTheme.None; - } + Theme = theme ?? ConsoleTheme.None; + } - [ThreadStatic] - public static ConsoleTheme Theme; + [ThreadStatic] + public static ConsoleTheme? Theme; - [ThreadStatic] - static List EmittedList; + [ThreadStatic] + static List? EmittedList; - public static List Emitted => EmittedList ?? (EmittedList = new List()); + public static List Emitted => EmittedList ??= new List(); - public void Emit(LogEvent logEvent) - { - Emitted.Add(logEvent); - } + public void Emit(LogEvent logEvent) + { + Emitted.Add(logEvent); } - } diff --git a/test/TestDummies/Console/Themes/ConcreteConsoleTheme.cs b/test/TestDummies/Console/Themes/ConcreteConsoleTheme.cs index 8b3b041b..67d8343e 100644 --- a/test/TestDummies/Console/Themes/ConcreteConsoleTheme.cs +++ b/test/TestDummies/Console/Themes/ConcreteConsoleTheme.cs @@ -1,6 +1,5 @@ -namespace TestDummies.Console.Themes +namespace TestDummies.Console.Themes; + +class ConcreteConsoleTheme : ConsoleTheme { - class ConcreteConsoleTheme : ConsoleTheme - { - } } diff --git a/test/TestDummies/Console/Themes/ConsoleTheme.cs b/test/TestDummies/Console/Themes/ConsoleTheme.cs index 1c5aaf5d..ff14007b 100644 --- a/test/TestDummies/Console/Themes/ConsoleTheme.cs +++ b/test/TestDummies/Console/Themes/ConsoleTheme.cs @@ -1,7 +1,6 @@ -namespace TestDummies.Console.Themes +namespace TestDummies.Console.Themes; + +public abstract class ConsoleTheme { - public abstract class ConsoleTheme - { - public static ConsoleTheme None { get; } = new EmptyConsoleTheme(); - } + public static ConsoleTheme None { get; } = new EmptyConsoleTheme(); } diff --git a/test/TestDummies/Console/Themes/ConsoleThemes.cs b/test/TestDummies/Console/Themes/ConsoleThemes.cs index 7bb414cb..2804ba71 100644 --- a/test/TestDummies/Console/Themes/ConsoleThemes.cs +++ b/test/TestDummies/Console/Themes/ConsoleThemes.cs @@ -1,7 +1,6 @@ -namespace TestDummies.Console.Themes +namespace TestDummies.Console.Themes; + +public static class ConsoleThemes { - public static class ConsoleThemes - { - public static ConsoleTheme Theme1 { get; } = new ConcreteConsoleTheme(); - } + public static ConsoleTheme Theme1 { get; } = new ConcreteConsoleTheme(); } diff --git a/test/TestDummies/Console/Themes/EmptyConsoleTheme.cs b/test/TestDummies/Console/Themes/EmptyConsoleTheme.cs index 100e89e8..f01847f8 100644 --- a/test/TestDummies/Console/Themes/EmptyConsoleTheme.cs +++ b/test/TestDummies/Console/Themes/EmptyConsoleTheme.cs @@ -1,6 +1,5 @@ -namespace TestDummies.Console.Themes +namespace TestDummies.Console.Themes; + +class EmptyConsoleTheme : ConsoleTheme { - class EmptyConsoleTheme : ConsoleTheme - { - } } diff --git a/test/TestDummies/DummyAnonymousUserFilter.cs b/test/TestDummies/DummyAnonymousUserFilter.cs index a47dd11b..89bae72b 100644 --- a/test/TestDummies/DummyAnonymousUserFilter.cs +++ b/test/TestDummies/DummyAnonymousUserFilter.cs @@ -2,24 +2,23 @@ using Serilog.Core; using Serilog.Events; -namespace TestDummies +namespace TestDummies; + +public class DummyAnonymousUserFilter : ILogEventFilter { - public class DummyAnonymousUserFilter : ILogEventFilter + public bool IsEnabled(LogEvent logEvent) { - public bool IsEnabled(LogEvent logEvent) + if (logEvent.Properties.ContainsKey("User")) { - if (logEvent.Properties.ContainsKey("User")) + if (logEvent.Properties["User"] is ScalarValue sv) { - if (logEvent.Properties["User"] is ScalarValue sv) + if (sv.Value is string s && s == "anonymous") { - if (sv.Value is string s && s == "anonymous") - { - return false; - } + return false; } } - - return true; } + + return true; } } diff --git a/test/TestDummies/DummyConfigurationSink.cs b/test/TestDummies/DummyConfigurationSink.cs index d2250e74..51620add 100644 --- a/test/TestDummies/DummyConfigurationSink.cs +++ b/test/TestDummies/DummyConfigurationSink.cs @@ -1,46 +1,43 @@ -using System; -using System.Collections.Generic; -using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration; using Serilog.Core; using Serilog.Events; -namespace TestDummies -{ - public class DummyConfigurationSink : ILogEventSink - { - [ThreadStatic] - static List _emitted; +namespace TestDummies; - [ThreadStatic] - static IConfiguration _configuration; +public class DummyConfigurationSink : ILogEventSink +{ + [ThreadStatic] + static List? _emitted; - [ThreadStatic] - static IConfigurationSection _configSection; + [ThreadStatic] + static IConfiguration? _configuration; - public static List Emitted => _emitted ?? (_emitted = new List()); + [ThreadStatic] + static IConfigurationSection? _configSection; - public static IConfiguration Configuration => _configuration; + public static List Emitted => _emitted ?? (_emitted = new List()); - public static IConfigurationSection ConfigSection => _configSection; + public static IConfiguration? Configuration => _configuration; + public static IConfigurationSection? ConfigSection => _configSection; - public DummyConfigurationSink(IConfiguration configuration, IConfigurationSection configSection) - { - _configuration = configuration; - _configSection = configSection; - } - public void Emit(LogEvent logEvent) - { - Emitted.Add(logEvent); - } + public DummyConfigurationSink(IConfiguration? configuration, IConfigurationSection? configSection) + { + _configuration = configuration; + _configSection = configSection; + } - public static void Reset() - { - _emitted = null; - _configuration = null; - _configSection = null; - } + public void Emit(LogEvent logEvent) + { + Emitted.Add(logEvent); + } + public static void Reset() + { + _emitted = null; + _configuration = null; + _configSection = null; } + } diff --git a/test/TestDummies/DummyHardCodedStringDestructuringPolicy.cs b/test/TestDummies/DummyHardCodedStringDestructuringPolicy.cs index 25d27249..727a46e4 100644 --- a/test/TestDummies/DummyHardCodedStringDestructuringPolicy.cs +++ b/test/TestDummies/DummyHardCodedStringDestructuringPolicy.cs @@ -1,22 +1,20 @@ -using System; -using Serilog.Core; +using Serilog.Core; using Serilog.Events; -namespace TestDummies +namespace TestDummies; + +public class DummyHardCodedStringDestructuringPolicy : IDestructuringPolicy { - public class DummyHardCodedStringDestructuringPolicy : IDestructuringPolicy - { - readonly string _hardCodedString; + readonly string _hardCodedString; - public DummyHardCodedStringDestructuringPolicy(string hardCodedString) - { - _hardCodedString = hardCodedString ?? throw new ArgumentNullException(nameof(hardCodedString)); - } + public DummyHardCodedStringDestructuringPolicy(string hardCodedString) + { + _hardCodedString = hardCodedString ?? throw new ArgumentNullException(nameof(hardCodedString)); + } - public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventPropertyValue result) - { - result = new ScalarValue(_hardCodedString); - return true; - } + public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventPropertyValue result) + { + result = new ScalarValue(_hardCodedString); + return true; } } diff --git a/test/TestDummies/DummyLoggerConfigurationExtensions.cs b/test/TestDummies/DummyLoggerConfigurationExtensions.cs index 2148e80b..ea870240 100644 --- a/test/TestDummies/DummyLoggerConfigurationExtensions.cs +++ b/test/TestDummies/DummyLoggerConfigurationExtensions.cs @@ -4,159 +4,199 @@ using Serilog.Core; using Serilog.Events; using Serilog.Formatting; -using System; -using System.Collections.Generic; using TestDummies.Console; using TestDummies.Console.Themes; -namespace TestDummies +namespace TestDummies; + +public static class DummyLoggerConfigurationExtensions { - public static class DummyLoggerConfigurationExtensions + public static LoggerConfiguration WithDummyThreadId(this LoggerEnrichmentConfiguration enrich) { - public static LoggerConfiguration WithDummyThreadId(this LoggerEnrichmentConfiguration enrich) - { - return enrich.With(new DummyThreadIdEnricher()); - } - - public static LoggerConfiguration DummyRollingFile( - this LoggerSinkConfiguration loggerSinkConfiguration, - string pathFormat, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - string outputTemplate = null, - IFormatProvider formatProvider = null) - { - return loggerSinkConfiguration.Sink(new DummyRollingFileSink(), restrictedToMinimumLevel); - } - - public static LoggerConfiguration DummyRollingFile( - this LoggerSinkConfiguration loggerSinkConfiguration, - ITextFormatter formatter, - string pathFormat, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) - { - return loggerSinkConfiguration.Sink(new DummyRollingFileSink(), restrictedToMinimumLevel); - } + return enrich.With(new DummyThreadIdEnricher()); + } - public static LoggerConfiguration DummyWithConfiguration( - this LoggerSinkConfiguration loggerSinkConfiguration, - IConfiguration appConfiguration, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) - { - return loggerSinkConfiguration.Sink(new DummyConfigurationSink(appConfiguration, null), restrictedToMinimumLevel); - } + public static LoggerConfiguration DummyRollingFile( + this LoggerSinkConfiguration loggerSinkConfiguration, + string pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + string? outputTemplate = null, + IFormatProvider? formatProvider = null) + { + return loggerSinkConfiguration.Sink(new DummyRollingFileSink(), restrictedToMinimumLevel); + } - public static LoggerConfiguration DummyWithOptionalConfiguration( - this LoggerSinkConfiguration loggerSinkConfiguration, - IConfiguration appConfiguration = null, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) - { - return loggerSinkConfiguration.Sink(new DummyConfigurationSink(appConfiguration, null), restrictedToMinimumLevel); - } + public static LoggerConfiguration DummyRollingFile( + this LoggerSinkConfiguration loggerSinkConfiguration, + ITextFormatter formatter, + string pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) + { + return loggerSinkConfiguration.Sink(new DummyRollingFileSink(), restrictedToMinimumLevel); + } - public static LoggerConfiguration DummyWithConfigSection( - this LoggerSinkConfiguration loggerSinkConfiguration, - IConfigurationSection configurationSection, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) - { - return loggerSinkConfiguration.Sink(new DummyConfigurationSink(null, configurationSection), restrictedToMinimumLevel); - } - - public static LoggerConfiguration DummyRollingFile( - this LoggerSinkConfiguration loggerSinkConfiguration, - List objectBinding, - string pathFormat, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) - { - return loggerSinkConfiguration.Sink(new DummyRollingFileSink(), restrictedToMinimumLevel); - } + public static LoggerConfiguration DummyWithConfiguration( + this LoggerSinkConfiguration loggerSinkConfiguration, + IConfiguration appConfiguration, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) + { + return loggerSinkConfiguration.Sink(new DummyConfigurationSink(appConfiguration, null), restrictedToMinimumLevel); + } - public class Binding - { - public string Foo { get; set; } + public static LoggerConfiguration DummyWithOptionalConfiguration( + this LoggerSinkConfiguration loggerSinkConfiguration, + IConfiguration? appConfiguration = null, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) + { + return loggerSinkConfiguration.Sink(new DummyConfigurationSink(appConfiguration, null), restrictedToMinimumLevel); + } - public string Abc { get; set; } - } + public static LoggerConfiguration DummyWithConfigSection( + this LoggerSinkConfiguration loggerSinkConfiguration, + IConfigurationSection configurationSection, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) + { + return loggerSinkConfiguration.Sink(new DummyConfigurationSink(null, configurationSection), restrictedToMinimumLevel); + } - public static LoggerConfiguration DummyRollingFile( - this LoggerSinkConfiguration loggerSinkConfiguration, - string[] stringArrayBinding, - string pathFormat, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) - { - return loggerSinkConfiguration.Sink(new DummyRollingFileSink(), restrictedToMinimumLevel); - } - - public static LoggerConfiguration DummyRollingFile( - this LoggerSinkConfiguration loggerSinkConfiguration, - int[] intArrayBinding, - string pathFormat, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) - { - return loggerSinkConfiguration.Sink(new DummyRollingFileSink(), restrictedToMinimumLevel); - } - - public static LoggerConfiguration DummyRollingFile( - this LoggerAuditSinkConfiguration loggerSinkConfiguration, - string pathFormat, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - string outputTemplate = null, - IFormatProvider formatProvider = null) - { - return loggerSinkConfiguration.Sink(new DummyRollingFileAuditSink(), restrictedToMinimumLevel); - } + public static LoggerConfiguration DummyRollingFile( + this LoggerSinkConfiguration loggerSinkConfiguration, + List objectBinding, + string pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) + { + return loggerSinkConfiguration.Sink(new DummyRollingFileSink(), restrictedToMinimumLevel); + } - public static LoggerConfiguration DummyWithLevelSwitch( - this LoggerSinkConfiguration loggerSinkConfiguration, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - LoggingLevelSwitch controlLevelSwitch = null) - { - return loggerSinkConfiguration.Sink(new DummyWithLevelSwitchSink(controlLevelSwitch), restrictedToMinimumLevel); - } - - public static LoggerConfiguration DummyConsole( - this LoggerSinkConfiguration loggerSinkConfiguration, - LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, - LoggingLevelSwitch levelSwitch = null, - ConsoleTheme theme = null) - { - return loggerSinkConfiguration.Sink(new DummyConsoleSink(theme), restrictedToMinimumLevel, levelSwitch); - } + public class Binding + { + public string? Foo { get; set; } - public static LoggerConfiguration Dummy( - this LoggerSinkConfiguration loggerSinkConfiguration, - Action wrappedSinkAction) - { - return LoggerSinkConfiguration.Wrap( - loggerSinkConfiguration, - s => new DummyWrappingSink(s), - wrappedSinkAction, - LogEventLevel.Verbose, - levelSwitch: null); - } - - public static LoggerConfiguration WithDummyHardCodedString( - this LoggerDestructuringConfiguration loggerDestructuringConfiguration, - string hardCodedString - ) - { - return loggerDestructuringConfiguration.With(new DummyHardCodedStringDestructuringPolicy(hardCodedString)); - } - - public static LoggerConfiguration DummyArrayOfType(this LoggerDestructuringConfiguration loggerSinkConfiguration, - List list, - Type[] array = null, - Type type = null, - CustomCollection custom = null, - CustomCollection customString = null) + public string? Abc { get; set; } + } + + public static LoggerConfiguration DummyRollingFile( + this LoggerSinkConfiguration loggerSinkConfiguration, + string[] stringArrayBinding, + string pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) + { + return loggerSinkConfiguration.Sink(new DummyRollingFileSink(), restrictedToMinimumLevel); + } + + public static LoggerConfiguration DummyRollingFile( + this LoggerSinkConfiguration loggerSinkConfiguration, + int[] intArrayBinding, + string pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum) + { + return loggerSinkConfiguration.Sink(new DummyRollingFileSink(), restrictedToMinimumLevel); + } + + public static LoggerConfiguration DummyRollingFile( + this LoggerAuditSinkConfiguration loggerSinkConfiguration, + string pathFormat, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + string? outputTemplate = null, + IFormatProvider? formatProvider = null) + { + return loggerSinkConfiguration.Sink(new DummyRollingFileAuditSink(), restrictedToMinimumLevel); + } + + public static LoggerConfiguration DummyWithLevelSwitch( + this LoggerSinkConfiguration loggerSinkConfiguration, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch? controlLevelSwitch = null) + { + return loggerSinkConfiguration.Sink(new DummyWithLevelSwitchSink(controlLevelSwitch), restrictedToMinimumLevel); + } + + public static LoggerConfiguration DummyConsole( + this LoggerSinkConfiguration loggerSinkConfiguration, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch? levelSwitch = null, + ConsoleTheme? theme = null) + { + return loggerSinkConfiguration.Sink(new DummyConsoleSink(theme), restrictedToMinimumLevel, levelSwitch); + } + + internal static LoggerConfiguration DummyConsoleInternal( + this LoggerSinkConfiguration loggerSinkConfiguration, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch? levelSwitch = null, + ConsoleTheme? theme = null) + { + return loggerSinkConfiguration.Sink(new DummyConsoleSink(theme), restrictedToMinimumLevel, levelSwitch); + } + + public static LoggerConfiguration Dummy( + this LoggerSinkConfiguration loggerSinkConfiguration, + Action wrappedSinkAction) + { + return LoggerSinkConfiguration.Wrap( + loggerSinkConfiguration, + s => new DummyWrappingSink(s), + wrappedSinkAction, + LogEventLevel.Verbose, + levelSwitch: null); + } + + public static LoggerConfiguration WithDummyHardCodedString( + this LoggerDestructuringConfiguration loggerDestructuringConfiguration, + string hardCodedString + ) + { + return loggerDestructuringConfiguration.With(new DummyHardCodedStringDestructuringPolicy(hardCodedString)); + } + + public static LoggerConfiguration DummyArrayOfType(this LoggerDestructuringConfiguration loggerSinkConfiguration, + List list, + Type[]? array = null, + Type? type = null, + CustomCollection? custom = null, + CustomCollection? customString = null) + { + return loggerSinkConfiguration.With(DummyPolicy.Current = new DummyPolicy + { + List = list, + Array = array, + Type = type, + Custom = custom, + CustomStrings = customString, + }); + } + + public static LoggerConfiguration DummyNumbers(this LoggerDestructuringConfiguration loggerSinkConfiguration, + float floatValue, + double doubleValue, + decimal decimalValue) + { + return loggerSinkConfiguration.With(DummyPolicy.Current = new DummyPolicy { - return loggerSinkConfiguration.With(DummyPolicy.Current = new DummyPolicy - { - List = list, - Array = array, - Type = type, - Custom = custom, - CustomStrings = customString, - }); - } + Float = floatValue, + Double = doubleValue, + Decimal = decimalValue, + }); + } +} + +internal static class DummyLoggerConfigurationExtensionsInternal +{ + public static LoggerConfiguration DummyConsolePublicInInternal( + this LoggerSinkConfiguration loggerSinkConfiguration, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch? levelSwitch = null, + ConsoleTheme? theme = null) + { + return loggerSinkConfiguration.Sink(new DummyConsoleSink(theme), restrictedToMinimumLevel, levelSwitch); + } + + internal static LoggerConfiguration DummyConsoleInternalInInternal( + this LoggerSinkConfiguration loggerSinkConfiguration, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch? levelSwitch = null, + ConsoleTheme? theme = null) + { + return loggerSinkConfiguration.Sink(new DummyConsoleSink(theme), restrictedToMinimumLevel, levelSwitch); } } diff --git a/test/TestDummies/DummyPolicy.cs b/test/TestDummies/DummyPolicy.cs index ada4c2f7..2104abfc 100644 --- a/test/TestDummies/DummyPolicy.cs +++ b/test/TestDummies/DummyPolicy.cs @@ -1,48 +1,51 @@ using Serilog.Core; using Serilog.Events; -using System; using System.Collections; -using System.Collections.Generic; -namespace TestDummies +namespace TestDummies; + +public class DummyPolicy : IDestructuringPolicy { - public class DummyPolicy : IDestructuringPolicy - { - public static DummyPolicy Current { get; set; } + public static DummyPolicy? Current { get; set; } - public Type[] Array { get; set; } + public Type[]? Array { get; set; } - public List List { get; set; } + public List? List { get; set; } - public CustomCollection Custom { get; set; } + public CustomCollection? Custom { get; set; } - public CustomCollection CustomStrings { get; set; } + public CustomCollection? CustomStrings { get; set; } - public Type Type { get; set; } + public Type? Type { get; set; } - public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventPropertyValue result) - { - result = null; - return false; - } - } + public float Float { get; set; } + + public double Double { get; set; } + + public decimal Decimal { get; set; } - public class CustomCollection : IEnumerable + public bool TryDestructure(object value, ILogEventPropertyValueFactory propertyValueFactory, out LogEventPropertyValue? result) { - private readonly List inner = new List(); + result = null; + return false; + } +} - public void Add(T item) => inner.Add(item); +public class CustomCollection : IEnumerable +{ + private readonly List inner = new List(); - // wrong signature for collection initializer - public int Add() => 0; + public void Add(T item) => inner.Add(item); - // wrong signature for collection initializer - public void Add(string a, byte b) { } + // wrong signature for collection initializer + public int Add() => 0; - public T First => inner.Count > 0 ? inner[0] : default; + // wrong signature for collection initializer + public void Add(string a, byte b) { } - public IEnumerator GetEnumerator() => inner.GetEnumerator(); + public T? First => inner.Count > 0 ? inner[0] : default; - IEnumerator IEnumerable.GetEnumerator() => inner.GetEnumerator(); - } + public IEnumerator GetEnumerator() => inner.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() => inner.GetEnumerator(); } diff --git a/test/TestDummies/DummyRollingFileAuditSink.cs b/test/TestDummies/DummyRollingFileAuditSink.cs index 2ba17e35..3365ba55 100644 --- a/test/TestDummies/DummyRollingFileAuditSink.cs +++ b/test/TestDummies/DummyRollingFileAuditSink.cs @@ -1,25 +1,22 @@ -using System; -using System.Collections.Generic; -using Serilog.Core; +using Serilog.Core; using Serilog.Events; -namespace TestDummies +namespace TestDummies; + +public class DummyRollingFileAuditSink : ILogEventSink { - public class DummyRollingFileAuditSink : ILogEventSink - { - [ThreadStatic] - static List _emitted; + [ThreadStatic] + static List? _emitted; - public static List Emitted => _emitted ?? (_emitted = new List()); + public static List Emitted => _emitted ??= new List(); - public void Emit(LogEvent logEvent) - { - Emitted.Add(logEvent); - } + public void Emit(LogEvent logEvent) + { + Emitted.Add(logEvent); + } - public static void Reset() - { - _emitted = null; - } + public static void Reset() + { + _emitted = null; } } diff --git a/test/TestDummies/DummyRollingFileSink.cs b/test/TestDummies/DummyRollingFileSink.cs index 2f6f229e..3f8fd0e7 100644 --- a/test/TestDummies/DummyRollingFileSink.cs +++ b/test/TestDummies/DummyRollingFileSink.cs @@ -1,25 +1,22 @@ -using System; -using System.Collections.Generic; -using Serilog.Core; +using Serilog.Core; using Serilog.Events; -namespace TestDummies +namespace TestDummies; + +public class DummyRollingFileSink : ILogEventSink { - public class DummyRollingFileSink : ILogEventSink - { - [ThreadStatic] - static List _emitted; + [ThreadStatic] + static List? _emitted; - public static List Emitted => _emitted ?? (_emitted = new List()); + public static List Emitted => _emitted ??= new List(); - public void Emit(LogEvent logEvent) - { - Emitted.Add(logEvent); - } + public void Emit(LogEvent logEvent) + { + Emitted.Add(logEvent); + } - public static void Reset() - { - _emitted = null; - } + public static void Reset() + { + _emitted = null; } } diff --git a/test/TestDummies/DummyThreadIdEnricher.cs b/test/TestDummies/DummyThreadIdEnricher.cs index a640d55e..c9ae9d86 100644 --- a/test/TestDummies/DummyThreadIdEnricher.cs +++ b/test/TestDummies/DummyThreadIdEnricher.cs @@ -1,14 +1,13 @@ using Serilog.Core; using Serilog.Events; -namespace TestDummies +namespace TestDummies; + +public class DummyThreadIdEnricher : ILogEventEnricher { - public class DummyThreadIdEnricher : ILogEventEnricher + public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) { - public void Enrich(LogEvent logEvent, ILogEventPropertyFactory propertyFactory) - { - logEvent.AddPropertyIfAbsent(propertyFactory - .CreateProperty("ThreadId", "SomeId")); - } + logEvent.AddPropertyIfAbsent(propertyFactory + .CreateProperty("ThreadId", "SomeId")); } } diff --git a/test/TestDummies/DummyWithLevelSwitchSink.cs b/test/TestDummies/DummyWithLevelSwitchSink.cs index 4e1d76fc..2a3fadb4 100644 --- a/test/TestDummies/DummyWithLevelSwitchSink.cs +++ b/test/TestDummies/DummyWithLevelSwitchSink.cs @@ -1,28 +1,25 @@ -using System; -using System.Collections.Generic; -using Serilog.Core; +using Serilog.Core; using Serilog.Events; -namespace TestDummies +namespace TestDummies; + +public class DummyWithLevelSwitchSink : ILogEventSink { - public class DummyWithLevelSwitchSink : ILogEventSink + public DummyWithLevelSwitchSink(LoggingLevelSwitch? loggingControlLevelSwitch) { - public DummyWithLevelSwitchSink(LoggingLevelSwitch loggingControlLevelSwitch) - { - ControlLevelSwitch = loggingControlLevelSwitch; - } + ControlLevelSwitch = loggingControlLevelSwitch; + } - [ThreadStatic] - public static LoggingLevelSwitch ControlLevelSwitch; + [ThreadStatic] + public static LoggingLevelSwitch? ControlLevelSwitch; - [ThreadStatic] - // ReSharper disable ThreadStaticFieldHasInitializer - public static List Emitted = new List(); - // ReSharper restore ThreadStaticFieldHasInitializer + [ThreadStatic] + // ReSharper disable ThreadStaticFieldHasInitializer + public static List Emitted = new List(); + // ReSharper restore ThreadStaticFieldHasInitializer - public void Emit(LogEvent logEvent) - { - Emitted.Add(logEvent); - } + public void Emit(LogEvent logEvent) + { + Emitted.Add(logEvent); } } diff --git a/test/TestDummies/DummyWrappingSink.cs b/test/TestDummies/DummyWrappingSink.cs index cb2f048f..9dc0d35c 100644 --- a/test/TestDummies/DummyWrappingSink.cs +++ b/test/TestDummies/DummyWrappingSink.cs @@ -1,33 +1,30 @@ -using System; -using Serilog.Core; +using Serilog.Core; using Serilog.Events; -using System.Collections.Generic; -namespace TestDummies +namespace TestDummies; + +public class DummyWrappingSink : ILogEventSink { - public class DummyWrappingSink : ILogEventSink - { - [ThreadStatic] - static List _emitted; + [ThreadStatic] + static List? _emitted; - public static List Emitted => _emitted ?? (_emitted = new List()); + public static List Emitted => _emitted ??= new List(); - readonly ILogEventSink _sink; + readonly ILogEventSink _sink; - public DummyWrappingSink(ILogEventSink sink) - { - _sink = sink; - } + public DummyWrappingSink(ILogEventSink sink) + { + _sink = sink; + } - public void Emit(LogEvent logEvent) - { - Emitted.Add(logEvent); - _sink.Emit(logEvent); - } + public void Emit(LogEvent logEvent) + { + Emitted.Add(logEvent); + _sink.Emit(logEvent); + } - public static void Reset() - { - _emitted = null; - } + public static void Reset() + { + _emitted = null; } } diff --git a/test/TestDummies/TestDummies.csproj b/test/TestDummies/TestDummies.csproj index 24f9c252..e410bbb5 100644 --- a/test/TestDummies/TestDummies.csproj +++ b/test/TestDummies/TestDummies.csproj @@ -2,10 +2,6 @@ netstandard2.0;net462 - TestDummies - ../../assets/Serilog.snk - true - true @@ -16,4 +12,8 @@ + + + +