Skip to content

Commit 16477c1

Browse files
author
Douglas Money (IT - Data Mgmt)
committed
Support for static member as the value of setting using ::
1 parent 6738464 commit 16477c1

File tree

11 files changed

+335
-7
lines changed

11 files changed

+335
-7
lines changed

src/Serilog.Settings.Configuration/Settings/Configuration/StringArgumentValue.cs

Lines changed: 60 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
using System.Collections.Generic;
33
using System.Linq;
44
using System.Reflection;
5-
5+
using System.Text.RegularExpressions;
66
using Microsoft.Extensions.Primitives;
77

88
using Serilog.Core;
@@ -16,6 +16,8 @@ class StringArgumentValue : IConfigurationArgumentValue
1616
readonly Func<string> _valueProducer;
1717
readonly Func<IChangeToken> _changeTokenProducer;
1818

19+
private static readonly Regex StaticMemberAccessorRegex = new Regex("^(?<shortTypeName>[^:]+)::(?<memberName>[A-Za-z][A-Za-z0-9]*)(?<typeNameExtraQualifiers>[^:]*)$");
20+
1921
public StringArgumentValue(Func<string> valueProducer, Func<IChangeToken> changeTokenProducer = null)
2022
{
2123
_valueProducer = valueProducer ?? throw new ArgumentNullException(nameof(valueProducer));
@@ -56,6 +58,39 @@ public object ConvertTo(Type toType)
5658

5759
if ((toTypeInfo.IsInterface || toTypeInfo.IsAbstract) && !string.IsNullOrWhiteSpace(argumentValue))
5860
{
61+
//check if value looks like a static property or field directive
62+
// like "Namespace.TypeName::StaticProperty, AssemblyName"
63+
if (TryParseStaticMemberAccessor(argumentValue, out var accessorTypeName, out var memberName))
64+
{
65+
var accessorType = Type.GetType(accessorTypeName, throwOnError: true);
66+
// is there a public static property with that name ?
67+
var publicStaticPropertyInfo = accessorType.GetTypeInfo().DeclaredProperties
68+
.Where(x => x.Name == memberName)
69+
.Where(x => x.GetMethod != null)
70+
.Where(x => x.GetMethod.IsPublic)
71+
.FirstOrDefault(x => x.GetMethod.IsStatic);
72+
73+
if (publicStaticPropertyInfo != null)
74+
{
75+
return publicStaticPropertyInfo.GetValue(null); // static property, no instance to pass
76+
}
77+
78+
// no property ? look for a public static field
79+
var publicStaticFieldInfo = accessorType.GetTypeInfo().DeclaredFields
80+
.Where(x => x.Name == memberName)
81+
.Where(x => x.IsPublic)
82+
.FirstOrDefault(x => x.IsStatic);
83+
84+
if (publicStaticFieldInfo != null)
85+
{
86+
return publicStaticFieldInfo.GetValue(null); // static field, no instance to pass
87+
}
88+
89+
throw new InvalidOperationException($"Could not find a public static property or field with name `{memberName}` on type `{accessorTypeName}`");
90+
}
91+
92+
// maybe it's the assembly-qualified type name of a concrete implementation
93+
// with a default constructor
5994
var type = Type.GetType(argumentValue.Trim());
6095
if (type != null)
6196
{
@@ -100,5 +135,29 @@ public object ConvertTo(Type toType)
100135

101136
return Convert.ChangeType(argumentValue, toType);
102137
}
138+
139+
internal static bool TryParseStaticMemberAccessor(string input, out string accessorTypeName, out string memberName)
140+
{
141+
if (input == null)
142+
{
143+
accessorTypeName = null;
144+
memberName = null;
145+
return false;
146+
}
147+
if (StaticMemberAccessorRegex.IsMatch(input))
148+
{
149+
var match = StaticMemberAccessorRegex.Match(input);
150+
var shortAccessorTypeName = match.Groups["shortTypeName"].Value;
151+
var rawMemberName = match.Groups["memberName"].Value;
152+
var extraQualifiers = match.Groups["typeNameExtraQualifiers"].Value;
153+
154+
memberName = rawMemberName.Trim();
155+
accessorTypeName = shortAccessorTypeName.Trim() + extraQualifiers.TrimEnd();
156+
return true;
157+
}
158+
accessorTypeName = null;
159+
memberName = null;
160+
return false;
161+
}
103162
}
104163
}

test/Serilog.Settings.Configuration.Tests/StringArgumentValueTests.cs

Lines changed: 103 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
1-
using Serilog.Formatting;
1+
using System;
2+
using Serilog.Formatting;
23
using Serilog.Formatting.Json;
4+
35
using Serilog.Settings.Configuration.Tests.Support;
6+
47
using Xunit;
58

69
namespace Serilog.Settings.Configuration.Tests
@@ -20,11 +23,105 @@ public void StringValuesConvertToDefaultInstancesIfTargetIsInterface()
2023
[Fact]
2124
public void StringValuesConvertToDefaultInstancesIfTargetIsAbstractClass()
2225
{
23-
var stringArgumentValue = new StringArgumentValue(() => "Serilog.Settings.Configuration.Tests.Support.ConcreteClass, Serilog.Settings.Configuration.Tests");
26+
var stringArgumentValue = new StringArgumentValue(() => "Serilog.Settings.Configuration.Tests.Support.ConcreteClass, Serilog.Settings.Configuration.Tests");
27+
28+
var result = stringArgumentValue.ConvertTo(typeof(AbstractClass));
29+
30+
Assert.IsType<ConcreteClass>(result);
31+
}
32+
33+
[Theory]
34+
[InlineData("My.NameSpace.Class+InnerClass::Member",
35+
"My.NameSpace.Class+InnerClass", "Member")]
36+
[InlineData(" TrimMe.NameSpace.Class::NeedsTrimming ",
37+
"TrimMe.NameSpace.Class", "NeedsTrimming")]
38+
[InlineData("My.NameSpace.Class::Member",
39+
"My.NameSpace.Class", "Member")]
40+
[InlineData("My.NameSpace.Class::Member, MyAssembly",
41+
"My.NameSpace.Class, MyAssembly", "Member")]
42+
[InlineData("My.NameSpace.Class::Member, MyAssembly, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
43+
"My.NameSpace.Class, MyAssembly, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089", "Member")]
44+
[InlineData("Just a random string with :: in it",
45+
null, null)]
46+
[InlineData("Its::a::trapWithColonsAppearingTwice",
47+
null, null)]
48+
[InlineData("ThereIsNoMemberHere::",
49+
null, null)]
50+
[InlineData(null,
51+
null, null)]
52+
[InlineData(" ",
53+
null, null)]
54+
// a full-qualified type name should not be considered a static member accessor
55+
[InlineData("My.NameSpace.Class, MyAssembly, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089",
56+
null, null)]
57+
public void TryParseStaticMemberAccessorReturnsExpectedResults(string input, string expectedAccessorType, string expectedPropertyName)
58+
{
59+
var actual = StringArgumentValue.TryParseStaticMemberAccessor(input,
60+
out var actualAccessorType,
61+
out var actualMemberName);
62+
63+
if (expectedAccessorType == null)
64+
{
65+
Assert.False(actual, $"Should not parse {input}");
66+
}
67+
else
68+
{
69+
Assert.True(actual, $"should successfully parse {input}");
70+
Assert.Equal(expectedAccessorType, actualAccessorType);
71+
Assert.Equal(expectedPropertyName, actualMemberName);
72+
}
73+
}
74+
75+
[Theory]
76+
[InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InterfaceProperty, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))]
77+
[InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::AbstractProperty, Serilog.Settings.Configuration.Tests", typeof(AnAbstractClass))]
78+
[InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InterfaceField, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))]
79+
[InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::AbstractField, Serilog.Settings.Configuration.Tests", typeof(AnAbstractClass))]
80+
private void StaticMembersAccessorsCanBeUsedForReferenceTypes(string input, Type targetType)
81+
{
82+
var stringArgumentValue = new StringArgumentValue(() => $"{input}");
83+
84+
var actual = stringArgumentValue.ConvertTo(targetType);
85+
86+
Assert.IsAssignableFrom(targetType, actual);
87+
Assert.Equal(ConcreteImpl.Instance, actual);
88+
}
2489

25-
var result = stringArgumentValue.ConvertTo(typeof(AbstractClass));
90+
[Theory]
91+
// unknown type
92+
[InlineData("Namespace.ThisIsNotAKnownType::InterfaceProperty, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))]
93+
// good type name, but wrong namespace
94+
[InlineData("Random.Namespace.ClassWithStaticAccessors::InterfaceProperty, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))]
95+
// good full type name, but missing or wrong assembly
96+
[InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InterfaceProperty", typeof(IAmAnInterface))]
97+
public void StaticAccessorOnUnknownTypeThrowsTypeLoadException(string input, Type targetType)
98+
{
99+
var stringArgumentValue = new StringArgumentValue(() => $"{input}");
100+
Assert.Throws<TypeLoadException>(() =>
101+
stringArgumentValue.ConvertTo(targetType)
102+
);
103+
}
104+
105+
[Theory]
106+
// unknown member
107+
[InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::UnknownMember, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))]
108+
// static property exists but it's private
109+
[InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::PrivateInterfaceProperty, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))]
110+
// static field exists but it's private
111+
[InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::PrivateInterfaceField, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))]
112+
// public property exists but it's not static
113+
[InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InstanceInterfaceProperty, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))]
114+
// public field exists but it's not static
115+
[InlineData("Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors::InstanceInterfaceField, Serilog.Settings.Configuration.Tests", typeof(IAmAnInterface))]
116+
public void StaticAccessorWithInvalidMemberThrowsInvalidOperationException(string input, Type targetType)
117+
{
118+
var stringArgumentValue = new StringArgumentValue(() => $"{input}");
119+
var exception = Assert.Throws<InvalidOperationException>(() =>
120+
stringArgumentValue.ConvertTo(targetType)
121+
);
26122

27-
Assert.IsType<ConcreteClass>(result);
123+
Assert.Contains("Could not find a public static property or field ", exception.Message);
124+
Assert.Contains("on type `Serilog.Settings.Configuration.Tests.Support.ClassWithStaticAccessors, Serilog.Settings.Configuration.Tests`", exception.Message);
28125
}
29-
}
30-
}
126+
}
127+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
namespace Serilog.Settings.Configuration.Tests.Support
2+
{
3+
public abstract class DummyAbstractClass
4+
{
5+
}
6+
7+
public class DummyConcreteClassWithDefaultConstructor : DummyAbstractClass
8+
{
9+
// ReSharper disable once UnusedParameter.Local
10+
public DummyConcreteClassWithDefaultConstructor(string param = "")
11+
{
12+
}
13+
}
14+
15+
public class DummyConcreteClassWithoutDefaultConstructor : DummyAbstractClass
16+
{
17+
// ReSharper disable once UnusedParameter.Local
18+
public DummyConcreteClassWithoutDefaultConstructor(string param)
19+
{
20+
}
21+
}
22+
}
Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
using Serilog.Settings.Configuration.Tests.TestDummies.Console.Themes;
2+
3+
namespace Serilog.Settings.Configuration.Tests.Support
4+
{
5+
class CustomConsoleTheme : ConsoleTheme
6+
{
7+
}
8+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
namespace Serilog.Settings.Configuration.Tests.Support
2+
{
3+
public interface IAmAnInterface
4+
{
5+
}
6+
7+
public abstract class AnAbstractClass
8+
{
9+
}
10+
11+
internal class ConcreteImpl : AnAbstractClass, IAmAnInterface
12+
{
13+
private ConcreteImpl()
14+
{
15+
}
16+
17+
public static ConcreteImpl Instance { get; } = new ConcreteImpl();
18+
}
19+
20+
public class ClassWithStaticAccessors
21+
{
22+
public static IAmAnInterface InterfaceProperty => ConcreteImpl.Instance;
23+
public static AnAbstractClass AbstractProperty => ConcreteImpl.Instance;
24+
25+
public static IAmAnInterface InterfaceField = ConcreteImpl.Instance;
26+
public static AnAbstractClass AbstractField = ConcreteImpl.Instance;
27+
28+
// ReSharper disable once UnusedMember.Local
29+
private static IAmAnInterface PrivateInterfaceProperty => ConcreteImpl.Instance;
30+
31+
#pragma warning disable 169
32+
private static IAmAnInterface PrivateInterfaceField = ConcreteImpl.Instance;
33+
#pragma warning restore 169
34+
public IAmAnInterface InstanceInterfaceProperty => ConcreteImpl.Instance;
35+
public IAmAnInterface InstanceInterfaceField = ConcreteImpl.Instance;
36+
}
37+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
using Serilog.Core;
2+
using Serilog.Events;
3+
using Serilog.Settings.Configuration.Tests.TestDummies.Console.Themes;
4+
using System;
5+
6+
namespace Serilog.Settings.Configuration.Tests.TestDummies.Console
7+
{
8+
public class DummyConsoleSink : ILogEventSink
9+
{
10+
public DummyConsoleSink(ConsoleTheme theme = null)
11+
{
12+
Theme = theme ?? ConsoleTheme.None;
13+
}
14+
15+
[ThreadStatic]
16+
public static ConsoleTheme Theme;
17+
18+
public void Emit(LogEvent logEvent)
19+
{
20+
}
21+
}
22+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace Serilog.Settings.Configuration.Tests.TestDummies.Console.Themes
8+
{
9+
class ConcreteConsoleTheme : ConsoleTheme
10+
{
11+
}
12+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace Serilog.Settings.Configuration.Tests.TestDummies.Console.Themes
8+
{
9+
public abstract class ConsoleTheme
10+
{
11+
public static ConsoleTheme None { get; } = new EmptyConsoleTheme();
12+
}
13+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
namespace Serilog.Settings.Configuration.Tests.TestDummies.Console.Themes
2+
{
3+
public static class ConsoleThemes
4+
{
5+
public static ConsoleTheme Theme1 { get; } = new ConcreteConsoleTheme();
6+
}
7+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Text;
5+
using System.Threading.Tasks;
6+
7+
namespace Serilog.Settings.Configuration.Tests.TestDummies.Console.Themes
8+
{
9+
class EmptyConsoleTheme : ConsoleTheme
10+
{
11+
}
12+
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
using System;
2+
using Serilog.Configuration;
3+
using Serilog.Events;
4+
using Serilog.Formatting;
5+
using Serilog.Settings.Configuration.Tests.TestDummies.Console;
6+
using Serilog.Settings.Configuration.Tests.TestDummies.Console.Themes;
7+
8+
namespace Serilog.Settings.Configuration.Tests.TestDummies
9+
{
10+
static class DummyLoggerConfigurationExtensions
11+
{
12+
public static LoggerConfiguration DummyRollingFile(
13+
LoggerSinkConfiguration loggerSinkConfiguration,
14+
string pathFormat,
15+
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
16+
string outputTemplate = null,
17+
IFormatProvider formatProvider = null)
18+
{
19+
return null;
20+
}
21+
22+
public static LoggerConfiguration DummyRollingFile(
23+
LoggerSinkConfiguration loggerSinkConfiguration,
24+
ITextFormatter formatter,
25+
string pathFormat,
26+
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum)
27+
{
28+
return null;
29+
}
30+
31+
public static LoggerConfiguration DummyConsole(
32+
this LoggerSinkConfiguration loggerSinkConfiguration,
33+
LogEventLevel restrictedToMinimumLevel = LevelAlias.Minimum,
34+
ConsoleTheme theme = null)
35+
{
36+
return loggerSinkConfiguration.Sink(new DummyConsoleSink(theme), restrictedToMinimumLevel);
37+
}
38+
}
39+
}

0 commit comments

Comments
 (0)