Skip to content
This repository has been archived by the owner on Nov 3, 2021. It is now read-only.

Commit

Permalink
Support for .AddExpression()
Browse files Browse the repository at this point in the history
Currently supported features : 
- MinimumLevel.Is() / MinimumLevel.{Level}() / MinimumLevel.Override()
- Enrich.WithProperty()
- Enrich.{ExtensionMethod}()
- WriteTo.{ExtensionMethod}()
- AuditTo.{ExtensionMethod}()
- Filter.{ExtensionMethod}()

Currently suported "parameters" : 
- constant expressions
- new Class() where Class inherits/implements an abstract class / interface
- public static property / field to acces an abstract class / interface

Tests : 
- expression serilization tests
- sanity checks (AddExpression(exp) behaves the same as applying the expression)
- combination tests (combines different settings sources)

Known advanced missing features as TODOs in ConfigurationExpressionSettingsSerializerTests.cs
  • Loading branch information
Thibaud DESODT authored and tsimbalar committed Mar 5, 2018
1 parent 03a2181 commit 86caa6e
Show file tree
Hide file tree
Showing 7 changed files with 1,198 additions and 4 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright 2013-2017 Serilog Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Linq.Expressions;
using Serilog.Settings.Combined;
using Serilog.Settings.ConfigExpression;

namespace Serilog
{
/// <summary>
/// Extensions to allow combination of settings originating from config file appSettings
/// </summary>
public static class ConfigExpressionSettingsBuilderExtensions
{
/// <summary>
/// Converts a configuration expression into a series of key-value pairs and add them to the pool of available settings
/// </summary>
/// <param name="builder">The combined settings builder</param>
/// <param name="loggerConfigExpression">A configuration expression</param>
/// <returns>An object allowing configuration to continue.</returns>
public static ICombinedSettingsBuilder AddExpression(this ICombinedSettingsBuilder builder, Expression<Func<LoggerConfiguration, LoggerConfiguration>> loggerConfigExpression)
{
if (builder == null) throw new ArgumentNullException(nameof(builder));
if (loggerConfigExpression == null) throw new ArgumentNullException(nameof(loggerConfigExpression));

var serializer = new ConfigurationExpressionSettingsSerializer();
var keyValuePairs = serializer.SerializeToKeyValuePairs(loggerConfigExpression);

return builder.AddKeyValuePairs(keyValuePairs);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,275 @@
// Copyright 2013-2017 Serilog Contributors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

using System;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
using System.Reflection;
using Serilog.Configuration;
using Serilog.Events;

namespace Serilog.Settings.ConfigExpression
{
class ConfigurationExpressionSettingsSerializer
{
public IEnumerable<KeyValuePair<string, string>> SerializeToKeyValuePairs(Expression<Func<LoggerConfiguration, LoggerConfiguration>> expression)
{
if (expression == null) throw new ArgumentNullException(nameof(expression));
var methodCallExpression = expression.Body as MethodCallExpression ?? throw new ArgumentException("Expression's body should be a Method call", $"{nameof(expression)}.{nameof(expression.Body)}");
return WalkFluentMethodCallsFromRightToLeft(methodCallExpression)
.Reverse()
.SelectMany(x => x);
}

static IEnumerable<List<KeyValuePair<string, string>>> WalkFluentMethodCallsFromRightToLeft(MethodCallExpression methodCallExp)
{
if (methodCallExp == null) throw new ArgumentNullException(nameof(methodCallExp));

Expression current = methodCallExp;

while (current is MethodCallExpression)
{
var methodCall = (MethodCallExpression)current;
var method = methodCall.Method;
var methodName = method.Name;
var (methodTarget, normalizedMethodArguments) = ExtractNormalizedTargetAndArguments(methodCall);

current = methodTarget.Expression;

switch (methodTarget.Member.Name)
{
case nameof(LoggerConfiguration.MinimumLevel):
// .MinimumLevel.Override(string namespace, LogEventLevel overridenLevel)
if (methodName == nameof(LoggerMinimumLevelConfiguration.Override))
{
var overrideNamespace = ((ConstantExpression)normalizedMethodArguments[0]).Value.ToString();
var overrideLevel = ConvertExpressionToSettingValue(normalizedMethodArguments[1], typeof(LogEventLevel));

yield return new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(SettingsDirectives.MinimumLevelOverride(overrideNamespace), overrideLevel)
};
continue;
}

// .MinimumLevel.Is(LogEventLevel level)
if (methodName == nameof(LoggerMinimumLevelConfiguration.Is))
{
var minimumLevelIs = ConvertExpressionToSettingValue(normalizedMethodArguments[0], typeof(LogEventLevel));
yield return new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(SettingsDirectives.MinimumLevel, minimumLevelIs)
};
continue;
}

// .MinimumLevel.Debug(), MinimumLevel.Information() etc ...
if (!Enum.TryParse(methodName, out LogEventLevel minimumLevel))
throw new NotImplementedException($"Not supported : MinimumLevel.{methodName}");

yield return new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(SettingsDirectives.MinimumLevel, minimumLevel.ToString())
};
continue;

case nameof(LoggerConfiguration.Enrich):
// .Enrich.WithProperty(string propertyName, object propertyValue, bool destructureObjects)
if (methodName == nameof(LoggerEnrichmentConfiguration.WithProperty))
{
var enrichPropertyName = ((ConstantExpression)normalizedMethodArguments[0]).Value.ToString();
var enrichWithArgument = normalizedMethodArguments[1];
var enrichmentValue = ConvertExpressionToSettingValue(enrichWithArgument, typeof(object));
yield return new List<KeyValuePair<string, string>>
{
new KeyValuePair<string, string>(SettingsDirectives.EnrichWithProperty(enrichPropertyName), enrichmentValue)
};
continue;
}

// method .Enrich.FromLogContext()
// or extension method .Enrich.WithBar(param1, param2)
yield return SerializeMethodInvocation(MethodInvocationType.Enrich, method, normalizedMethodArguments);
continue;

case nameof(LoggerConfiguration.WriteTo):
// method .WriteTo.Sink()
// or extension method .WriteTo.CustomSink(param1, param2)
yield return SerializeMethodInvocation(MethodInvocationType.WriteTo, method, normalizedMethodArguments);
continue;
case nameof(LoggerConfiguration.AuditTo):
// method .AuditTo.Sink()
// or extension method .AuditTo.CustomSink(param1, param2)
yield return SerializeMethodInvocation(MethodInvocationType.AuditTo, method, normalizedMethodArguments);
continue;
case nameof(LoggerConfiguration.Filter):
// extension method .Filter.ByCustomMethod(param1, param2)
yield return SerializeMethodInvocation(MethodInvocationType.Filter, method, normalizedMethodArguments);
continue;
default:
throw new NotSupportedException($"Not supported : LoggerConfiguration.{methodTarget.Member.Name}");
}
}
}

static List<KeyValuePair<string, string>> SerializeMethodInvocation(MethodInvocationType methodInvocationType, MethodInfo method, IReadOnlyList<Expression> normalizedMethodArguments)
{
var methodName = method.Name;
var normalizedMethodParameters = ExtractNormalizedParameters(method);
var resultingDirectives = new List<KeyValuePair<string, string>>();
// using
var usingDirectives = GetUsingDirectivesForMethodCall(method);
resultingDirectives.AddRange(usingDirectives);
var args = normalizedMethodArguments
.Zip(normalizedMethodParameters, (expression, param) => new
{
MethodArgument = expression,
Parameter = param
})
.Select(x => new
{
ParamName = x.Parameter.Name,
ParamValue = ConvertExpressionToSettingValue(x.MethodArgument, x.Parameter.ParameterType)
})
.Where(x => x.ParamValue != null);

var directives = args.Select(x => new KeyValuePair<string, string>(SettingsDirectives.MethodInvocationParameter(methodInvocationType, methodName, x.ParamName), x.ParamValue)).ToList();
if (directives.Count > 0)
{
resultingDirectives.AddRange(directives);
}
else
{
resultingDirectives.Add(new KeyValuePair<string, string>(SettingsDirectives.ParameterlessMethodInvocation(methodInvocationType, methodName), ""));
}
return resultingDirectives;
}

static IEnumerable<KeyValuePair<string, string>> GetUsingDirectivesForMethodCall(MethodInfo method)
{
var containingAssembly = method.DeclaringType.GetTypeInfo().Assembly;
if (containingAssembly == typeof(ILogger).GetTypeInfo().Assembly)
{
// no using is required for Serilog assembly
yield break;
}
var assemblyShortName = containingAssembly.GetName().Name;

yield return new KeyValuePair<string, string>(SettingsDirectives.Using(assemblyShortName), $"{assemblyShortName}");
}

/// <summary>
/// Extract target and parameters in a consistent way, whether method is a "regular" method call
/// or an extension method (actually a sttic method where the first parameter is the target)
/// </summary>
/// <returns></returns>
static (MemberExpression target, IReadOnlyList<Expression> normalizedArguments) ExtractNormalizedTargetAndArguments(MethodCallExpression methodCall)
{
var method = methodCall.Method;
MemberExpression leftSide;
List<Expression> methodArguments;
if (method.IsStatic)
{
// extension method : the first argument is the target
leftSide = (MemberExpression)methodCall.Arguments[0];
methodArguments = methodCall.Arguments.Skip(1).ToList();
}
else
{
// regular method
leftSide = (MemberExpression)methodCall.Object;
methodArguments = methodCall.Arguments.ToList();
}

return (target: leftSide, normalizedArguments: methodArguments.AsReadOnly());
}

static IReadOnlyList<ParameterInfo> ExtractNormalizedParameters(MethodInfo method)
{
if (method.IsStatic)
{
// extension method : the first parameter is actually the target !
return method.GetParameters().Skip(1).ToList().AsReadOnly();
}

return method.GetParameters().ToList().AsReadOnly();
}

static string ConvertExpressionToSettingValue(Expression exp, Type targetParameterType)
{
if (exp == null) throw new ArgumentNullException(nameof(exp));
if (targetParameterType == null) throw new ArgumentNullException(nameof(targetParameterType));

if (exp is ConstantExpression constantExp)
{
return constantExp.Value == null ? null : $"{constantExp.Value}";
}

var targetTypeInfo = targetParameterType.GetTypeInfo();
if (targetTypeInfo.IsAbstract || targetTypeInfo.IsInterface)
{
// when target type is abstract, we support :
// calling the default constructor of an implementation
if (exp is NewExpression newExp && !newExp.Arguments.Any())
{
return newExp.Type.AssemblyQualifiedName;
}

// accessing a public static property/field of that type
if (exp is MemberExpression memberExp)
{
var propertyOrFieldInfo = memberExp.Member;
var memberOwner = propertyOrFieldInfo.DeclaringType;
if (propertyOrFieldInfo is PropertyInfo propInfo)
{
if (!(propInfo.GetMethod.IsPublic && propInfo.GetMethod.IsStatic))
{
throw new NotSupportedException($"Property {memberOwner.FullName}.{propInfo.Name} is not public static. Only public static properties are supported");
}
}

if (propertyOrFieldInfo is FieldInfo fieldInfo)
{
if (!(fieldInfo.IsPublic && fieldInfo.IsStatic))
{
throw new NotSupportedException($"Field {memberOwner.FullName}.{fieldInfo.Name} is not public static. Only public static fields are supported");
}
}

return $"{memberOwner.FullName}::{propertyOrFieldInfo.Name}, {memberOwner.GetTypeInfo().Assembly.FullName}";
}

throw new NotSupportedException($"Not supported : {exp.GetType()} `{exp}`");
}
switch (exp)
{
// a boolean is a UnaryExpression Convert(true), for some reason
case UnaryExpression unaryExp:
return $"{unaryExp.Operand}";

case NewExpression newExp:
// constructor new Uri(string uri)
if (newExp.Type == typeof(Uri))
{
return ((ConstantExpression)newExp.Arguments[0]).Value.ToString();
}
throw new NotImplementedException($"Not supported : new {newExp.Type}(...)");

default:
throw new NotImplementedException($"Cannot extract a string value from `{exp}`");
}
}
}
}
Loading

0 comments on commit 86caa6e

Please sign in to comment.