From a20dce5651d3fada10b67e366b7676b8818c8531 Mon Sep 17 00:00:00 2001 From: Thibaud DESODT Date: Fri, 17 Nov 2017 07:26:38 +0100 Subject: [PATCH] Add support for reference to public static properties in settings as a value for parameters, using the syntax `NameSpace.To.ConcreteType::StaticProperty, AssemblyName` --- .../LoggerSettingsConfiguration.cs | 1 - .../KeyValuePairs/SettingValueConversions.cs | 46 +++++++++ .../Settings/KeyValuePairSettingsTests.cs | 38 ++++++++ .../Settings/SettingValueConversionsTests.cs | 95 +++++++++++++++++++ test/Serilog.Tests/Support/ClassHierarchy.cs | 1 - .../Support/CustomConsoleTheme.cs | 8 ++ .../Support/StaticAccessorClasses.cs | 35 +++++++ test/TestDummies/Console/DummyConsoleSink.cs | 24 +++++ .../Console/Themes/ConcreteConsoleTheme.cs | 6 ++ .../Console/Themes/ConsoleTheme.cs | 7 ++ .../Console/Themes/ConsoleThemes.cs | 7 ++ .../Console/Themes/EmptyConsoleTheme.cs | 6 ++ .../DummyLoggerConfigurationExtensions.cs | 10 ++ 13 files changed, 282 insertions(+), 2 deletions(-) create mode 100644 test/Serilog.Tests/Support/CustomConsoleTheme.cs create mode 100644 test/Serilog.Tests/Support/StaticAccessorClasses.cs create mode 100644 test/TestDummies/Console/DummyConsoleSink.cs create mode 100644 test/TestDummies/Console/Themes/ConcreteConsoleTheme.cs create mode 100644 test/TestDummies/Console/Themes/ConsoleTheme.cs create mode 100644 test/TestDummies/Console/Themes/ConsoleThemes.cs create mode 100644 test/TestDummies/Console/Themes/EmptyConsoleTheme.cs diff --git a/src/Serilog/Configuration/LoggerSettingsConfiguration.cs b/src/Serilog/Configuration/LoggerSettingsConfiguration.cs index 6acbf70ac..fc63f4cfe 100644 --- a/src/Serilog/Configuration/LoggerSettingsConfiguration.cs +++ b/src/Serilog/Configuration/LoggerSettingsConfiguration.cs @@ -14,7 +14,6 @@ using System; using System.Collections.Generic; -using System.Linq; using Serilog.Settings.KeyValuePairs; namespace Serilog.Configuration diff --git a/src/Serilog/Settings/KeyValuePairs/SettingValueConversions.cs b/src/Serilog/Settings/KeyValuePairs/SettingValueConversions.cs index 662fe60d4..f1069af67 100644 --- a/src/Serilog/Settings/KeyValuePairs/SettingValueConversions.cs +++ b/src/Serilog/Settings/KeyValuePairs/SettingValueConversions.cs @@ -16,11 +16,17 @@ using System.Collections.Generic; using System.Linq; using System.Reflection; +using System.Text.RegularExpressions; namespace Serilog.Settings.KeyValuePairs { class SettingValueConversions { + // should match "The.NameSpace.TypeName::MemberName" optionnally followed by + // usual assembly qualifiers like : + // ", MyAssembly, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089" + static Regex StaticMemberAccessorRegex = new Regex("^(?[^:]+)::(?[A-Za-z][A-Za-z0-9]*)(?[^:]*)$"); + static Dictionary> ExtendedTypeConversions = new Dictionary> { { typeof(Uri), s => new Uri(s) }, @@ -29,6 +35,22 @@ class SettingValueConversions public static object ConvertToType(string value, Type toType) { + if (TryParseStaticMemberAccessor(value, out var accessorTypeName, out var memberName)) + { + var accessorType = Type.GetType(accessorTypeName, throwOnError: true); + var publicStaticPropertyInfo = accessorType.GetTypeInfo().DeclaredProperties + .Where(x => x.Name == memberName) + .Where(x => x.GetMethod != null) + .Where(x => x.GetMethod.IsPublic) + .FirstOrDefault(x => x.GetMethod.IsStatic); + + if (publicStaticPropertyInfo == null) + { + throw new InvalidOperationException($"Could not find public static property `{memberName}` on type `{accessorTypeName}`"); + } + return publicStaticPropertyInfo.GetValue(null); // static property, no instance to pass + } + var toTypeInfo = toType.GetTypeInfo(); if (toTypeInfo.IsGenericType && toType.GetGenericTypeDefinition() == typeof(Nullable<>)) { @@ -72,5 +94,29 @@ public static object ConvertToType(string value, Type toType) return Convert.ChangeType(value, toType); } + + internal static bool TryParseStaticMemberAccessor(string input, out string accessorTypeName, 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/test/Serilog.Tests/Settings/KeyValuePairSettingsTests.cs b/test/Serilog.Tests/Settings/KeyValuePairSettingsTests.cs index 275f82b82..fcceea943 100644 --- a/test/Serilog.Tests/Settings/KeyValuePairSettingsTests.cs +++ b/test/Serilog.Tests/Settings/KeyValuePairSettingsTests.cs @@ -10,6 +10,8 @@ using Serilog.Configuration; using Serilog.Core; using Serilog.Formatting; +using TestDummies.Console; +using TestDummies.Console.Themes; namespace Serilog.Tests.Settings { @@ -343,5 +345,41 @@ public void LoggingLevelSwitchCanBeUsedForMinimumLevelOverrides() // ReSharper restore HeuristicUnreachableCode } + [Fact] + public void SinksWithAbstractParamsAreConfiguredWithTypeName() + { + var settings = new Dictionary + { + ["using:TestDummies"] = typeof(DummyLoggerConfigurationExtensions).GetTypeInfo().Assembly.FullName, + ["write-to:DummyConsole.theme"] = "Serilog.Tests.Support.CustomConsoleTheme, Serilog.Tests" + }; + + DummyConsoleSink.Theme = null; + + new LoggerConfiguration() + .ReadFrom.KeyValuePairs(settings) + .CreateLogger(); + + Assert.NotNull(DummyConsoleSink.Theme); + Assert.IsType(DummyConsoleSink.Theme); + } + + [Fact] + public void SinksAreConfiguredWithStaticMember() + { + var settings = new Dictionary + { + ["using:TestDummies"] = typeof(DummyLoggerConfigurationExtensions).GetTypeInfo().Assembly.FullName, + ["write-to:DummyConsole.theme"] = "TestDummies.Console.Themes.ConsoleThemes::Theme1, TestDummies" + }; + + DummyConsoleSink.Theme = null; + + new LoggerConfiguration() + .ReadFrom.KeyValuePairs(settings) + .CreateLogger(); + + Assert.Equal(ConsoleThemes.Theme1, DummyConsoleSink.Theme); + } } } diff --git a/test/Serilog.Tests/Settings/SettingValueConversionsTests.cs b/test/Serilog.Tests/Settings/SettingValueConversionsTests.cs index 586ba748a..c03273ff6 100644 --- a/test/Serilog.Tests/Settings/SettingValueConversionsTests.cs +++ b/test/Serilog.Tests/Settings/SettingValueConversionsTests.cs @@ -82,5 +82,100 @@ public void TimeSpanValuesCanBeParsed(string input, int expDays, int expHours, i Assert.IsType(actual); Assert.Equal(expectedTimeSpan, actual); } + + [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)] + public void TryParseStaticMemberAccessorReturnsExpectedResults(string input, string expectedAccessorType, string expectedPropertyName) + { + var actual = SettingValueConversions.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); + } + } + + [Theory] + [InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::StringProperty, Serilog.Tests", typeof(string), "StringProperty")] + [InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::IntProperty, Serilog.Tests", typeof(int), 42)] + public void StaticMembersAccessorsCanBeUsedForSImpleTypes(string input, Type targetType, object expectedValue) + { + var actual = SettingValueConversions.ConvertToType(input, targetType); + + Assert.IsAssignableFrom(targetType, actual); + Assert.Equal(expectedValue, actual); + } + + [Theory] + [InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::InterfaceProperty, Serilog.Tests", typeof(IAmAnInterface))] + [InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::AbstractProperty, Serilog.Tests", typeof(AnAbstractClass))] + public void StaticMembersAccessorsCanBeUsedForReferenceTypes(string input, Type targetType) + { + var actual = SettingValueConversions.ConvertToType(input, targetType); + + Assert.IsAssignableFrom(targetType, actual); + Assert.Equal(ConcreteImpl.Instance, actual); + } + + [Theory] + // unknown type + [InlineData("Namespace.ThisIsNotAKnownType::StringProperty, Serilog.Tests", typeof(string))] + // good type name, but wrong namespace + [InlineData("Random.Namespace.ClassWithStaticAccessors::StringProperty, Serilog.Tests", typeof(string))] + // good full type name, but missing or wrong assembly + [InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::StringProperty", typeof(string))] + [InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::StringProperty, TestDummies", typeof(string))] + public void StaticAccessorOnUnknownTypeThrowsTypeLoadException(string input, Type targetType) + { + Assert.Throws(() => + SettingValueConversions.ConvertToType(input, targetType) + ); + } + + [Theory] + // unknown member + [InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::UnknownMember, Serilog.Tests", typeof(string))] + // static property exists but it's private + [InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::PrivateStringProperty, Serilog.Tests", typeof(string))] + // public member exists but it's a field + [InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::StringField, Serilog.Tests", typeof(string))] + // public property exists but it's not static + [InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::InstanceStringProperty, Serilog.Tests", typeof(string))] + public void StaticAccessorWithInvalidMemberThrowsInvalidOperationException(string input, Type targetType) + { + var exception = Assert.Throws(() => + SettingValueConversions.ConvertToType(input, targetType) + ); + + Assert.Contains("Could not find public static property ", exception.Message); + Assert.Contains("on type `Serilog.Tests.Support.ClassWithStaticAccessors, Serilog.Tests`", exception.Message); + } } } diff --git a/test/Serilog.Tests/Support/ClassHierarchy.cs b/test/Serilog.Tests/Support/ClassHierarchy.cs index 554fcf94a..597317bb3 100644 --- a/test/Serilog.Tests/Support/ClassHierarchy.cs +++ b/test/Serilog.Tests/Support/ClassHierarchy.cs @@ -1,6 +1,5 @@ namespace Serilog.Tests.Support { - public abstract class DummyAbstractClass { } diff --git a/test/Serilog.Tests/Support/CustomConsoleTheme.cs b/test/Serilog.Tests/Support/CustomConsoleTheme.cs new file mode 100644 index 000000000..345425b9d --- /dev/null +++ b/test/Serilog.Tests/Support/CustomConsoleTheme.cs @@ -0,0 +1,8 @@ +using TestDummies.Console.Themes; + +namespace Serilog.Tests.Support +{ + class CustomConsoleTheme : ConsoleTheme + { + } +} diff --git a/test/Serilog.Tests/Support/StaticAccessorClasses.cs b/test/Serilog.Tests/Support/StaticAccessorClasses.cs new file mode 100644 index 000000000..e5f788c40 --- /dev/null +++ b/test/Serilog.Tests/Support/StaticAccessorClasses.cs @@ -0,0 +1,35 @@ + +namespace Serilog.Tests.Support +{ + public interface IAmAnInterface + { + + } + + public abstract class AnAbstractClass + { + + } + + class ConcreteImpl : AnAbstractClass, IAmAnInterface + { + ConcreteImpl() + { + + } + + public static ConcreteImpl Instance { get; } = new ConcreteImpl(); + } + + public class ClassWithStaticAccessors + { + public static string StringProperty => nameof(StringProperty); + public static int IntProperty => 42; + public static IAmAnInterface InterfaceProperty => ConcreteImpl.Instance; + public static AnAbstractClass AbstractProperty => ConcreteImpl.Instance; + // ReSharper disable once UnusedMember.Local + static string PrivateStringProperty => nameof(PrivateStringProperty); + public string InstanceStringProperty => nameof(InstanceStringProperty); + public static string StringField = nameof(StringField); + } +} diff --git a/test/TestDummies/Console/DummyConsoleSink.cs b/test/TestDummies/Console/DummyConsoleSink.cs new file mode 100644 index 000000000..571e73fc8 --- /dev/null +++ b/test/TestDummies/Console/DummyConsoleSink.cs @@ -0,0 +1,24 @@ +using System; +using Serilog.Core; +using Serilog.Events; +using TestDummies.Console.Themes; + +namespace TestDummies.Console +{ + public class DummyConsoleSink : ILogEventSink + { + public DummyConsoleSink(ConsoleTheme theme = null) + { + Theme = theme ?? ConsoleTheme.None; + } + + [ThreadStatic] + public static ConsoleTheme Theme; + + public void Emit(LogEvent logEvent) + { + } + } + +} + diff --git a/test/TestDummies/Console/Themes/ConcreteConsoleTheme.cs b/test/TestDummies/Console/Themes/ConcreteConsoleTheme.cs new file mode 100644 index 000000000..8b3b041b2 --- /dev/null +++ b/test/TestDummies/Console/Themes/ConcreteConsoleTheme.cs @@ -0,0 +1,6 @@ +namespace TestDummies.Console.Themes +{ + class ConcreteConsoleTheme : ConsoleTheme + { + } +} diff --git a/test/TestDummies/Console/Themes/ConsoleTheme.cs b/test/TestDummies/Console/Themes/ConsoleTheme.cs new file mode 100644 index 000000000..1c5aaf5da --- /dev/null +++ b/test/TestDummies/Console/Themes/ConsoleTheme.cs @@ -0,0 +1,7 @@ +namespace TestDummies.Console.Themes +{ + public abstract class ConsoleTheme + { + public static ConsoleTheme None { get; } = new EmptyConsoleTheme(); + } +} diff --git a/test/TestDummies/Console/Themes/ConsoleThemes.cs b/test/TestDummies/Console/Themes/ConsoleThemes.cs new file mode 100644 index 000000000..7bb414cb8 --- /dev/null +++ b/test/TestDummies/Console/Themes/ConsoleThemes.cs @@ -0,0 +1,7 @@ +namespace TestDummies.Console.Themes +{ + public static class ConsoleThemes + { + public static ConsoleTheme Theme1 { get; } = new ConcreteConsoleTheme(); + } +} diff --git a/test/TestDummies/Console/Themes/EmptyConsoleTheme.cs b/test/TestDummies/Console/Themes/EmptyConsoleTheme.cs new file mode 100644 index 000000000..100e89e87 --- /dev/null +++ b/test/TestDummies/Console/Themes/EmptyConsoleTheme.cs @@ -0,0 +1,6 @@ +namespace TestDummies.Console.Themes +{ + class EmptyConsoleTheme : ConsoleTheme + { + } +} diff --git a/test/TestDummies/DummyLoggerConfigurationExtensions.cs b/test/TestDummies/DummyLoggerConfigurationExtensions.cs index 2fb795c0c..7c728cc94 100644 --- a/test/TestDummies/DummyLoggerConfigurationExtensions.cs +++ b/test/TestDummies/DummyLoggerConfigurationExtensions.cs @@ -4,6 +4,8 @@ using Serilog.Formatting; using Serilog.Configuration; using Serilog.Core; +using TestDummies.Console; +using TestDummies.Console.Themes; namespace TestDummies { @@ -51,6 +53,14 @@ public static LoggerConfiguration DummyWithLevelSwitch( return loggerSinkConfiguration.Sink(new DummyWithLevelSwitchSink(controlLevelSwitch), restrictedToMinimumLevel); } + public static LoggerConfiguration DummyConsole( + this LoggerSinkConfiguration loggerSinkConfiguration, + LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum, + ConsoleTheme theme = null) + { + return loggerSinkConfiguration.Sink(new DummyConsoleSink(theme), restrictedToMinimumLevel); + } + public static LoggerConfiguration Dummy( this LoggerSinkConfiguration loggerSinkConfiguration, Action wrappedSinkAction)