Skip to content

Commit

Permalink
Merge pull request serilog#1064 from tsimbalar/settings-static-member
Browse files Browse the repository at this point in the history
Add support for reference to public static properties in settings
  • Loading branch information
nblumhardt authored Nov 23, 2017
2 parents f0d5d29 + e91e377 commit f284f85
Show file tree
Hide file tree
Showing 13 changed files with 304 additions and 5 deletions.
1 change: 0 additions & 1 deletion src/Serilog/Configuration/LoggerSettingsConfiguration.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@

using System;
using System.Collections.Generic;
using System.Linq;
using Serilog.Settings.KeyValuePairs;

namespace Serilog.Configuration
Expand Down
63 changes: 63 additions & 0 deletions src/Serilog/Settings/KeyValuePairs/SettingValueConversions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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("^(?<shortTypeName>[^:]+)::(?<memberName>[A-Za-z][A-Za-z0-9]*)(?<typeNameExtraQualifiers>[^:]*)$");

static Dictionary<Type, Func<string, object>> ExtendedTypeConversions = new Dictionary<Type, Func<string, object>>
{
{ typeof(Uri), s => new Uri(s) },
Expand Down Expand Up @@ -53,6 +59,39 @@ public static object ConvertToType(string value, Type toType)

if ((toTypeInfo.IsInterface || toTypeInfo.IsAbstract) && !string.IsNullOrWhiteSpace(value))
{
// check if value looks like a static property or field directive
// like "Namespace.TypeName::StaticProperty, AssemblyName"
if (TryParseStaticMemberAccessor(value, out var accessorTypeName, out var memberName))
{
var accessorType = Type.GetType(accessorTypeName, throwOnError: true);
// is there a public static property with that name ?
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)
{
return publicStaticPropertyInfo.GetValue(null); // static property, no instance to pass
}

// 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);

if (publicStaticFieldInfo != null)
{
return publicStaticFieldInfo.GetValue(null); // static field, no instance to pass
}

throw new InvalidOperationException($"Could not find a public static property or field with name `{memberName}` on type `{accessorTypeName}`");
}

// maybe it's the assembly-qualified type name of a concrete implementation
// with a default constructor
var type = Type.GetType(value.Trim(), throwOnError: false);
if (type != null)
{
Expand All @@ -72,5 +111,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;
}
}
}
40 changes: 39 additions & 1 deletion test/Serilog.Tests/Settings/KeyValuePairSettingsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@
using Serilog.Configuration;
using Serilog.Core;
using Serilog.Formatting;
using TestDummies.Console;
using TestDummies.Console.Themes;

namespace Serilog.Tests.Settings
{
Expand Down Expand Up @@ -323,7 +325,7 @@ public void LoggingLevelSwitchCanBeUsedForMinimumLevelOverrides()
var systemLogger = log.ForContext(Constants.SourceContextPropertyName, "System.Bar");

log.Write(Some.InformationEvent());
Assert.False(evt is null, "Minimul level is Debug. It should log Information messages");
Assert.False(evt is null, "Minimum level is Debug. It should log Information messages");

evt = null;
// ReSharper disable HeuristicUnreachableCode
Expand All @@ -343,5 +345,41 @@ public void LoggingLevelSwitchCanBeUsedForMinimumLevelOverrides()
// ReSharper restore HeuristicUnreachableCode
}

[Fact]
public void SinksWithAbstractParamsAreConfiguredWithTypeName()
{
var settings = new Dictionary<string, string>
{
["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<CustomConsoleTheme>(DummyConsoleSink.Theme);
}

[Fact]
public void SinksAreConfiguredWithStaticMember()
{
var settings = new Dictionary<string, string>
{
["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);
}
}
}
91 changes: 91 additions & 0 deletions test/Serilog.Tests/Settings/SettingValueConversionsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -82,5 +82,96 @@ public void TimeSpanValuesCanBeParsed(string input, int expDays, int expHours, i
Assert.IsType<TimeSpan>(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)]
// 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 = 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::InterfaceProperty, Serilog.Tests", typeof(IAmAnInterface))]
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::AbstractProperty, Serilog.Tests", typeof(AnAbstractClass))]
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::InterfaceField, Serilog.Tests", typeof(IAmAnInterface))]
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::AbstractField, 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::InterfaceProperty, Serilog.Tests", typeof(IAmAnInterface))]
// good type name, but wrong namespace
[InlineData("Random.Namespace.ClassWithStaticAccessors::InterfaceProperty, Serilog.Tests", typeof(IAmAnInterface))]
// good full type name, but missing or wrong assembly
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::InterfaceProperty", typeof(IAmAnInterface))]
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::InterfaceProperty, TestDummies", typeof(IAmAnInterface))]
public void StaticAccessorOnUnknownTypeThrowsTypeLoadException(string input, Type targetType)
{
Assert.Throws<TypeLoadException>(() =>
SettingValueConversions.ConvertToType(input, targetType)
);
}

[Theory]
// unknown member
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::UnknownMember, Serilog.Tests", typeof(IAmAnInterface))]
// static property exists but it's private
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::PrivateInterfaceProperty, Serilog.Tests", typeof(IAmAnInterface))]
// static field exists but it's private
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::PrivateInterfaceField, Serilog.Tests", typeof(IAmAnInterface))]
// public property exists but it's not static
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::InstanceInterfaceProperty, Serilog.Tests", typeof(IAmAnInterface))]
// public field exists but it's not static
[InlineData("Serilog.Tests.Support.ClassWithStaticAccessors::InstanceInterfaceField, Serilog.Tests", typeof(IAmAnInterface))]
public void StaticAccessorWithInvalidMemberThrowsInvalidOperationException(string input, Type targetType)
{
var exception = Assert.Throws<InvalidOperationException>(() =>
SettingValueConversions.ConvertToType(input, targetType)
);

Assert.Contains("Could not find a public static property or field ", exception.Message);
Assert.Contains("on type `Serilog.Tests.Support.ClassWithStaticAccessors, Serilog.Tests`", exception.Message);
}
}
}
5 changes: 2 additions & 3 deletions test/Serilog.Tests/Support/ClassHierarchy.cs
Original file line number Diff line number Diff line change
@@ -1,19 +1,18 @@
namespace Serilog.Tests.Support
{

public abstract class DummyAbstractClass
{
}

public class DummyConcreteClassWithDefaultConstructor
public class DummyConcreteClassWithDefaultConstructor : DummyAbstractClass
{
// ReSharper disable once UnusedParameter.Local
public DummyConcreteClassWithDefaultConstructor(string param = "")
{
}
}

public class DummyConcreteClassWithoutDefaultConstructor
public class DummyConcreteClassWithoutDefaultConstructor : DummyAbstractClass
{
// ReSharper disable once UnusedParameter.Local
public DummyConcreteClassWithoutDefaultConstructor(string param)
Expand Down
8 changes: 8 additions & 0 deletions test/Serilog.Tests/Support/CustomConsoleTheme.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
using TestDummies.Console.Themes;

namespace Serilog.Tests.Support
{
class CustomConsoleTheme : ConsoleTheme
{
}
}
41 changes: 41 additions & 0 deletions test/Serilog.Tests/Support/StaticAccessorClasses.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@

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 IAmAnInterface InterfaceProperty => ConcreteImpl.Instance;
public static AnAbstractClass AbstractProperty => ConcreteImpl.Instance;

public static IAmAnInterface InterfaceField = ConcreteImpl.Instance;
public static AnAbstractClass AbstractField = ConcreteImpl.Instance;

// ReSharper disable once UnusedMember.Local
static IAmAnInterface PrivateInterfaceProperty => ConcreteImpl.Instance;
#pragma warning disable 169
static IAmAnInterface PrivateInterfaceField = ConcreteImpl.Instance;
#pragma warning restore 169
public IAmAnInterface InstanceInterfaceProperty => ConcreteImpl.Instance;
public IAmAnInterface InstanceInterfaceField = ConcreteImpl.Instance;

}
}
24 changes: 24 additions & 0 deletions test/TestDummies/Console/DummyConsoleSink.cs
Original file line number Diff line number Diff line change
@@ -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)
{
}
}

}

6 changes: 6 additions & 0 deletions test/TestDummies/Console/Themes/ConcreteConsoleTheme.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace TestDummies.Console.Themes
{
class ConcreteConsoleTheme : ConsoleTheme
{
}
}
7 changes: 7 additions & 0 deletions test/TestDummies/Console/Themes/ConsoleTheme.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace TestDummies.Console.Themes
{
public abstract class ConsoleTheme
{
public static ConsoleTheme None { get; } = new EmptyConsoleTheme();
}
}
7 changes: 7 additions & 0 deletions test/TestDummies/Console/Themes/ConsoleThemes.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
namespace TestDummies.Console.Themes
{
public static class ConsoleThemes
{
public static ConsoleTheme Theme1 { get; } = new ConcreteConsoleTheme();
}
}
6 changes: 6 additions & 0 deletions test/TestDummies/Console/Themes/EmptyConsoleTheme.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
namespace TestDummies.Console.Themes
{
class EmptyConsoleTheme : ConsoleTheme
{
}
}
Loading

0 comments on commit f284f85

Please sign in to comment.