Skip to content

Commit ca759ac

Browse files
authored
Merge pull request #77 from MrMon3y/appsettings-static-member
Support for static member as the value of setting using `::` #73
2 parents 6738464 + c9bd655 commit ca759ac

File tree

6 files changed

+207
-11
lines changed

6 files changed

+207
-11
lines changed

sample/Sample/Program.cs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,9 @@ public static void Main(string[] args)
2525
do
2626
{
2727
logger.ForContext<Program>().Information("Hello, world!");
28+
logger.ForContext<Program>().Error("Hello, world!");
2829
logger.ForContext(Constants.SourceContextPropertyName, "Microsoft").Warning("Hello, world!");
30+
logger.ForContext(Constants.SourceContextPropertyName, "Microsoft").Error("Hello, world!");
2931
logger.ForContext(Constants.SourceContextPropertyName, "MyApp.Something.Tricky").Verbose("Hello, world!");
3032

3133
Console.WriteLine();

sample/Sample/Sample.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
<ItemGroup>
1717
<PackageReference Include="Microsoft.Extensions.Configuration.Json" Version="1.1.1" />
1818
<PackageReference Include="Serilog.Sinks.Async" Version="1.0.1" />
19-
<PackageReference Include="Serilog.Sinks.Literate" Version="2.0.0" />
19+
<PackageReference Include="Serilog.Sinks.Console" Version="3.1.1" />
2020
<PackageReference Include="Serilog.Sinks.RollingFile" Version="3.0.0" />
2121
<PackageReference Include="Serilog.Enrichers.Environment" Version="2.0.0" />
2222
<PackageReference Include="Serilog.Enrichers.Thread" Version="2.0.0" />

sample/Sample/appsettings.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"Serilog": {
3-
"Using": ["Serilog.Sinks.Literate"],
3+
"Using": ["Serilog.Sinks.Console"],
44
"MinimumLevel": {
55
"Default": "Debug",
66
"Override": {
@@ -14,9 +14,10 @@
1414
"configureLogger": {
1515
"WriteTo": [
1616
{
17-
"Name": "LiterateConsole",
17+
"Name": "Console",
1818
"Args": {
19-
"outputTemplate": "[{Timestamp:HH:mm:ss} {SourceContext} [{Level}] {Message}{NewLine}{Exception}"
19+
"outputTemplate": "[{Timestamp:HH:mm:ss} {SourceContext} [{Level}] {Message}{NewLine}{Exception}",
20+
"theme": "Serilog.Sinks.SystemConsole.Themes.AnsiConsoleTheme::Code, Serilog.Sinks.Console"
2021
}
2122
}
2223
]

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: 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+
}

0 commit comments

Comments
 (0)