Skip to content

Commit 2972dbb

Browse files
committed
User-defined functions
1 parent 503c73c commit 2972dbb

16 files changed

+262
-31
lines changed

README.md

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,3 +250,40 @@ convert the result to plain-old-.NET-types like `string`, `bool`, `Dictionary<K,
250250
Console.WriteLine("The event matched.");
251251
}
252252
```
253+
254+
## Implementing user-defined functions
255+
256+
User-defined functions can be plugged in by implementing static methods that:
257+
258+
* Return `LogEventPropertyValue?`,
259+
* Have arguments of type `LogEventPropertyValue?`, and
260+
* If the `ci` modifier is supported, accept a `StringComparison` in the first argument position.
261+
262+
For example:
263+
264+
```csharp
265+
public static class MyFunctions
266+
{
267+
public static LogEventPropertyValue? IsFoo(
268+
StringComparison comparison,
269+
LogEventPropertyValue? maybeFoo)
270+
{
271+
if (maybeFoo is ScalarValue sv && sv.Value is string s)
272+
return new ScalarValue(s.Equals("Foo", comparison));
273+
274+
// Undefined - argument was not a string.
275+
return null;
276+
}
277+
}
278+
```
279+
280+
In the example, `IsFoo('Foo')` will evaluate to `true`, `IsFoo('FOO')` will be `false`, `IsFoo('FOO') ci`
281+
will be `true`, and `IsFoo(42)` will be undefined.
282+
283+
User-defined functions are supplied through an instance of `NameResolver`:
284+
285+
```csharp
286+
var myFunctions = new StaticMemberNameResolver(typeof(MyFunctions));
287+
var expr = SerilogExpression.Compile("IsFoo(User.Name)", new[] { myFunctions });
288+
// Filter events based on whether `User.Name` is `'Foo'` :-)
289+
```

src/Serilog.Expressions/Expressions/Compilation/Arrays/ConstantArrayEvaluator.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
using Serilog.Events;
33
using Serilog.Expressions.Ast;
44
using Serilog.Expressions.Compilation.Transformations;
5-
using Serilog.Expressions.Runtime;
65

76
namespace Serilog.Expressions.Compilation.Arrays
87
{
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
using System.Collections.Generic;
2+
using System.Linq;
3+
using Serilog.Expressions.Runtime;
4+
5+
namespace Serilog.Expressions.Compilation
6+
{
7+
static class DefaultFunctionNameResolver
8+
{
9+
public static NameResolver Build(IEnumerable<NameResolver>? orderedResolvers)
10+
{
11+
var defaultResolver = new StaticMemberNameResolver(typeof(RuntimeOperators));
12+
return orderedResolvers == null
13+
? (NameResolver) defaultResolver
14+
: new OrderedNameResolver(
15+
new NameResolver[] {defaultResolver}.Concat(orderedResolvers));
16+
}
17+
}
18+
}

src/Serilog.Expressions/Expressions/Compilation/ExpressionCompiler.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ namespace Serilog.Expressions.Compilation
1010
{
1111
static class ExpressionCompiler
1212
{
13-
public static CompiledExpression Compile(Expression expression)
13+
public static CompiledExpression Compile(Expression expression, NameResolver nameResolver)
1414
{
1515
var actual = expression;
1616
actual = VariadicCallRewriter.Rewrite(actual);
@@ -20,7 +20,7 @@ public static CompiledExpression Compile(Expression expression)
2020
actual = ConstantArrayEvaluator.Evaluate(actual);
2121
actual = WildcardComprehensionTransformer.Expand(actual);
2222

23-
return LinqExpressionCompiler.Compile(actual);
23+
return LinqExpressionCompiler.Compile(actual, nameResolver);
2424
}
2525
}
2626
}

src/Serilog.Expressions/Expressions/Compilation/Linq/Intrinsics.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
using System.Linq;
55
using System.Text.RegularExpressions;
66
using Serilog.Events;
7-
using Serilog.Expressions.Runtime;
87
using Serilog.Formatting.Display;
98

109
namespace Serilog.Expressions.Compilation.Linq

src/Serilog.Expressions/Expressions/Compilation/Linq/LinqExpressionCompiler.cs

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
using Serilog.Events;
77
using Serilog.Expressions.Ast;
88
using Serilog.Expressions.Compilation.Transformations;
9-
using Serilog.Expressions.Runtime;
109
using ConstantExpression = Serilog.Expressions.Ast.ConstantExpression;
1110
using Expression = Serilog.Expressions.Ast.Expression;
1211
using ParameterExpression = System.Linq.Expressions.ParameterExpression;
@@ -17,10 +16,7 @@ namespace Serilog.Expressions.Compilation.Linq
1716
{
1817
class LinqExpressionCompiler : SerilogExpressionTransformer<ExpressionBody>
1918
{
20-
static readonly IDictionary<string, MethodInfo> OperatorMethods = typeof(RuntimeOperators)
21-
.GetTypeInfo()
22-
.GetMethods(BindingFlags.Static | BindingFlags.Public)
23-
.ToDictionary(m => m.Name, StringComparer.OrdinalIgnoreCase);
19+
readonly NameResolver _nameResolver;
2420

2521
static readonly MethodInfo ConstructSequenceValueMethod = typeof(Intrinsics)
2622
.GetMethod(nameof(Intrinsics.ConstructSequenceValue), BindingFlags.Static | BindingFlags.Public)!;
@@ -38,11 +34,16 @@ class LinqExpressionCompiler : SerilogExpressionTransformer<ExpressionBody>
3834
.GetMethod(nameof(Intrinsics.TryGetStructurePropertyValue), BindingFlags.Static | BindingFlags.Public)!;
3935

4036
ParameterExpression Context { get; } = LX.Variable(typeof(LogEvent), "evt");
37+
38+
public LinqExpressionCompiler(NameResolver nameResolver)
39+
{
40+
_nameResolver = nameResolver;
41+
}
4142

42-
public static CompiledExpression Compile(Expression expression)
43+
public static CompiledExpression Compile(Expression expression, NameResolver nameResolver)
4344
{
4445
if (expression == null) throw new ArgumentNullException(nameof(expression));
45-
var compiler = new LinqExpressionCompiler();
46+
var compiler = new LinqExpressionCompiler(nameResolver);
4647
var body = compiler.Transform(expression);
4748
return LX.Lambda<CompiledExpression>(body, compiler.Context).Compile();
4849
}
@@ -54,8 +55,8 @@ ExpressionBody Splice(Expression<CompiledExpression> lambda)
5455

5556
protected override ExpressionBody Transform(CallExpression lx)
5657
{
57-
if (!OperatorMethods.TryGetValue(lx.OperatorName, out var m))
58-
throw new ArgumentException($"The function name `{lx.OperatorName}` was not recognised.");
58+
if (!_nameResolver.TryResolveFunctionName(lx.OperatorName, out var m))
59+
throw new ArgumentException($"The function name `{lx.OperatorName}` was not recognized.");
5960

6061
var parameterCount = m.GetParameters().Count(pi => pi.ParameterType == typeof(LogEventPropertyValue));
6162
if (parameterCount != lx.Operands.Length)
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
using System.Collections.Generic;
2+
using System.Diagnostics.CodeAnalysis;
3+
using System.Linq;
4+
using System.Reflection;
5+
6+
namespace Serilog.Expressions.Compilation
7+
{
8+
class OrderedNameResolver : NameResolver
9+
{
10+
readonly NameResolver[] _orderedResolvers;
11+
12+
public OrderedNameResolver(IEnumerable<NameResolver> orderedResolvers)
13+
{
14+
_orderedResolvers = orderedResolvers.ToArray();
15+
}
16+
17+
public override bool TryResolveFunctionName(string name, [MaybeNullWhen(false)] out MethodInfo implementation)
18+
{
19+
foreach (var resolver in _orderedResolvers)
20+
{
21+
if (resolver.TryResolveFunctionName(name, out implementation))
22+
return true;
23+
}
24+
25+
implementation = null;
26+
return false;
27+
}
28+
}
29+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
using System;
2+
using System.Diagnostics.CodeAnalysis;
3+
using System.Reflection;
4+
using Serilog.Events;
5+
6+
namespace Serilog.Expressions
7+
{
8+
/// <summary>
9+
/// Looks up the implementations of functions that appear in expressions.
10+
/// </summary>
11+
public abstract class NameResolver
12+
{
13+
/// <summary>
14+
/// Match a function name to a method that implements it.
15+
/// </summary>
16+
/// <param name="name">The function name as it appears in the expression source. Names are not case-sensitive.</param>
17+
/// <param name="implementation">A <see cref="MethodInfo"/> implementing the function.</param>
18+
/// <returns><c>True</c> if the name could be resolved; otherwise, <c>false</c>.</returns>
19+
/// <remarks>The method implementing a function should be <c>static</c>, return <see cref="LogEventPropertyValue"/>,
20+
/// and accept parameters of type <see cref="LogEventPropertyValue"/>. If the <c>ci</c> modifier is supported,
21+
/// a <see cref="StringComparison"/> should be in the first argument position.</remarks>
22+
public abstract bool TryResolveFunctionName(string name, [MaybeNullWhen(false)] out MethodInfo implementation);
23+
}
24+
}

src/Serilog.Expressions/Expressions/SerilogExpression.cs

Lines changed: 41 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
1313
// limitations under the License.
1414

1515
using System;
16+
using System.Collections.Generic;
1617
using System.Diagnostics.CodeAnalysis;
1718
using System.Linq;
1819
using Serilog.Expressions.Compilation;
@@ -31,10 +32,15 @@ public static class SerilogExpression
3132
/// Create an evaluation function based on the provided expression.
3233
/// </summary>
3334
/// <param name="expression">An expression.</param>
35+
/// <param name="orderedResolvers">Optionally, an ordered list of <see cref="NameResolver"/>s
36+
/// from which to resolve function names that appear in the template.</param>
3437
/// <returns>A function that evaluates the expression in the context of a log event.</returns>
35-
public static CompiledExpression Compile(string expression)
38+
public static CompiledExpression Compile(
39+
string expression,
40+
IEnumerable<NameResolver>? orderedResolvers = null)
3641
{
37-
if (!TryCompile(expression, out var filter, out var error))
42+
if (expression == null) throw new ArgumentNullException(nameof(expression));
43+
if (!TryCompileImpl(expression, orderedResolvers, out var filter, out var error))
3844
throw new ArgumentException(error);
3945

4046
return filter;
@@ -53,14 +59,46 @@ public static bool TryCompile(
5359
string expression,
5460
[MaybeNullWhen(false)] out CompiledExpression result,
5561
[MaybeNullWhen(true)] out string error)
62+
{
63+
if (expression == null) throw new ArgumentNullException(nameof(expression));
64+
return TryCompileImpl(expression, null, out result, out error);
65+
}
66+
67+
/// <summary>
68+
/// Create an evaluation function based on the provided expression.
69+
/// </summary>
70+
/// <param name="expression">An expression.</param>
71+
/// <param name="result">A function that evaluates the expression in the context of a log event.</param>
72+
/// <param name="error">The reported error, if compilation was unsuccessful.</param>
73+
/// <param name="orderedResolvers">An ordered list of <see cref="NameResolver"/>s
74+
/// from which to resolve function names that appear in the template.</param>
75+
/// <returns>True if the function could be created; otherwise, false.</returns>
76+
/// <remarks>Regular expression syntax errors currently generate exceptions instead of producing friendly
77+
/// errors.</remarks>
78+
public static bool TryCompile(
79+
string expression,
80+
IEnumerable<NameResolver> orderedResolvers,
81+
[MaybeNullWhen(false)] out CompiledExpression result,
82+
[MaybeNullWhen(true)] out string error)
83+
{
84+
if (expression == null) throw new ArgumentNullException(nameof(expression));
85+
if (orderedResolvers == null) throw new ArgumentNullException(nameof(orderedResolvers));
86+
return TryCompileImpl(expression, orderedResolvers, out result, out error);
87+
}
88+
89+
static bool TryCompileImpl(
90+
string expression,
91+
IEnumerable<NameResolver>? orderedResolvers,
92+
[MaybeNullWhen(false)] out CompiledExpression result,
93+
[MaybeNullWhen(true)] out string error)
5694
{
5795
if (!ExpressionParser.TryParse(expression, out var root, out error))
5896
{
5997
result = null;
6098
return false;
6199
}
62100

63-
result = ExpressionCompiler.Compile(root);
101+
result = ExpressionCompiler.Compile(root, DefaultFunctionNameResolver.Build(orderedResolvers));
64102
error = null;
65103
return true;
66104
}
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Reflection;
5+
6+
namespace Serilog.Expressions
7+
{
8+
/// <summary>
9+
/// A <see cref="NameResolver"/> that matches public static members of a class by name.
10+
/// </summary>
11+
public class StaticMemberNameResolver : NameResolver
12+
{
13+
readonly IReadOnlyDictionary<string, MethodInfo> _methods;
14+
15+
/// <summary>
16+
/// Create a <see cref="StaticMemberNameResolver"/> that returns members of the specified <see cref="Type"/>.
17+
/// </summary>
18+
/// <param name="type">A <see cref="Type"/> with public static members implementing runtime functions.</param>
19+
public StaticMemberNameResolver(Type type)
20+
{
21+
if (type == null) throw new ArgumentNullException(nameof(type));
22+
23+
_methods = type
24+
.GetTypeInfo()
25+
.GetMethods(BindingFlags.Static | BindingFlags.Public)
26+
.ToDictionary(m => m.Name, StringComparer.OrdinalIgnoreCase);
27+
}
28+
29+
/// <inheritdoc />
30+
public override bool TryResolveFunctionName(string name, out MethodInfo implementation)
31+
{
32+
return _methods.TryGetValue(name, out implementation);
33+
}
34+
}
35+
}

0 commit comments

Comments
 (0)