Skip to content

Improve parameter binding and allow user-defined parameter types #43

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 4, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,6 +195,7 @@ calling a function will be undefined if:
| `LastIndexOf(s, t)` | Returns the last index of substring `t` in string `s`, or -1 if the substring does not appear. |
| `Length(x)` | Returns the length of a string or array. |
| `Now()` | Returns `DateTimeOffset.Now`. |
| `Rest()` | In an `ExpressionTemplate`, returns an object containing the first-class event properties not otherwise referenced in the template or the event's message. |
| `Round(n, m)` | Round the number `n` to `m` decimal places. |
| `StartsWith(s, t)` | Tests whether the string `s` starts with substring `t`. |
| `Substring(s, start, [length])` | Return the substring of string `s` from `start` to the end of the string, or of `length` characters, if this argument is supplied. |
Expand Down
3 changes: 2 additions & 1 deletion example/Sample/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ public static void Main()
static void TextFormattingExample1()
{
using var log = new LoggerConfiguration()
.Enrich.WithProperty("Application", "Sample")
.WriteTo.Console(new ExpressionTemplate(
"[{@t:HH:mm:ss} {@l:u3}" +
"{#if SourceContext is not null} ({Substring(SourceContext, LastIndexOf(SourceContext, '.') + 1)}){#end}] " +
"{@m} (first item is {coalesce(Items[0], '<empty>')})\n{@x}",
"{@m} (first item is {coalesce(Items[0], '<empty>')}) {rest()}\n{@x}",
theme: TemplateTheme.Code))
.CreateLogger();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,9 @@ class LinqExpressionCompiler : SerilogExpressionTransformer<ExpressionBody>
static readonly MethodInfo TryGetStructurePropertyValueMethod = typeof(Intrinsics)
.GetMethod(nameof(Intrinsics.TryGetStructurePropertyValue), BindingFlags.Static | BindingFlags.Public)!;

static readonly PropertyInfo EvaluationContextLogEventProperty = typeof(EvaluationContext)
.GetProperty(nameof(EvaluationContext.LogEvent), BindingFlags.Instance | BindingFlags.Public)!;

ParameterExpression Context { get; } = LX.Variable(typeof(EvaluationContext), "ctx");

LinqExpressionCompiler(IFormatProvider? formatProvider, NameResolver nameResolver)
Expand All @@ -93,36 +96,44 @@ ExpressionBody Splice(Expression<Evaluatable> lambda)
return ParameterReplacementVisitor.ReplaceParameters(lambda, Context);
}

protected override ExpressionBody Transform(CallExpression lx)
protected override ExpressionBody Transform(CallExpression call)
{
if (!_nameResolver.TryResolveFunctionName(lx.OperatorName, out var m))
throw new ArgumentException($"The function name `{lx.OperatorName}` was not recognized.");
if (!_nameResolver.TryResolveFunctionName(call.OperatorName, out var m))
throw new ArgumentException($"The function name `{call.OperatorName}` was not recognized.");

var methodParameters = m.GetParameters();

var parameterCount = methodParameters.Count(pi => pi.ParameterType == typeof(LogEventPropertyValue));
if (parameterCount != lx.Operands.Length)
throw new ArgumentException($"The function `{lx.OperatorName}` requires {parameterCount} arguments.");
if (parameterCount != call.Operands.Length)
throw new ArgumentException($"The function `{call.OperatorName}` requires {parameterCount} arguments.");

var operands = lx.Operands.Select(Transform).ToList();
var operands = new Queue<LX>(call.Operands.Select(Transform));

// `and` and `or` short-circuit to save execution time; unlike the earlier Serilog.Filters.Expressions, nothing else does.
if (Operators.SameOperator(lx.OperatorName, Operators.RuntimeOpAnd))
return CompileLogical(LX.AndAlso, operands[0], operands[1]);
if (Operators.SameOperator(call.OperatorName, Operators.RuntimeOpAnd))
return CompileLogical(LX.AndAlso, operands.Dequeue(), operands.Dequeue());

if (Operators.SameOperator(lx.OperatorName, Operators.RuntimeOpOr))
return CompileLogical(LX.OrElse, operands[0], operands[1]);
if (Operators.SameOperator(call.OperatorName, Operators.RuntimeOpOr))
return CompileLogical(LX.OrElse, operands.Dequeue(), operands.Dequeue());

for (var i = 0; i < methodParameters.Length; ++i)
var boundParameters = new List<LX>(methodParameters.Length);
foreach (var pi in methodParameters)
{
var pi = methodParameters[i];
if (pi.ParameterType == typeof(StringComparison))
operands.Insert(i, LX.Constant(lx.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal));
if (pi.ParameterType == typeof(LogEventPropertyValue))
boundParameters.Add(operands.Dequeue());
else if (pi.ParameterType == typeof(StringComparison))
boundParameters.Add(LX.Constant(call.IgnoreCase ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal));
else if (pi.ParameterType == typeof(IFormatProvider))
operands.Insert(i, LX.Constant(_formatProvider, typeof(IFormatProvider)));
boundParameters.Add(LX.Constant(_formatProvider, typeof(IFormatProvider)));
else if (pi.ParameterType == typeof(LogEvent))
boundParameters.Add(LX.Property(Context, EvaluationContextLogEventProperty));
else if (_nameResolver.TryBindFunctionParameter(pi, out var binding))
boundParameters.Add(LX.Constant(binding, pi.ParameterType));
else
throw new ArgumentException($"The method `{m.Name}` implementing function `{call.OperatorName}` has parameter `{pi.Name}` which could not be bound.");
}

return LX.Call(m, operands);
return LX.Call(m, boundParameters);
}

static ExpressionBody CompileLogical(Func<ExpressionBody, ExpressionBody, ExpressionBody> apply, ExpressionBody lhs, ExpressionBody rhs)
Expand All @@ -138,8 +149,8 @@ static ExpressionBody CompileLogical(Func<ExpressionBody, ExpressionBody, Expres

protected override ExpressionBody Transform(AccessorExpression spx)
{
var recv = Transform(spx.Receiver);
return LX.Call(TryGetStructurePropertyValueMethod, LX.Constant(StringComparison.OrdinalIgnoreCase), recv, LX.Constant(spx.MemberName, typeof(string)));
var receiver = Transform(spx.Receiver);
return LX.Call(TryGetStructurePropertyValueMethod, LX.Constant(StringComparison.OrdinalIgnoreCase), receiver, LX.Constant(spx.MemberName, typeof(string)));
}

protected override ExpressionBody Transform(ConstantExpression cx)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,17 @@ public override bool TryResolveFunctionName(string name, [MaybeNullWhen(false)]
implementation = null;
return false;
}

public override bool TryBindFunctionParameter(ParameterInfo parameter, [MaybeNullWhen(false)] out object boundValue)
{
foreach (var resolver in _orderedResolvers)
{
if (resolver.TryBindFunctionParameter(parameter, out boundValue))
return true;
}

boundValue = null;
return false;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,21 +30,21 @@ public static Expression Rewrite(Expression expression)
return Instance.Transform(expression);
}

protected override Expression Transform(CallExpression lx)
protected override Expression Transform(CallExpression call)
{
if (lx.Operands.Length != 2)
return base.Transform(lx);
if (call.Operands.Length != 2)
return base.Transform(call);

if (Operators.SameOperator(lx.OperatorName, Operators.IntermediateOpLike))
return TryCompileLikeExpression(lx.IgnoreCase, lx.Operands[0], lx.Operands[1]);
if (Operators.SameOperator(call.OperatorName, Operators.IntermediateOpLike))
return TryCompileLikeExpression(call.IgnoreCase, call.Operands[0], call.Operands[1]);

if (Operators.SameOperator(lx.OperatorName, Operators.IntermediateOpNotLike))
if (Operators.SameOperator(call.OperatorName, Operators.IntermediateOpNotLike))
return new CallExpression(
false,
Operators.RuntimeOpStrictNot,
TryCompileLikeExpression(lx.IgnoreCase, lx.Operands[0], lx.Operands[1]));
TryCompileLikeExpression(call.IgnoreCase, call.Operands[0], call.Operands[1]));

return base.Transform(lx);
return base.Transform(call);
}

Expression TryCompileLikeExpression(bool ignoreCase, Expression corpus, Expression like)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -30,22 +30,22 @@ public static Expression Rewrite(Expression expression)
return Instance.Transform(expression);
}

protected override Expression Transform(CallExpression lx)
protected override Expression Transform(CallExpression call)
{
if (lx.Operands.Length != 2)
return base.Transform(lx);
if (call.Operands.Length != 2)
return base.Transform(call);

if (Operators.SameOperator(lx.OperatorName, Operators.OpIndexOfMatch))
return TryCompileIndexOfMatch(lx.IgnoreCase, lx.Operands[0], lx.Operands[1]);
if (Operators.SameOperator(call.OperatorName, Operators.OpIndexOfMatch))
return TryCompileIndexOfMatch(call.IgnoreCase, call.Operands[0], call.Operands[1]);

if (Operators.SameOperator(lx.OperatorName, Operators.OpIsMatch))
if (Operators.SameOperator(call.OperatorName, Operators.OpIsMatch))
return new CallExpression(
false,
Operators.RuntimeOpNotEqual,
TryCompileIndexOfMatch(lx.IgnoreCase, lx.Operands[0], lx.Operands[1]),
TryCompileIndexOfMatch(call.IgnoreCase, call.Operands[0], call.Operands[1]),
new ConstantExpression(new ScalarValue(-1)));

return base.Transform(lx);
return base.Transform(call);
}

Expression TryCompileIndexOfMatch(bool ignoreCase, Expression corpus, Expression regex)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,21 @@ bool TryTransform(Expression expr, out Expression result)
return !ReferenceEquals(expr, result);
}

protected override Expression Transform(CallExpression lx)
protected override Expression Transform(CallExpression call)
{
var any = false;
var operands = new List<Expression>();
foreach (var op in lx.Operands)
foreach (var op in call.Operands)
{
if (TryTransform(op, out var result))
any = true;
operands.Add(result);
}

if (!any)
return lx;
return call;

return new CallExpression(lx.IgnoreCase, lx.OperatorName, operands.ToArray());
return new CallExpression(call.IgnoreCase, call.OperatorName, operands.ToArray());
}

protected override Expression Transform(ConstantExpression cx)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,7 @@ protected virtual TResult Transform(Expression expression)
};
}

protected abstract TResult Transform(CallExpression lx);
protected abstract TResult Transform(CallExpression call);
protected abstract TResult Transform(ConstantExpression cx);
protected abstract TResult Transform(AmbientNameExpression px);
protected abstract TResult Transform(LocalNameExpression nlx);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,38 +28,38 @@ public static Expression Rewrite(Expression expression)
return Instance.Transform(expression);
}

protected override Expression Transform(CallExpression lx)
protected override Expression Transform(CallExpression call)
{
if (Operators.SameOperator(lx.OperatorName, Operators.OpSubstring) && lx.Operands.Length == 2)
if (Operators.SameOperator(call.OperatorName, Operators.OpSubstring) && call.Operands.Length == 2)
{
var operands = lx.Operands
var operands = call.Operands
.Select(Transform)
.Concat(new[] {CallUndefined()})
.ToArray();
return new CallExpression(lx.IgnoreCase, lx.OperatorName, operands);
return new CallExpression(call.IgnoreCase, call.OperatorName, operands);
}

if (Operators.SameOperator(lx.OperatorName, Operators.OpCoalesce))
if (Operators.SameOperator(call.OperatorName, Operators.OpCoalesce))
{
if (lx.Operands.Length == 0)
if (call.Operands.Length == 0)
return CallUndefined();
if (lx.Operands.Length == 1)
return Transform(lx.Operands.Single());
if (lx.Operands.Length > 2)
if (call.Operands.Length == 1)
return Transform(call.Operands.Single());
if (call.Operands.Length > 2)
{
var first = Transform(lx.Operands.First());
return new CallExpression(lx.IgnoreCase, lx.OperatorName, first,
Transform(new CallExpression(lx.IgnoreCase, lx.OperatorName, lx.Operands.Skip(1).ToArray())));
var first = Transform(call.Operands.First());
return new CallExpression(call.IgnoreCase, call.OperatorName, first,
Transform(new CallExpression(call.IgnoreCase, call.OperatorName, call.Operands.Skip(1).ToArray())));
}
}

if (Operators.SameOperator(lx.OperatorName, Operators.OpToString) &&
lx.Operands.Length == 1)
if (Operators.SameOperator(call.OperatorName, Operators.OpToString) &&
call.Operands.Length == 1)
{
return new CallExpression(lx.IgnoreCase, lx.OperatorName, lx.Operands[0], CallUndefined());
return new CallExpression(call.IgnoreCase, call.OperatorName, call.Operands[0], CallUndefined());
}

return base.Transform(lx);
return base.Transform(call);
}

static CallExpression CallUndefined()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -76,14 +76,14 @@ class WildcardSearch : SerilogExpressionTransformer<IndexerExpression?>
return null;
}

protected override IndexerExpression? Transform(CallExpression lx)
protected override IndexerExpression? Transform(CallExpression call)
{
// If we hit a wildcard-compatible operation, then any wildcards within its operands "belong" to
// it and can't be the result of this search.
if (Operators.WildcardComparators.Contains(lx.OperatorName))
if (Operators.WildcardComparators.Contains(call.OperatorName))
return null;

return lx.Operands.Select(Transform).FirstOrDefault(e => e != null);
return call.Operands.Select(Transform).FirstOrDefault(e => e != null);
}

protected override IndexerExpression? Transform(IndexOfMatchExpression mx)
Expand Down
22 changes: 20 additions & 2 deletions src/Serilog.Expressions/Expressions/NameResolver.cs
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,24 @@ public abstract class NameResolver
/// and accept parameters of type <see cref="LogEventPropertyValue"/>. If the <c>ci</c> modifier is supported,
/// a <see cref="StringComparison"/> should be included in the argument list. If the function is culture-specific,
/// an <see cref="IFormatProvider"/> should be included in the argument list.</remarks>
public abstract bool TryResolveFunctionName(string name, [MaybeNullWhen(false)] out MethodInfo implementation);
public virtual bool TryResolveFunctionName(string name, [MaybeNullWhen(false)] out MethodInfo implementation)
{
implementation = null;
return false;
}

/// <summary>
/// Provide a value for a non-<see cref="LogEventPropertyValue"/> parameter. This allows user-defined state to
/// be threaded through user-defined functions.
/// </summary>
/// <param name="parameter">A parameter of a method implementing a user-defined function, which could not be
/// bound to any of the standard runtime-provided values or operands.</param>
/// <param name="boundValue">The value that should be provided when the method is called.</param>
/// <returns><c>True</c> if the parameter could be bound; otherwise, <c>false</c>.</returns>
public virtual bool TryBindFunctionParameter(ParameterInfo parameter, [MaybeNullWhen(false)] out object boundValue)
{
boundValue = null;
return false;
}
}
}
}
2 changes: 1 addition & 1 deletion src/Serilog.Expressions/Serilog.Expressions.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<Description>An embeddable mini-language for filtering, enriching, and formatting Serilog
events, ideal for use with JSON or XML configuration.</Description>
<VersionPrefix>3.0.0</VersionPrefix>
<VersionPrefix>3.1.0</VersionPrefix>
<Authors>Serilog Contributors</Authors>
<TargetFrameworks>netstandard2.0;netstandard2.1;net5.0</TargetFrameworks>
<TreatWarningsAsErrors>true</TreatWarningsAsErrors>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
// Copyright © 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.Collections.Generic;
using Serilog.Expressions;
using Serilog.Expressions.Compilation;
using Serilog.Expressions.Runtime;
using Serilog.Templates.Ast;
using Serilog.Templates.Compilation.UnreferencedProperties;

namespace Serilog.Templates.Compilation
{
static class TemplateFunctionNameResolver
{
public static NameResolver Build(NameResolver? additionalNameResolver, Template template)
{
var resolvers = new List<NameResolver>
{
new StaticMemberNameResolver(typeof(RuntimeOperators)),
new UnreferencedPropertiesFunction(template)
};

if (additionalNameResolver != null)
resolvers.Add(additionalNameResolver);

return new OrderedNameResolver(resolvers);
}
}
}
Loading