diff --git a/README.md b/README.md index 9a737a0..26d133b 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Serilog.Settings.Configuration [![Build status](https://ci.appveyor.com/api/projects/status/r2bgfimd9ocr61px/branch/master?svg=true)](https://ci.appveyor.com/project/serilog/serilog-settings-configuration/branch/master) +# Serilog.Settings.Configuration [![Build status](https://ci.appveyor.com/api/projects/status/r2bgfimd9ocr61px/branch/master?svg=true)](https://ci.appveyor.com/project/serilog/serilog-settings-configuration/branch/master) [![NuGet Version](http://img.shields.io/nuget/v/Serilog.Settings.Configuration.svg?style=flat)](https://www.nuget.org/packages/Serilog.Settings.Configuration/) A Serilog settings provider that reads from _Microsoft.Extensions.Configuration_, .NET Core's `appsettings.json` file. diff --git a/appveyor.yml b/appveyor.yml index 0e93876..396e0ef 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -10,7 +10,7 @@ artifacts: deploy: - provider: NuGet api_key: - secure: bd9z4P73oltOXudAjPehwp9iDKsPtC+HbgshOrSgoyQKr5xVK+bxJQngrDJkHdY8 + secure: N59tiJECUYpip6tEn0xvdmDAEiP9SIzyLEFLpwiigm/8WhJvBNs13QxzT1/3/JW/ skip_symbols: true on: branch: /^(master|dev)$/ diff --git a/sample/Sample/appsettings.json b/sample/Sample/appsettings.json index 2cbcccc..2771a7f 100644 --- a/sample/Sample/appsettings.json +++ b/sample/Sample/appsettings.json @@ -1,6 +1,7 @@ { "Serilog": { - "Using": ["Serilog.Sinks.Console"], + "Using": [ "Serilog.Sinks.Console" ], + "LevelSwitches": { "$controlSwitch": "Verbose" }, "MinimumLevel": { "Default": "Debug", "Override": { @@ -12,6 +13,7 @@ "Name": "Logger", "Args": { "configureLogger": { + "MinimumLevel": "Verbose", "WriteTo": [ { "Name": "Console", @@ -22,7 +24,8 @@ } ] }, - "restrictedToMinimumLevel": "Debug" + "restrictedToMinimumLevel": "Verbose", + "levelSwitch": "$controlSwitch" } }, "WriteTo:Async": { @@ -39,7 +42,7 @@ ] } }, - "Enrich": ["FromLogContext", "WithMachineName", "WithThreadId"], + "Enrich": [ "FromLogContext", "WithMachineName", "WithThreadId" ], "Properties": { "Application": "Sample" }, diff --git a/src/Serilog.Settings.Configuration/ConfigurationLoggerConfigurationExtensions.cs b/src/Serilog.Settings.Configuration/ConfigurationLoggerConfigurationExtensions.cs index 2de4db4..bc93db1 100644 --- a/src/Serilog.Settings.Configuration/ConfigurationLoggerConfigurationExtensions.cs +++ b/src/Serilog.Settings.Configuration/ConfigurationLoggerConfigurationExtensions.cs @@ -17,7 +17,7 @@ using Microsoft.Extensions.DependencyModel; using Serilog.Configuration; using Serilog.Settings.Configuration; -using System.Reflection; +using Serilog.Settings.Configuration.Assemblies; namespace Serilog { @@ -32,28 +32,53 @@ public static class ConfigurationLoggerConfigurationExtensions public const string DefaultSectionName = "Serilog"; /// - /// Reads logger settings from the provided configuration object using the default section name. Generally this + /// 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, - dependencyContext ?? (Assembly.GetEntryAssembly() != null ? DependencyContext.Default : null))); + 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. @@ -63,6 +88,7 @@ public static LoggerConfiguration Configuration( /// 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, @@ -71,38 +97,57 @@ public static LoggerConfiguration ConfigurationSection( 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, - dependencyContext ?? (Assembly.GetEntryAssembly() != null ? DependencyContext.Default : null))); + assemblyFinder, + configuration: null)); } /// - /// Reads logger settings from the provided configuration object using the default section name. Generally this + /// 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); - if(configurationAssemblySource == ConfigurationAssemblySource.UseLoadedAssemblies) - { - return Configuration(settingConfiguration, configuration); - } - else - { - return settingConfiguration.Settings(new ConfigurationReader(configuration, null)); - } + 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. @@ -111,6 +156,7 @@ public static LoggerConfiguration 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, @@ -119,14 +165,9 @@ public static LoggerConfiguration ConfigurationSection( if (settingConfiguration == null) throw new ArgumentNullException(nameof(settingConfiguration)); if (configSection == null) throw new ArgumentNullException(nameof(configSection)); - if (configurationAssemblySource == ConfigurationAssemblySource.UseLoadedAssemblies) - { - return Configuration(settingConfiguration, configSection); - } - else - { - return settingConfiguration.Settings(new ConfigurationReader(configSection, null)); - } + var assemblyFinder = AssemblyFinder.ForSource(configurationAssemblySource); + + return settingConfiguration.Settings(new ConfigurationReader(configSection, assemblyFinder, configuration: null)); } } } diff --git a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj index a45deb5..e62947a 100644 --- a/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj +++ b/src/Serilog.Settings.Configuration/Serilog.Settings.Configuration.csproj @@ -2,7 +2,7 @@ Microsoft.Extensions.Configuration (appsettings.json) support for Serilog. - 3.0.1 + 3.1.0 Serilog Contributors netstandard2.0;net451;net461 true @@ -15,12 +15,16 @@ serilog;json https://serilog.net/images/serilog-configuration-nuget.png https://github.com/serilog/serilog-settings-configuration - https://www.apache.org/licenses/LICENSE-2.0 + Apache-2.0 https://github.com/serilog/serilog-settings-configuration git Serilog + + $(DefineConstants);PRIVATE_BIN + + diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/AssemblyFinder.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/AssemblyFinder.cs new file mode 100644 index 0000000..4d356a8 --- /dev/null +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/AssemblyFinder.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Reflection; +using Microsoft.Extensions.DependencyModel; + +namespace Serilog.Settings.Configuration.Assemblies +{ + abstract class AssemblyFinder + { + public abstract IReadOnlyList FindAssembliesContainingName(string nameToFind); + + protected static bool IsCaseInsensitiveMatch(string text, string textToFind) + { + return text != null && text.ToLowerInvariant().Contains(textToFind.ToLowerInvariant()); + } + + public static AssemblyFinder Auto() + { + // 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); + } + return new DllScanningAssemblyFinder(); + } + + public static AssemblyFinder ForSource(ConfigurationAssemblySource configurationAssemblySource) + { + switch (configurationAssemblySource) + { + case ConfigurationAssemblySource.UseLoadedAssemblies: + return Auto(); + case ConfigurationAssemblySource.AlwaysScanDllFiles: + return new DllScanningAssemblyFinder(); + default: + throw new ArgumentOutOfRangeException(nameof(configurationAssemblySource), configurationAssemblySource, null); + } + } + + public static AssemblyFinder ForDependencyContext(DependencyContext dependencyContext) + { + return new DependencyContextAssemblyFinder(dependencyContext); + } + } +} diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationAssemblySource.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/ConfigurationAssemblySource.cs similarity index 100% rename from src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationAssemblySource.cs rename to src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/ConfigurationAssemblySource.cs diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DependencyContextAssemblyFinder.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DependencyContextAssemblyFinder.cs new file mode 100644 index 0000000..d7faa77 --- /dev/null +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DependencyContextAssemblyFinder.cs @@ -0,0 +1,28 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Reflection; +using Microsoft.Extensions.DependencyModel; + +namespace Serilog.Settings.Configuration.Assemblies +{ + sealed class DependencyContextAssemblyFinder : AssemblyFinder + { + readonly DependencyContext _dependencyContext; + + public DependencyContextAssemblyFinder(DependencyContext dependencyContext) + { + _dependencyContext = dependencyContext ?? throw new ArgumentNullException(nameof(dependencyContext)); + } + + public override IReadOnlyList FindAssembliesContainingName(string nameToFind) + { + var query = from library in _dependencyContext.RuntimeLibraries + from assemblyName in library.GetDefaultAssemblyNames(_dependencyContext) + where IsCaseInsensitiveMatch(assemblyName.Name, nameToFind) + select assemblyName; + + return query.ToList().AsReadOnly(); + } + } +} diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DllScanningAssemblyFinder.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DllScanningAssemblyFinder.cs new file mode 100644 index 0000000..95584e2 --- /dev/null +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/Assemblies/DllScanningAssemblyFinder.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Reflection; + +namespace Serilog.Settings.Configuration.Assemblies +{ + sealed class DllScanningAssemblyFinder : AssemblyFinder + { + public override IReadOnlyList FindAssembliesContainingName(string nameToFind) + { + var probeDirs = new List(); + + if (!string.IsNullOrEmpty(AppDomain.CurrentDomain.BaseDirectory)) + { + probeDirs.Add(AppDomain.CurrentDomain.BaseDirectory); + +#if PRIVATE_BIN + var privateBinPath = AppDomain.CurrentDomain.SetupInformation.PrivateBinPath; + if (!string.IsNullOrEmpty(privateBinPath)) + { + foreach (var path in privateBinPath.Split(';')) + { + if (Path.IsPathRooted(path)) + { + probeDirs.Add(path); + } + else + { + probeDirs.Add(Path.Combine(AppDomain.CurrentDomain.BaseDirectory, path)); + } + } + } +#endif + } + else + { + probeDirs.Add(Path.GetDirectoryName(typeof(AssemblyFinder).Assembly.Location)); + } + + 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(); + + AssemblyName TryGetAssemblyNameFrom(string path) + { + try + { + 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 39be648..3bf16df 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ConfigurationReader.cs @@ -1,17 +1,17 @@ -using System; +using System; using System.Collections.Generic; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyModel; using Microsoft.Extensions.Primitives; +using System.Text.RegularExpressions; using Serilog.Configuration; using Serilog.Core; using Serilog.Debugging; using Serilog.Events; -using System.Text.RegularExpressions; +using Serilog.Settings.Configuration.Assemblies; namespace Serilog.Settings.Configuration { @@ -19,52 +19,40 @@ class ConfigurationReader : IConfigurationReader { const string LevelSwitchNameRegex = @"^\$[A-Za-z]+[A-Za-z0-9]*$"; - static IConfiguration _configuration; - readonly IConfigurationSection _section; - readonly DependencyContext _dependencyContext; readonly IReadOnlyCollection _configurationAssemblies; + readonly ResolutionContext _resolutionContext; - public ConfigurationReader(IConfiguration configuration, DependencyContext dependencyContext) - { - _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - _section = configuration.GetSection(ConfigurationLoggerConfigurationExtensions.DefaultSectionName); - _dependencyContext = dependencyContext; - _configurationAssemblies = LoadConfigurationAssemblies(); - } - - // Generally the initial call should use IConfiguration rather than IConfigurationSection, otherwise - // IConfiguration parameters in the target methods will not be populated. - public ConfigurationReader(IConfigurationSection configSection, DependencyContext dependencyContext) + public ConfigurationReader(IConfigurationSection configSection, AssemblyFinder assemblyFinder, IConfiguration configuration = null) { _section = configSection ?? throw new ArgumentNullException(nameof(configSection)); - _dependencyContext = dependencyContext; - _configurationAssemblies = LoadConfigurationAssemblies(); + _configurationAssemblies = LoadConfigurationAssemblies(_section, assemblyFinder); + _resolutionContext = new ResolutionContext(configuration); } // Used internally for processing nested configuration sections -- see GetMethodCalls below. - internal ConfigurationReader(IConfigurationSection configSection, IReadOnlyCollection configurationAssemblies, DependencyContext dependencyContext) + internal ConfigurationReader(IConfigurationSection configSection, IReadOnlyCollection configurationAssemblies, ResolutionContext resolutionContext) { _section = configSection ?? throw new ArgumentNullException(nameof(configSection)); - _dependencyContext = dependencyContext; _configurationAssemblies = configurationAssemblies ?? throw new ArgumentNullException(nameof(configurationAssemblies)); + _resolutionContext = resolutionContext ?? throw new ArgumentNullException(nameof(resolutionContext)); } public void Configure(LoggerConfiguration loggerConfiguration) { - var declaredLevelSwitches = ProcessLevelSwitchDeclarations(); - ApplyMinimumLevel(loggerConfiguration, declaredLevelSwitches); - ApplyEnrichment(loggerConfiguration, declaredLevelSwitches); - ApplyFilters(loggerConfiguration, declaredLevelSwitches); - ApplyDestructuring(loggerConfiguration, declaredLevelSwitches); - ApplySinks(loggerConfiguration, declaredLevelSwitches); - ApplyAuditSinks(loggerConfiguration, declaredLevelSwitches); + ProcessLevelSwitchDeclarations(); + + ApplyMinimumLevel(loggerConfiguration); + ApplyEnrichment(loggerConfiguration); + ApplyFilters(loggerConfiguration); + ApplyDestructuring(loggerConfiguration); + ApplySinks(loggerConfiguration); + ApplyAuditSinks(loggerConfiguration); } - IReadOnlyDictionary ProcessLevelSwitchDeclarations() + void ProcessLevelSwitchDeclarations() { var levelSwitchesDirective = _section.GetSection("LevelSwitches"); - var namedSwitches = new Dictionary(); foreach (var levelSwitchDeclaration in levelSwitchesDirective.GetChildren()) { var switchName = levelSwitchDeclaration.Key; @@ -74,6 +62,7 @@ IReadOnlyDictionary ProcessLevelSwitchDeclarations() { throw new FormatException($"\"{switchName}\" is not a valid name for a Level Switch declaration. Level switch must be declared with a '$' sign, like \"LevelSwitches\" : {{\"$switchName\" : \"InitialLevel\"}}"); } + LoggingLevelSwitch newSwitch; if (string.IsNullOrEmpty(switchInitialLevel)) { @@ -84,12 +73,15 @@ IReadOnlyDictionary ProcessLevelSwitchDeclarations() var initialLevel = ParseLogEventLevel(switchInitialLevel); newSwitch = new LoggingLevelSwitch(initialLevel); } - namedSwitches.Add(switchName, newSwitch); + + SubscribeToLoggingLevelChanges(levelSwitchDeclaration, newSwitch); + + // make them available later on when resolving argument values + _resolutionContext.AddLevelSwitch(switchName, newSwitch); } - return namedSwitches; } - void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary declaredLevelSwitches) + void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration) { var minimumLevelDirective = _section.GetSection("MinimumLevel"); @@ -102,7 +94,7 @@ void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration, IReadOnlyDiction var minLevelControlledByDirective = minimumLevelDirective.GetSection("ControlledBy"); if (minLevelControlledByDirective.Value != null) { - var globalMinimumLevelSwitch = declaredLevelSwitches.LookUpSwitchByName(minLevelControlledByDirective.Value); + var globalMinimumLevelSwitch = _resolutionContext.LookUpSwitchByName(minLevelControlledByDirective.Value); // not calling ApplyMinimumLevel local function because here we have a reference to a LogLevelSwitch already loggerConfiguration.MinimumLevel.ControlledBy(globalMinimumLevelSwitch); } @@ -117,7 +109,7 @@ void ApplyMinimumLevel(LoggerConfiguration loggerConfiguration, IReadOnlyDiction } else { - var overrideSwitch = declaredLevelSwitches.LookUpSwitchByName(overridenLevelOrSwitch); + var overrideSwitch = _resolutionContext.LookUpSwitchByName(overridenLevelOrSwitch); // not calling ApplyMinimumLevel local function because here we have a reference to a LogLevelSwitch already loggerConfiguration.MinimumLevel.Override(overridePrefix, overrideSwitch); } @@ -130,71 +122,76 @@ void ApplyMinimumLevel(IConfigurationSection directive, Action - { - if (Enum.TryParse(directive.Value, out minimumLevel)) - levelSwitch.MinimumLevel = minimumLevel; - else - SelfLog.WriteLine($"The value {directive.Value} is not a valid Serilog level."); - }); + SubscribeToLoggingLevelChanges(directive, levelSwitch); } } - void ApplyFilters(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary declaredLevelSwitches) + 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."); + }); + } + + void ApplyFilters(LoggerConfiguration loggerConfiguration) { var filterDirective = _section.GetSection("Filter"); if (filterDirective.GetChildren().Any()) { var methodCalls = GetMethodCalls(filterDirective); - CallConfigurationMethods(methodCalls, FindFilterConfigurationMethods(_configurationAssemblies), loggerConfiguration.Filter, declaredLevelSwitches); + CallConfigurationMethods(methodCalls, FindFilterConfigurationMethods(_configurationAssemblies), loggerConfiguration.Filter); } } - void ApplyDestructuring(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary declaredLevelSwitches) + void ApplyDestructuring(LoggerConfiguration loggerConfiguration) { var destructureDirective = _section.GetSection("Destructure"); if (destructureDirective.GetChildren().Any()) { var methodCalls = GetMethodCalls(destructureDirective); - CallConfigurationMethods(methodCalls, FindDestructureConfigurationMethods(_configurationAssemblies), loggerConfiguration.Destructure, declaredLevelSwitches); + CallConfigurationMethods(methodCalls, FindDestructureConfigurationMethods(_configurationAssemblies), loggerConfiguration.Destructure); } } - void ApplySinks(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary declaredLevelSwitches) + void ApplySinks(LoggerConfiguration loggerConfiguration) { var writeToDirective = _section.GetSection("WriteTo"); if (writeToDirective.GetChildren().Any()) { var methodCalls = GetMethodCalls(writeToDirective); - CallConfigurationMethods(methodCalls, FindSinkConfigurationMethods(_configurationAssemblies), loggerConfiguration.WriteTo, declaredLevelSwitches); + CallConfigurationMethods(methodCalls, FindSinkConfigurationMethods(_configurationAssemblies), loggerConfiguration.WriteTo); } } - void ApplyAuditSinks(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary declaredLevelSwitches) + void ApplyAuditSinks(LoggerConfiguration loggerConfiguration) { var auditToDirective = _section.GetSection("AuditTo"); if (auditToDirective.GetChildren().Any()) { var methodCalls = GetMethodCalls(auditToDirective); - CallConfigurationMethods(methodCalls, FindAuditSinkConfigurationMethods(_configurationAssemblies), loggerConfiguration.AuditTo, declaredLevelSwitches); + CallConfigurationMethods(methodCalls, FindAuditSinkConfigurationMethods(_configurationAssemblies), loggerConfiguration.AuditTo); } } - void IConfigurationReader.ApplySinks(LoggerSinkConfiguration loggerSinkConfiguration, IReadOnlyDictionary declaredLevelSwitches) + void IConfigurationReader.ApplySinks(LoggerSinkConfiguration loggerSinkConfiguration) { var methodCalls = GetMethodCalls(_section); - CallConfigurationMethods(methodCalls, FindSinkConfigurationMethods(_configurationAssemblies), loggerSinkConfiguration, declaredLevelSwitches); + CallConfigurationMethods(methodCalls, FindSinkConfigurationMethods(_configurationAssemblies), loggerSinkConfiguration); } - void ApplyEnrichment(LoggerConfiguration loggerConfiguration, IReadOnlyDictionary declaredLevelSwitches) + void ApplyEnrichment(LoggerConfiguration loggerConfiguration) { var enrichDirective = _section.GetSection("Enrich"); if (enrichDirective.GetChildren().Any()) { var methodCalls = GetMethodCalls(enrichDirective); - CallConfigurationMethods(methodCalls, FindEventEnricherConfigurationMethods(_configurationAssemblies), loggerConfiguration.Enrich, declaredLevelSwitches); + CallConfigurationMethods(methodCalls, FindEventEnricherConfigurationMethods(_configurationAssemblies), loggerConfiguration.Enrich); } var propertiesDirective = _section.GetSection("Properties"); @@ -245,11 +242,11 @@ IConfigurationArgumentValue GetArgumentValue(IConfigurationSection argumentSecti if (argumentSection.Value != null) { - argumentValue = new StringArgumentValue(() => argumentSection.Value, argumentSection.GetReloadToken); + argumentValue = new StringArgumentValue(argumentSection.Value); } else { - argumentValue = new ObjectArgumentValue(argumentSection, _configurationAssemblies, _dependencyContext); + argumentValue = new ObjectArgumentValue(argumentSection, _configurationAssemblies); } return argumentValue; @@ -265,11 +262,11 @@ string GetSectionName(IConfigurationSection s) } } - IReadOnlyCollection LoadConfigurationAssemblies() + static IReadOnlyCollection LoadConfigurationAssemblies(IConfigurationSection section, AssemblyFinder assemblyFinder) { var assemblies = new Dictionary(); - var usingSection = _section.GetSection("Using"); + var usingSection = section.GetSection("Using"); if (usingSection.GetChildren().Any()) { foreach (var simpleName in usingSection.GetChildren().Select(c => c.Value)) @@ -284,7 +281,7 @@ IReadOnlyCollection LoadConfigurationAssemblies() } } - foreach (var assemblyName in GetSerilogConfigurationAssemblies()) + foreach (var assemblyName in assemblyFinder.FindAssembliesContainingName("serilog")) { var assumed = Assembly.Load(assemblyName); if (assumed != null && !assemblies.ContainsKey(assumed.FullName)) @@ -294,76 +291,73 @@ IReadOnlyCollection LoadConfigurationAssemblies() return assemblies.Values.ToList().AsReadOnly(); } - AssemblyName[] GetSerilogConfigurationAssemblies() - { - // ReSharper disable once RedundantAssignment - var query = Enumerable.Empty(); - var filter = new Func(name => name != null && name.ToLowerInvariant().Contains("serilog")); - - if (_dependencyContext != null) - { - query = from library in _dependencyContext.RuntimeLibraries - from assemblyName in library.GetDefaultAssemblyNames(_dependencyContext) - where filter(assemblyName.Name) - select assemblyName; - } - else - { - query = from outputAssemblyPath in System.IO.Directory.GetFiles(AppDomain.CurrentDomain.BaseDirectory, "*.dll") - let assemblyFileName = System.IO.Path.GetFileNameWithoutExtension(outputAssemblyPath) - where filter(assemblyFileName) - select AssemblyName.GetAssemblyName(outputAssemblyPath); - } - - return query.ToArray(); - } - - static void CallConfigurationMethods(ILookup> methods, IList configurationMethods, object receiver, IReadOnlyDictionary declaredLevelSwitches) + void CallConfigurationMethods(ILookup> methods, IList 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); + var methodInfo = SelectConfigurationMethod(configurationMethods, method.Key, method.Value.Keys); if (methodInfo != null) { var call = (from p in methodInfo.GetParameters().Skip(1) - let directive = method.Value.FirstOrDefault(s => s.Key.Equals(p.Name, StringComparison.OrdinalIgnoreCase)) - select directive.Key == null ? p.DefaultValue : directive.Value.ConvertTo(p.ParameterType, declaredLevelSwitches)).ToList(); - - var parm = methodInfo.GetParameters().FirstOrDefault(i => i.ParameterType == typeof(IConfiguration)); - if (parm != null && !parm.HasDefaultValue) - { - if (_configuration is null) - { - 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 '{methodInfo}')"); - } - call[parm.Position - 1] = _configuration; - } + 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()); } } } - internal static MethodInfo SelectConfigurationMethod(IEnumerable candidateMethods, string name, Dictionary suppliedArgumentValues) + static bool HasImplicitValueWhenNotSpecified(ParameterInfo paramInfo) + { + return paramInfo.HasDefaultValue + // parameters of type IConfiguration are implicitly populated with provided Configuration + || paramInfo.ParameterType == typeof(IConfiguration); + } + + object GetImplicitValueForNotSpecifiedKey(ParameterInfo parameter, MethodInfo methodToInvoke) + { + if (!HasImplicitValueWhenNotSpecified(parameter)) + { + 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."); + } + + if (parameter.ParameterType == typeof(IConfiguration)) + { + 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; + } + + internal static MethodInfo SelectConfigurationMethod(IEnumerable candidateMethods, string name, IEnumerable 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). - return candidateMethods - .Where(m => m.Name == name && - m.GetParameters().Skip(1) - .All(p => p.HasDefaultValue - || suppliedArgumentValues.Any(s => s.Key.Equals(p.Name, StringComparison.OrdinalIgnoreCase)) - // parameters of type IConfiguration are implicitly populated with provided Configuration - || p.ParameterType == typeof(IConfiguration) - )) + 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 => suppliedArgumentValues.Any(s => s.Key.Equals(p.Name, StringComparison.OrdinalIgnoreCase))).ToList(); + 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 @@ -372,6 +366,37 @@ internal static MethodInfo SelectConfigurationMethod(IEnumerable can matchingArgs.Count(p => p.ParameterType == typeof(string))); }) .FirstOrDefault(); + + if (selectedMethod == null) + { + 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()) + 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)); + } + + return selectedMethod; + } + + 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 IList FindSinkConfigurationMethods(IReadOnlyCollection configurationAssemblies) diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationArgumentValue.cs index 7ef3e04..e24e8e0 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationArgumentValue.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationArgumentValue.cs @@ -1,11 +1,9 @@ using System; -using System.Collections.Generic; -using Serilog.Core; namespace Serilog.Settings.Configuration { interface IConfigurationArgumentValue { - object ConvertTo(Type toType, IReadOnlyDictionary declaredLevelSwitches); + 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 65f3693..f19f142 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationReader.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/IConfigurationReader.cs @@ -1,11 +1,9 @@ -using System.Collections.Generic; -using Serilog.Configuration; -using Serilog.Core; +using Serilog.Configuration; namespace Serilog.Settings.Configuration { interface IConfigurationReader : ILoggerSettings { - void ApplySinks(LoggerSinkConfiguration loggerSinkConfiguration, IReadOnlyDictionary declaredLevelSwitches); + void ApplySinks(LoggerSinkConfiguration loggerSinkConfiguration); } } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/LevelSwitchDictionaryExtensions.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/LevelSwitchDictionaryExtensions.cs deleted file mode 100644 index e7743cf..0000000 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/LevelSwitchDictionaryExtensions.cs +++ /dev/null @@ -1,27 +0,0 @@ -using System; -using System.Collections.Generic; -using Serilog.Core; - -namespace Serilog.Settings.Configuration -{ - internal static class LevelSwitchDictionaryExtensions - { - /// - /// Looks up a switch in the declared LoggingLevelSwitches - /// - /// the dictionary of switches to look up by name - /// the name of a switch to look up - /// the LoggingLevelSwitch registered with the name - /// if no switch has been registered with - public static LoggingLevelSwitch LookUpSwitchByName(this IReadOnlyDictionary namedLevelSwitches, string switchName) - { - if (namedLevelSwitches == null) throw new ArgumentNullException(nameof(namedLevelSwitches)); - if (namedLevelSwitches.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\"}}"); - } - } -} diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs index 8242476..6b54c34 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ObjectArgumentValue.cs @@ -1,57 +1,54 @@ using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.DependencyModel; -using Serilog.Configuration; -using Serilog.Core; using System; using System.Collections.Generic; using System.Reflection; +using Serilog.Configuration; + namespace Serilog.Settings.Configuration { class ObjectArgumentValue : IConfigurationArgumentValue { - readonly IConfigurationSection section; - readonly IReadOnlyCollection configurationAssemblies; - readonly DependencyContext dependencyContext; + readonly IConfigurationSection _section; + readonly IReadOnlyCollection _configurationAssemblies; - public ObjectArgumentValue(IConfigurationSection section, IReadOnlyCollection configurationAssemblies, DependencyContext dependencyContext) + public ObjectArgumentValue(IConfigurationSection section, IReadOnlyCollection configurationAssemblies) { - this.section = section; + _section = section ?? throw new ArgumentNullException(nameof(section)); // used by nested logger configurations to feed a new pass by ConfigurationReader - this.configurationAssemblies = configurationAssemblies; - this.dependencyContext = dependencyContext; + _configurationAssemblies = configurationAssemblies ?? throw new ArgumentNullException(nameof(configurationAssemblies)); } - public object ConvertTo(Type toType, IReadOnlyDictionary declaredLevelSwitches) + public object ConvertTo(Type toType, ResolutionContext resolutionContext) { // return the entire section for internal processing - if(toType == typeof(IConfigurationSection)) return section; + if (toType == typeof(IConfigurationSection)) return _section; // process a nested configuration to populate an Action<> logger/sink config parameter? var typeInfo = toType.GetTypeInfo(); - if(typeInfo.IsGenericType && + if (typeInfo.IsGenericType && typeInfo.GetGenericTypeDefinition() is Type genericType && genericType == typeof(Action<>)) { var configType = typeInfo.GenericTypeArguments[0]; - if(configType != typeof(LoggerConfiguration) && configType != typeof(LoggerSinkConfiguration)) + if (configType != typeof(LoggerConfiguration) && configType != typeof(LoggerSinkConfiguration)) throw new ArgumentException($"Configuration for Action<{configType}> is not implemented."); - IConfigurationReader configReader = new ConfigurationReader(section, configurationAssemblies, dependencyContext); + IConfigurationReader configReader = new ConfigurationReader(_section, _configurationAssemblies, resolutionContext); - if(configType == typeof(LoggerConfiguration)) + if (configType == typeof(LoggerConfiguration)) { return new Action(configReader.Configure); } - if(configType == typeof(LoggerSinkConfiguration)) + if (configType == typeof(LoggerSinkConfiguration)) { - return new Action(loggerSinkConfig => configReader.ApplySinks(loggerSinkConfig, declaredLevelSwitches)); + return new Action(loggerSinkConfig => configReader.ApplySinks(loggerSinkConfig)); } } // MS Config binding - return section.Get(toType); + return _section.Get(toType); } } } diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/ResolutionContext.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/ResolutionContext.cs new file mode 100644 index 0000000..90ab06d --- /dev/null +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/ResolutionContext.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; + +using Serilog.Core; + +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 IConfiguration _appConfiguration; + + public ResolutionContext(IConfiguration appConfiguration = null) + { + _declaredLevelSwitches = new Dictionary(); + _appConfiguration = appConfiguration; + } + + /// + /// 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 LookUpSwitchByName(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\"}}"); + } + + public bool HasAppConfiguration => _appConfiguration != null; + + public IConfiguration AppConfiguration + { + get + { + if (!HasAppConfiguration) + { + throw new InvalidOperationException("AppConfiguration is not available"); + } + + return _appConfiguration; + } + } + + public void AddLevelSwitch(string levelSwitchName, LoggingLevelSwitch levelSwitch) + { + if (levelSwitchName == null) throw new ArgumentNullException(nameof(levelSwitchName)); + if (levelSwitch == null) throw new ArgumentNullException(nameof(levelSwitch)); + _declaredLevelSwitches[levelSwitchName] = levelSwitch; + } + } +} diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs index 5291f2a..0a7c09b 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs @@ -3,25 +3,20 @@ using System.Linq; using System.Reflection; using System.Text.RegularExpressions; -using Microsoft.Extensions.Primitives; using Serilog.Core; -using Serilog.Debugging; -using Serilog.Events; namespace Serilog.Settings.Configuration { class StringArgumentValue : IConfigurationArgumentValue { - readonly Func _valueProducer; - readonly Func _changeTokenProducer; + readonly string _providedValue; static readonly Regex StaticMemberAccessorRegex = new Regex("^(?[^:]+)::(?[A-Za-z][A-Za-z0-9]*)(?[^:]*)$"); - public StringArgumentValue(Func valueProducer, Func changeTokenProducer = null) + public StringArgumentValue(string providedValue) { - _valueProducer = valueProducer ?? throw new ArgumentNullException(nameof(valueProducer)); - _changeTokenProducer = changeTokenProducer; + _providedValue = providedValue ?? throw new ArgumentNullException(nameof(providedValue)); } static readonly Dictionary> ExtendedTypeConversions = new Dictionary> @@ -31,13 +26,13 @@ public StringArgumentValue(Func valueProducer, Func change { typeof(Type), s => Type.GetType(s, throwOnError:true) }, }; - public object ConvertTo(Type toType, IReadOnlyDictionary declaredLevelSwitches) + public object ConvertTo(Type toType, ResolutionContext resolutionContext) { - var argumentValue = Environment.ExpandEnvironmentVariables(_valueProducer()); + var argumentValue = Environment.ExpandEnvironmentVariables(_providedValue); if (toType == typeof(LoggingLevelSwitch)) { - return declaredLevelSwitches.LookUpSwitchByName(argumentValue); + return resolutionContext.LookUpSwitchByName(argumentValue); } var toTypeInfo = toType.GetTypeInfo(); @@ -97,7 +92,7 @@ public object ConvertTo(Type toType, IReadOnlyDictionary @@ -114,32 +109,21 @@ public object ConvertTo(Type toType, IReadOnlyDictionary - { - var newArgumentValue = _valueProducer(); - - if (Enum.TryParse(newArgumentValue, out minimumLevel)) - levelSwitch.MinimumLevel = minimumLevel; - else - SelfLog.WriteLine($"The value `{newArgumentValue}` is not a valid Serilog level."); - }); + type = Type.GetType($"{typeName}, Serilog"); } - - return levelSwitch; } - return Convert.ChangeType(argumentValue, toType); + return type; } internal static bool TryParseStaticMemberAccessor(string input, out string accessorTypeName, out string memberName) diff --git a/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs b/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs index 6350eca..bbde10d 100644 --- a/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs +++ b/src/Serilog.Settings.Configuration/Settings/Configuration/SurrogateConfigurationMethods.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; + using Serilog.Configuration; using Serilog.Core; using Serilog.Events; diff --git a/test/Serilog.Settings.Configuration.Tests/ConfigurationReaderTests.cs b/test/Serilog.Settings.Configuration.Tests/ConfigurationReaderTests.cs index 701cfee..b24007b 100644 --- a/test/Serilog.Settings.Configuration.Tests/ConfigurationReaderTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ConfigurationReaderTests.cs @@ -1,9 +1,8 @@ -using System.Collections.Generic; -using Serilog.Formatting; -using Xunit; +using Xunit; using System.Reflection; using System.Linq; -using Serilog.Core; +using Serilog.Formatting; +using Serilog.Settings.Configuration.Assemblies; using Serilog.Settings.Configuration.Tests.Support; namespace Serilog.Settings.Configuration.Tests @@ -14,7 +13,9 @@ public class ConfigurationReaderTests public ConfigurationReaderTests() { - _configurationReader = new ConfigurationReader(JsonStringConfigSource.LoadSection(@"{ 'Serilog': { } }", "Serilog"), null); + _configurationReader = new ConfigurationReader( + JsonStringConfigSource.LoadSection(@"{ 'Serilog': { } }", "Serilog"), + AssemblyFinder.ForSource(ConfigurationAssemblySource.UseLoadedAssemblies)); } [Fact] @@ -75,7 +76,7 @@ public void WriteToSupportExpandedSyntaxWithArgs() Assert.Equal(1, args.Length); Assert.Equal("outputTemplate", args[0].Key); - Assert.Equal("{Message}", args[0].Value.ConvertTo(typeof(string), new Dictionary())); + Assert.Equal("{Message}", args[0].Value.ConvertTo(typeof(string), new ResolutionContext())); } [Fact] @@ -142,12 +143,9 @@ public void CallableMethodsAreSelected() { var options = typeof(DummyLoggerConfigurationExtensions).GetTypeInfo().DeclaredMethods.ToList(); Assert.Equal(2, options.Count(mi => mi.Name == "DummyRollingFile")); - var suppliedArguments = new Dictionary - { - {"pathFormat", new StringArgumentValue(() => "C:\\") } - }; + var suppliedArgumentNames = new[] { "pathFormat" }; - var selected = ConfigurationReader.SelectConfigurationMethod(options, "DummyRollingFile", suppliedArguments); + var selected = ConfigurationReader.SelectConfigurationMethod(options, "DummyRollingFile", suppliedArgumentNames); Assert.Equal(typeof(string), selected.GetParameters()[1].ParameterType); } @@ -156,13 +154,10 @@ public void MethodsAreSelectedBasedOnCountOfMatchedArguments() { var options = typeof(DummyLoggerConfigurationExtensions).GetTypeInfo().DeclaredMethods.ToList(); Assert.Equal(2, options.Count(mi => mi.Name == "DummyRollingFile")); - var suppliedArguments = new Dictionary() - { - { "pathFormat", new StringArgumentValue(() => "C:\\") }, - { "formatter", new StringArgumentValue(() => "SomeFormatter, SomeAssembly") } - }; - var selected = ConfigurationReader.SelectConfigurationMethod(options, "DummyRollingFile", suppliedArguments); + var suppliedArgumentNames = new[] { "pathFormat", "formatter" }; + + var selected = ConfigurationReader.SelectConfigurationMethod(options, "DummyRollingFile", suppliedArgumentNames); Assert.Equal(typeof(ITextFormatter), selected.GetParameters()[1].ParameterType); } @@ -171,13 +166,10 @@ public void MethodsAreSelectedBasedOnCountOfMatchedArgumentsAndThenStringType() { var options = typeof(DummyLoggerConfigurationWithMultipleMethodsExtensions).GetTypeInfo().DeclaredMethods.ToList(); Assert.Equal(3, options.Count(mi => mi.Name == "DummyRollingFile")); - var suppliedArguments = new Dictionary() - { - { "pathFormat", new StringArgumentValue(() => "C:\\") }, - { "formatter", new StringArgumentValue(() => "SomeFormatter, SomeAssembly") } - }; - var selected = ConfigurationReader.SelectConfigurationMethod(options, "DummyRollingFile", suppliedArguments); + var suppliedArgumentNames = new[] { "pathFormat", "formatter" }; + + var selected = ConfigurationReader.SelectConfigurationMethod(options, "DummyRollingFile", suppliedArgumentNames); Assert.Equal(typeof(string), selected.GetParameters()[2].ParameterType); } } diff --git a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs index d5c5369..eed3803 100644 --- a/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/ConfigurationSettingsTests.cs @@ -13,13 +13,23 @@ namespace Serilog.Settings.Configuration.Tests public class ConfigurationSettingsTests { static LoggerConfiguration ConfigFromJson(string jsonString, string secondJsonSource = null) + { + return ConfigFromJson(jsonString, secondJsonSource, out _); + } + + static LoggerConfiguration ConfigFromJson(string jsonString, out IConfiguration configuration) + { + return ConfigFromJson(jsonString, null, out configuration); + } + + static LoggerConfiguration ConfigFromJson(string jsonString, string secondJsonSource, out IConfiguration configuration) { var builder = new ConfigurationBuilder().AddJsonString(jsonString); if (secondJsonSource != null) builder.AddJsonString(secondJsonSource); - var config = builder.Build(); + configuration = builder.Build(); return new LoggerConfiguration() - .ReadFrom.Configuration(config); + .ReadFrom.Configuration(configuration); } [Fact] @@ -184,6 +194,40 @@ public void TestMinimumLevelOverrides() Assert.NotNull(evt); } + [Fact] + public void TestMinimumLevelOverridesForChildContext() + { + var json = @"{ + ""Serilog"": { + ""MinimumLevel"" : { + ""Default"" : ""Warning"", + ""Override"" : { + ""System"" : ""Warning"", + ""System.Threading"": ""Debug"" + } + } + } + }"; + + LogEvent evt = null; + + var log = ConfigFromJson(json) + .WriteTo.Sink(new DelegatingSink(e => evt = e)) + .CreateLogger(); + + 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); + + evt = null; + var systemThreadingLogger = log.ForContext(); + systemThreadingLogger.Write(Some.DebugEvent()); + Assert.NotNull(evt); + } + [Fact] public void SinksWithAbstractParamsAreConfiguredWithTypeName() { @@ -419,7 +463,7 @@ public void LoggingLevelSwitchCanBeUsedForMinimumLevelOverrides() } [Fact] - + [Trait("BugFix", "https://github.com/serilog/serilog-settings-configuration/issues/142")] public void SinkWithIConfigurationArguments() { @@ -434,14 +478,15 @@ public void SinkWithIConfigurationArguments() }"; DummyConfigurationSink.Reset(); - var log = ConfigFromJson(json) + var log = ConfigFromJson(json, out var expectedConfig) .CreateLogger(); log.Write(Some.InformationEvent()); Assert.NotNull(DummyConfigurationSink.Configuration); + Assert.Same(expectedConfig, DummyConfigurationSink.Configuration); } - + [Fact] [Trait("BugFix", "https://github.com/serilog/serilog-settings-configuration/issues/142")] public void SinkWithOptionalIConfigurationArguments() @@ -457,15 +502,16 @@ public void SinkWithOptionalIConfigurationArguments() }"; DummyConfigurationSink.Reset(); - var log = ConfigFromJson(json) + var log = ConfigFromJson(json, out var expectedConfig) .CreateLogger(); log.Write(Some.InformationEvent()); - // null is the default value - Assert.Null(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() { @@ -484,7 +530,7 @@ public void SinkWithIConfigSectionArguments() .CreateLogger(); log.Write(Some.InformationEvent()); - + Assert.NotNull(DummyConfigurationSink.ConfigSection); Assert.Equal("bar", DummyConfigurationSink.ConfigSection["foo"]); } @@ -763,7 +809,7 @@ public void DestructureLimitsCollectionCount() Assert.DoesNotContain("4", msg); } - private string GetDestructuredProperty(object x, string json) + private static string GetDestructuredProperty(object x, string json) { LogEvent evt = null; var log = ConfigFromJson(json) diff --git a/test/Serilog.Settings.Configuration.Tests/DllScanningAssemblyFinderTests.cs b/test/Serilog.Settings.Configuration.Tests/DllScanningAssemblyFinderTests.cs new file mode 100644 index 0000000..d8ab19a --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/DllScanningAssemblyFinderTests.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; + +using Xunit; + +using Serilog.Settings.Configuration.Assemblies; + +namespace Serilog.Settings.Configuration.Tests +{ + public class DllScanningAssemblyFinderTests : IDisposable + { + const string BinDir1 = "bin1"; + const string BinDir2 = "bin2"; + const string BinDir3 = "bin3"; + + readonly string _privateBinPath; + + public DllScanningAssemblyFinderTests() + { + var d1 = GetOrCreateDirectory(BinDir1); + var d2 = GetOrCreateDirectory(BinDir2); + var d3 = GetOrCreateDirectory(BinDir3); + + _privateBinPath = $"{d1.Name};{d2.FullName};{d3.Name}"; + + DirectoryInfo GetOrCreateDirectory(string name) + => Directory.Exists(name) ? new DirectoryInfo(name) : Directory.CreateDirectory(name); + } + + public void Dispose() + { + Directory.Delete(BinDir1, true); + Directory.Delete(BinDir2, true); + Directory.Delete(BinDir3, true); + } + + [Fact] + public void ShouldProbeCurrentDirectory() + { + var assemblyNames = new DllScanningAssemblyFinder().FindAssembliesContainingName("testdummies"); + Assert.Single(assemblyNames); + } + +#if PRIVATE_BIN + [Fact] + public void ShouldProbePrivateBinPath() + { + File.Copy("testdummies.dll", $"{BinDir1}/customSink1.dll", true); + File.Copy("testdummies.dll", $"{BinDir2}/customSink2.dll", true); + File.Copy("testdummies.dll", $"{BinDir3}/thirdpartydependency.dll", true); + + var ad = AppDomain.CreateDomain("serilog", null, + new AppDomainSetup + { + ApplicationBase = AppDomain.CurrentDomain.BaseDirectory, + PrivateBinPath = _privateBinPath + }); + + try + { + ad.DoCallBack(DoTestInner); + } + finally + { + AppDomain.Unload(ad); + } + + void DoTestInner() + { + var assemblyNames = new DllScanningAssemblyFinder().FindAssembliesContainingName("customSink"); + Assert.Equal(2, assemblyNames.Count); + } + } +#endif + } +} diff --git a/test/Serilog.Settings.Configuration.Tests/DynamicLevelChangeTests.cs b/test/Serilog.Settings.Configuration.Tests/DynamicLevelChangeTests.cs new file mode 100644 index 0000000..b1b1f43 --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/DynamicLevelChangeTests.cs @@ -0,0 +1,90 @@ +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 +{ + public class DynamicLevelChangeTests + { + const string DefaultConfig = @"{ + 'Serilog': { + 'Using': [ 'TestDummies' ], + 'MinimumLevel': { + 'Default': 'Information', + 'Override': { + 'Root.Test': 'Information' + } + }, + 'LevelSwitches': { '$mySwitch': 'Information' }, + 'WriteTo:Dummy': { + 'Name': 'DummyConsole', + 'Args': { + 'levelSwitch': '$mySwitch' + } + } + } + }"; + + 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); + } + } + + void UpdateConfig(LogEventLevel? minimumLevel = null, LogEventLevel? switchLevel = null, LogEventLevel? overrideLevel = null) + { + 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()); + } + + _configSource.Reload(); + } + } +} diff --git a/test/Serilog.Settings.Configuration.Tests/LoggerConfigurationExtensionsTests.cs b/test/Serilog.Settings.Configuration.Tests/LoggerConfigurationExtensionsTests.cs index 80d64b0..e2f679e 100644 --- a/test/Serilog.Settings.Configuration.Tests/LoggerConfigurationExtensionsTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/LoggerConfigurationExtensionsTests.cs @@ -35,8 +35,10 @@ public void ReadFromConfigurationSectionReadsFromAnArbitrarySection() .AddJsonString(json) .Build(); +#pragma warning disable CS0618 // Type or member is obsolete 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(); @@ -46,7 +48,7 @@ public void ReadFromConfigurationSectionReadsFromAnArbitrarySection() Assert.Equal("Test", evt.Properties["App"].LiteralValue()); } - [Fact(Skip = "Passes when run alone, but fails when the whole suite is run - to fix")] + [Fact] [Trait("BugFix", "https://github.com/serilog/serilog-settings-configuration/issues/143")] public void ReadFromConfigurationSectionThrowsWhenTryingToCallConfigurationMethodWithIConfigurationParam() { @@ -65,17 +67,42 @@ public void ReadFromConfigurationSectionThrowsWhenTryingToCallConfigurationMetho .Build(); var exception = Assert.Throws(() => +#pragma warning disable CS0618 // Type or member is obsolete 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, Microsoft.Extensions.Configuration.IConfigurationSection, System.String, Serilog.Events.LogEventLevel)')", + "(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"": {} + }] + } + }"; + + var config = new ConfigurationBuilder() + .AddJsonString(json) + .Build(); + + var exception = new LoggerConfiguration() + .ReadFrom.Configuration(config, "NotSerilog") + .CreateLogger(); + + } + [Fact] [Trait("BugFix", "https://github.com/serilog/serilog-settings-configuration/issues/143")] public void ReadFromConfigurationSectionDoesNotThrowWhenTryingToCallConfigurationMethodWithOptionalIConfigurationParam() @@ -95,8 +122,10 @@ public void ReadFromConfigurationSectionDoesNotThrowWhenTryingToCallConfiguratio .Build(); // 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")) +#pragma warning restore CS0618 // Type or member is obsolete .CreateLogger(); } 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 3a88c1b..69ceb38 100644 --- a/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj +++ b/test/Serilog.Settings.Configuration.Tests/Serilog.Settings.Configuration.Tests.csproj @@ -19,6 +19,10 @@ true + + $(DefineConstants);PRIVATE_BIN + + diff --git a/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs b/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs index e432eda..a58cee8 100644 --- a/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs +++ b/test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs @@ -1,5 +1,4 @@ using System; -using System.Collections.Generic; using Serilog.Core; using Serilog.Events; using Serilog.Formatting; @@ -16,9 +15,9 @@ public class StringArgumentValueTests [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 Dictionary()); + var result = stringArgumentValue.ConvertTo(typeof(ITextFormatter), new ResolutionContext()); Assert.IsType(result); } @@ -26,9 +25,9 @@ public void StringValuesConvertToDefaultInstancesIfTargetIsInterface() [Fact] public void StringValuesConvertToDefaultInstancesIfTargetIsAbstractClass() { - var stringArgumentValue = new StringArgumentValue(() => "Serilog.Settings.Configuration.Tests.Support.ConcreteClass, Serilog.Settings.Configuration.Tests"); + var stringArgumentValue = new StringArgumentValue("Serilog.Settings.Configuration.Tests.Support.ConcreteClass, Serilog.Settings.Configuration.Tests"); - var result = stringArgumentValue.ConvertTo(typeof(AbstractClass), new Dictionary()); + var result = stringArgumentValue.ConvertTo(typeof(AbstractClass), new ResolutionContext()); Assert.IsType(result); } @@ -75,16 +74,26 @@ public void TryParseStaticMemberAccessorReturnsExpectedResults(string input, str } } + [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); + } + [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))] - private void StaticMembersAccessorsCanBeUsedForReferenceTypes(string input, Type targetType) + public void StaticMembersAccessorsCanBeUsedForReferenceTypes(string input, Type targetType) { - var stringArgumentValue = new StringArgumentValue(() => $"{input}"); + var stringArgumentValue = new StringArgumentValue($"{input}"); - var actual = stringArgumentValue.ConvertTo(targetType, new Dictionary()); + var actual = stringArgumentValue.ConvertTo(targetType, new ResolutionContext()); Assert.IsAssignableFrom(targetType, actual); Assert.Equal(ConcreteImpl.Instance, actual); @@ -99,9 +108,9 @@ private void StaticMembersAccessorsCanBeUsedForReferenceTypes(string input, Type [InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InterfaceProperty", typeof(IAmAnInterface))] public void StaticAccessorOnUnknownTypeThrowsTypeLoadException(string input, Type targetType) { - var stringArgumentValue = new StringArgumentValue(() => $"{input}"); + var stringArgumentValue = new StringArgumentValue($"{input}"); Assert.Throws(() => - stringArgumentValue.ConvertTo(targetType, new Dictionary()) + stringArgumentValue.ConvertTo(targetType, new ResolutionContext()) ); } @@ -118,9 +127,9 @@ public void StaticAccessorOnUnknownTypeThrowsTypeLoadException(string input, Typ [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 stringArgumentValue = new StringArgumentValue($"{input}"); var exception = Assert.Throws(() => - stringArgumentValue.ConvertTo(targetType, new Dictionary()) + stringArgumentValue.ConvertTo(targetType, new ResolutionContext()) ); Assert.Contains("Could not find a public static property or field ", exception.Message); @@ -132,14 +141,12 @@ public void LevelSwitchesCanBeLookedUpByName() { var @switch = new LoggingLevelSwitch(LogEventLevel.Verbose); var switchName = "$theSwitch"; - var declaredSwitches = new Dictionary() - { - {switchName, @switch } - }; + var resolutionContext = new ResolutionContext(); + resolutionContext.AddLevelSwitch(switchName, @switch); - var stringArgumentValue = new StringArgumentValue(() => switchName); + var stringArgumentValue = new StringArgumentValue(switchName); - var resolvedSwitch = stringArgumentValue.ConvertTo(typeof(LoggingLevelSwitch), declaredSwitches); + var resolvedSwitch = stringArgumentValue.ConvertTo(typeof(LoggingLevelSwitch), resolutionContext); Assert.IsType(resolvedSwitch); Assert.Same(@switch, resolvedSwitch); @@ -149,15 +156,13 @@ public void LevelSwitchesCanBeLookedUpByName() [Fact] public void ReferencingUndeclaredLevelSwitchThrows() { - var declaredSwitches = new Dictionary() - { - {"$anotherSwitch", new LoggingLevelSwitch(LogEventLevel.Verbose) } - }; + var resolutionContext = new ResolutionContext(); + resolutionContext.AddLevelSwitch("$anotherSwitch", new LoggingLevelSwitch(LogEventLevel.Verbose)); - var stringArgumentValue = new StringArgumentValue(() => "$mySwitch"); + var stringArgumentValue = new StringArgumentValue("$mySwitch"); var ex = Assert.Throws(() => - stringArgumentValue.ConvertTo(typeof(LoggingLevelSwitch), declaredSwitches) + stringArgumentValue.ConvertTo(typeof(LoggingLevelSwitch), resolutionContext) ); Assert.Contains("$mySwitch", ex.Message); @@ -168,9 +173,9 @@ public void ReferencingUndeclaredLevelSwitchThrows() public void StringValuesConvertToTypeFromShortTypeName() { var shortTypeName = "System.Version"; - var stringArgumentValue = new StringArgumentValue(() => shortTypeName); + var stringArgumentValue = new StringArgumentValue(shortTypeName); - var actual = (Type)stringArgumentValue.ConvertTo(typeof(Type), new Dictionary()); + var actual = (Type)stringArgumentValue.ConvertTo(typeof(Type), new ResolutionContext()); Assert.Equal(typeof(Version), actual); } @@ -179,9 +184,9 @@ public void StringValuesConvertToTypeFromShortTypeName() public void StringValuesConvertToTypeFromAssemblyQualifiedName() { var assemblyQualifiedName = typeof(Version).AssemblyQualifiedName; - var stringArgumentValue = new StringArgumentValue(() => assemblyQualifiedName); + var stringArgumentValue = new StringArgumentValue(assemblyQualifiedName); - var actual = (Type)stringArgumentValue.ConvertTo(typeof(Type), new Dictionary()); + var actual = (Type)stringArgumentValue.ConvertTo(typeof(Type), new ResolutionContext()); Assert.Equal(typeof(Version), actual); } diff --git a/test/Serilog.Settings.Configuration.Tests/Support/JsonStringConfigSource.cs b/test/Serilog.Settings.Configuration.Tests/Support/JsonStringConfigSource.cs index 2269491..31cf8ac 100644 --- a/test/Serilog.Settings.Configuration.Tests/Support/JsonStringConfigSource.cs +++ b/test/Serilog.Settings.Configuration.Tests/Support/JsonStringConfigSource.cs @@ -1,8 +1,9 @@ -using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Configuration.Json; - +using System.Collections.Generic; using System.IO; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Configuration.Json; + namespace Serilog.Settings.Configuration.Tests.Support { class JsonStringConfigSource : IConfigurationSource @@ -24,6 +25,13 @@ 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; @@ -33,6 +41,8 @@ public JsonStringConfigProvider(string json) : base(new JsonConfigurationSource _json = json; } + public new IDictionary Data => base.Data; + public override void Load() { Load(StringToStream(_json)); diff --git a/test/Serilog.Settings.Configuration.Tests/Support/ReloadableConfigurationSource.cs b/test/Serilog.Settings.Configuration.Tests/Support/ReloadableConfigurationSource.cs new file mode 100644 index 0000000..8f90b34 --- /dev/null +++ b/test/Serilog.Settings.Configuration.Tests/Support/ReloadableConfigurationSource.cs @@ -0,0 +1,37 @@ +using System.Collections.Generic; +using Microsoft.Extensions.Configuration; + +namespace Serilog.Settings.Configuration.Tests.Support +{ + class ReloadableConfigurationSource : IConfigurationSource + { + readonly ReloadableConfigurationProvider _configProvider; + readonly IDictionary _source; + + public ReloadableConfigurationSource(IDictionary source) + { + _source = source; + _configProvider = new ReloadableConfigurationProvider(source); + } + + public IConfigurationProvider Build(IConfigurationBuilder builder) => _configProvider; + + public void Reload() => _configProvider.Reload(); + + public void Set(string key, string value) => _source[key] = value; + + class ReloadableConfigurationProvider : ConfigurationProvider + { + readonly IDictionary _source; + + public ReloadableConfigurationProvider(IDictionary source) + { + _source = source; + } + + public override void Load() => Data = _source; + + public void Reload() => OnReload(); + } + } +} diff --git a/test/TestDummies/Console/DummyConsoleSink.cs b/test/TestDummies/Console/DummyConsoleSink.cs index e03de1f..a286bf0 100644 --- a/test/TestDummies/Console/DummyConsoleSink.cs +++ b/test/TestDummies/Console/DummyConsoleSink.cs @@ -17,9 +17,9 @@ public DummyConsoleSink(ConsoleTheme theme = null) public static ConsoleTheme Theme; [ThreadStatic] - // ReSharper disable ThreadStaticFieldHasInitializer - public static List Emitted = new List(); - // ReSharper restore ThreadStaticFieldHasInitializer + static List EmittedList = new List(); + + public static List Emitted => EmittedList ?? (EmittedList = new List()); public void Emit(LogEvent logEvent) { diff --git a/test/TestDummies/DummyLoggerConfigurationExtensions.cs b/test/TestDummies/DummyLoggerConfigurationExtensions.cs index ec41b34..ecae9cd 100644 --- a/test/TestDummies/DummyLoggerConfigurationExtensions.cs +++ b/test/TestDummies/DummyLoggerConfigurationExtensions.cs @@ -109,9 +109,10 @@ public static LoggerConfiguration DummyWithLevelSwitch( public static LoggerConfiguration DummyConsole( this LoggerSinkConfiguration loggerSinkConfiguration, LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + LoggingLevelSwitch levelSwitch = null, ConsoleTheme theme = null) { - return loggerSinkConfiguration.Sink(new DummyConsoleSink(theme), restrictedToMinimumLevel); + return loggerSinkConfiguration.Sink(new DummyConsoleSink(theme), restrictedToMinimumLevel, levelSwitch); } public static LoggerConfiguration Dummy(