diff --git a/README.md b/README.md
index d3265a8..2d497f7 100644
--- a/README.md
+++ b/README.md
@@ -95,12 +95,12 @@ Log.Logger = new LoggerConfiguration()
Note the use of `{Items[0]}`: "holes" in expression templates can include any valid expression.
-Newline-delimited JSON (for example, emulating the [CLEF format](https://github.com/serilog/serilog-formatting-compact)) can be generated
+Newline-delimited JSON (for example, replicating the [CLEF format](https://github.com/serilog/serilog-formatting-compact)) can be generated
using object literals:
```csharp
.WriteTo.Console(new ExpressionTemplate(
- "{ {@t, @mt, @l: if @l = 'Information' then undefined() else @l, @x, ..@p} }\n"))
+ "{ {@t, @mt, @r, @l: if @l = 'Information' then undefined() else @l, @x, ..@p} }\n"))
```
## Language reference
@@ -116,6 +116,10 @@ The following properties are available in expressions:
* `@l` - the event's level, as a `LogEventLevel`
* `@x` - the exception associated with the event, if any, as an `Exception`
* `@p` - a dictionary containing all first-class properties; this supports properties with non-identifier names, for example `@p['snake-case-name']`
+ * `@i` - event id; a 32-bit numeric hash of the event's message template
+ * `@r` - renderings; if any tokens in the message template include .NET-specific formatting, an array of rendered values for each such token
+
+The built-in properties mirror those available in the CLEF format.
### Literals
@@ -175,12 +179,15 @@ calling a function will be undefined if:
| `IsDefined(x)` | Returns `true` if the expression `x` has a value, including `null`, or `false` if `x` is undefined. |
| `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`. |
| `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. |
| `TagOf(o)` | Returns the `TypeTag` field of a captured object (i.e. where `TypeOf(x)` is `'object'`). |
+| `ToString(x, f)` | Applies the format string `f` to the formattable value `x`. |
| `TypeOf(x)` | Returns a string describing the type of expression `x`: a .NET type name if `x` is scalar and non-null, or, `'array'`, `'object'`, `'dictionary'`, `'null'`, or `'undefined'`. |
| `Undefined()` | Explicitly mark an undefined value. |
+| `UtcDateTime(x)` | Convert a `DateTime` or `DateTimeOffset` into a UTC `DateTime`. |
Functions that compare text accept an optional postfix `ci` modifier to select case-insensitive comparisons:
diff --git a/src/Serilog.Expressions/Expressions/BuiltInProperty.cs b/src/Serilog.Expressions/Expressions/BuiltInProperty.cs
index e499dda..cf1a563 100644
--- a/src/Serilog.Expressions/Expressions/BuiltInProperty.cs
+++ b/src/Serilog.Expressions/Expressions/BuiltInProperty.cs
@@ -1,5 +1,20 @@
+// 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.
+
namespace Serilog.Expressions
{
+ // See https://github.com/serilog/serilog-formatting-compact#reified-properties
static class BuiltInProperty
{
public const string Exception = "x";
@@ -8,5 +23,7 @@ static class BuiltInProperty
public const string Message = "m";
public const string MessageTemplate = "mt";
public const string Properties = "p";
+ public const string Renderings = "r";
+ public const string EventId = "i";
}
}
\ No newline at end of file
diff --git a/src/Serilog.Expressions/Expressions/Compilation/Linq/EventIdHash.cs b/src/Serilog.Expressions/Expressions/Compilation/Linq/EventIdHash.cs
new file mode 100644
index 0000000..087a86f
--- /dev/null
+++ b/src/Serilog.Expressions/Expressions/Compilation/Linq/EventIdHash.cs
@@ -0,0 +1,55 @@
+// 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;
+
+// ReSharper disable ForCanBeConvertedToForeach
+
+namespace Serilog.Expressions.Compilation.Linq
+{
+ ///
+ /// Hash functions for message templates. See .
+ ///
+ public static class EventIdHash
+ {
+ ///
+ /// Compute a 32-bit hash of the provided . The
+ /// resulting hash value can be uses as an event id in lieu of transmitting the
+ /// full template string.
+ ///
+ /// A message template.
+ /// A 32-bit hash of the template.
+ [CLSCompliant(false)]
+ public static uint Compute(string messageTemplate)
+ {
+ if (messageTemplate == null) throw new ArgumentNullException(nameof(messageTemplate));
+
+ // Jenkins one-at-a-time https://en.wikipedia.org/wiki/Jenkins_hash_function
+ unchecked
+ {
+ uint hash = 0;
+ for (var i = 0; i < messageTemplate.Length; ++i)
+ {
+ hash += messageTemplate[i];
+ hash += hash << 10;
+ hash ^= hash >> 6;
+ }
+ hash += hash << 3;
+ hash ^= hash >> 11;
+ hash += hash << 15;
+ return hash;
+ }
+ }
+ }
+}
diff --git a/src/Serilog.Expressions/Expressions/Compilation/Linq/Intrinsics.cs b/src/Serilog.Expressions/Expressions/Compilation/Linq/Intrinsics.cs
index 5892758..ff4b3a5 100644
--- a/src/Serilog.Expressions/Expressions/Compilation/Linq/Intrinsics.cs
+++ b/src/Serilog.Expressions/Expressions/Compilation/Linq/Intrinsics.cs
@@ -6,6 +6,7 @@
using System.Text.RegularExpressions;
using Serilog.Events;
using Serilog.Formatting.Display;
+using Serilog.Parsing;
// ReSharper disable ParameterTypeCanBeEnumerable.Global
@@ -15,6 +16,8 @@ static class Intrinsics
{
static readonly LogEventPropertyValue NegativeOne = new ScalarValue(-1);
static readonly LogEventPropertyValue Tombstone = new ScalarValue("😬 (if you see this you have found a bug.)");
+
+ // TODO #19: formatting is culture-specific.
static readonly MessageTemplateTextFormatter MessageFormatter = new MessageTemplateTextFormatter("{Message:lj}");
public static List CollectSequenceElements(LogEventPropertyValue?[] elements)
@@ -159,5 +162,25 @@ public static string RenderMessage(LogEvent logEvent)
MessageFormatter.Format(logEvent, sw);
return sw.ToString();
}
+
+ public static LogEventPropertyValue? GetRenderings(LogEvent logEvent)
+ {
+ List? elements = null;
+ foreach (var token in logEvent.MessageTemplate.Tokens)
+ {
+ if (token is PropertyToken pt && pt.Format != null)
+ {
+ elements ??= new List();
+
+ var space = new StringWriter();
+
+ // TODO #19: formatting is culture-specific.
+ pt.Render(logEvent.Properties, space);
+ elements.Add(new ScalarValue(space.ToString()));
+ }
+ }
+
+ return elements == null ? null : new SequenceValue(elements);
+ }
}
}
\ No newline at end of file
diff --git a/src/Serilog.Expressions/Expressions/Compilation/Linq/LinqExpressionCompiler.cs b/src/Serilog.Expressions/Expressions/Compilation/Linq/LinqExpressionCompiler.cs
index bfcfd85..d56fe2e 100644
--- a/src/Serilog.Expressions/Expressions/Compilation/Linq/LinqExpressionCompiler.cs
+++ b/src/Serilog.Expressions/Expressions/Compilation/Linq/LinqExpressionCompiler.cs
@@ -1,4 +1,18 @@
-using System;
+// 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;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
@@ -124,25 +138,22 @@ protected override ExpressionBody Transform(AmbientPropertyExpression px)
{
if (px.IsBuiltIn)
{
- if (px.PropertyName == BuiltInProperty.Level)
- return Splice(context => new ScalarValue(context.Level));
-
- if (px.PropertyName == BuiltInProperty.Message)
- return Splice(context => new ScalarValue(Intrinsics.RenderMessage(context)));
-
- if (px.PropertyName == BuiltInProperty.Exception)
- return Splice(context => context.Exception == null ? null : new ScalarValue(context.Exception));
-
- if (px.PropertyName == BuiltInProperty.Timestamp)
- return Splice(context => new ScalarValue(context.Timestamp));
-
- if (px.PropertyName == BuiltInProperty.MessageTemplate)
- return Splice(context => new ScalarValue(context.MessageTemplate.Text));
-
- if (px.PropertyName == BuiltInProperty.Properties)
- return Splice(context => new StructureValue(context.Properties.Select(kvp => new LogEventProperty(kvp.Key, kvp.Value)), null));
-
- return LX.Constant(null, typeof(LogEventPropertyValue));
+ return px.PropertyName switch
+ {
+ BuiltInProperty.Level => Splice(context => new ScalarValue(context.Level)),
+ BuiltInProperty.Message => Splice(context => new ScalarValue(Intrinsics.RenderMessage(context))),
+ BuiltInProperty.Exception => Splice(context =>
+ context.Exception == null ? null : new ScalarValue(context.Exception)),
+ BuiltInProperty.Timestamp => Splice(context => new ScalarValue(context.Timestamp)),
+ BuiltInProperty.MessageTemplate => Splice(context => new ScalarValue(context.MessageTemplate.Text)),
+ BuiltInProperty.Properties => Splice(context =>
+ new StructureValue(context.Properties.Select(kvp => new LogEventProperty(kvp.Key, kvp.Value)),
+ null)),
+ BuiltInProperty.Renderings => Splice(context => Intrinsics.GetRenderings(context)),
+ BuiltInProperty.EventId => Splice(context =>
+ new ScalarValue(EventIdHash.Compute(context.MessageTemplate.Text))),
+ _ => LX.Constant(null, typeof(LogEventPropertyValue))
+ };
}
var propertyName = px.PropertyName;
diff --git a/src/Serilog.Expressions/Expressions/Compilation/Wildcards/WildcardComprehensionTransformer.cs b/src/Serilog.Expressions/Expressions/Compilation/Wildcards/WildcardComprehensionTransformer.cs
index 1bcaf0d..6c60f68 100644
--- a/src/Serilog.Expressions/Expressions/Compilation/Wildcards/WildcardComprehensionTransformer.cs
+++ b/src/Serilog.Expressions/Expressions/Compilation/Wildcards/WildcardComprehensionTransformer.cs
@@ -1,4 +1,5 @@
-using Serilog.Expressions.Ast;
+using System.Linq;
+using Serilog.Expressions.Ast;
using Serilog.Expressions.Compilation.Transformations;
namespace Serilog.Expressions.Compilation.Wildcards
@@ -15,25 +16,34 @@ public static Expression Expand(Expression root)
protected override Expression Transform(CallExpression lx)
{
- if (!Operators.WildcardComparators.Contains(lx.OperatorName) || lx.Operands.Length != 2)
+ if (!Operators.WildcardComparators.Contains(lx.OperatorName))
return base.Transform(lx);
- var lhsIs = WildcardSearch.FindElementAtWildcard(lx.Operands[0]);
- var rhsIs = WildcardSearch.FindElementAtWildcard(lx.Operands[1]);
- if (lhsIs != null && rhsIs != null || lhsIs == null && rhsIs == null)
+ IndexerExpression? indexer = null;
+ Expression? wildcardPath = null;
+ var indexerOperand = -1;
+ for (var i = 0; i < lx.Operands.Length; ++i)
+ {
+ indexer = WildcardSearch.FindElementAtWildcard(lx.Operands[i]);
+ if (indexer != null)
+ {
+ indexerOperand = i;
+ wildcardPath = lx.Operands[i];
+ break;
+ }
+ }
+
+ if (indexer == null || wildcardPath == null)
return base.Transform(lx); // N/A, or invalid
- var wildcardPath = lhsIs != null ? lx.Operands[0] : lx.Operands[1];
- var comparand = lhsIs != null ? lx.Operands[1] : lx.Operands[0];
- var indexer = lhsIs ?? rhsIs!;
-
var px = new ParameterExpression("p" + _nextParameter++);
var nestedComparand = NodeReplacer.Replace(wildcardPath, indexer, px);
var coll = indexer.Receiver;
var wc = ((IndexerWildcardExpression)indexer.Index).Wildcard;
- var comparisonArgs = lhsIs != null ? new[] { nestedComparand, comparand } : new[] { comparand, nestedComparand };
+ var comparisonArgs = lx.Operands.ToArray();
+ comparisonArgs[indexerOperand] = nestedComparand;
var body = new CallExpression(lx.IgnoreCase, lx.OperatorName, comparisonArgs);
var lambda = new LambdaExpression(new[] { px }, body);
diff --git a/src/Serilog.Expressions/Expressions/Operators.cs b/src/Serilog.Expressions/Expressions/Operators.cs
index cd22499..b8b7d30 100644
--- a/src/Serilog.Expressions/Expressions/Operators.cs
+++ b/src/Serilog.Expressions/Expressions/Operators.cs
@@ -24,12 +24,15 @@ static class Operators
public const string OpIsDefined = "IsDefined";
public const string OpLastIndexOf = "LastIndexOf";
public const string OpLength = "Length";
+ public const string OpNow = "Now";
public const string OpRound = "Round";
public const string OpStartsWith = "StartsWith";
public const string OpSubstring = "Substring";
public const string OpTagOf = "TagOf";
+ public const string OpToString = "ToString";
public const string OpTypeOf = "TypeOf";
public const string OpUndefined = "Undefined";
+ public const string OpUtcDateTime = "UtcDateTime";
public const string IntermediateOpLike = "_Internal_Like";
public const string IntermediateOpNotLike = "_Internal_NotLike";
diff --git a/src/Serilog.Expressions/Expressions/Runtime/RuntimeOperators.cs b/src/Serilog.Expressions/Expressions/Runtime/RuntimeOperators.cs
index 7cdc21d..580fa2b 100644
--- a/src/Serilog.Expressions/Expressions/Runtime/RuntimeOperators.cs
+++ b/src/Serilog.Expressions/Expressions/Runtime/RuntimeOperators.cs
@@ -467,5 +467,38 @@ public static LogEventPropertyValue _Internal_IsNotNull(LogEventPropertyValue? v
{
return Coerce.IsTrue(condition) ? consequent : alternative;
}
+
+ public static LogEventPropertyValue? ToString(LogEventPropertyValue? value, LogEventPropertyValue? format)
+ {
+ if (!(value is ScalarValue sv && sv.Value is IFormattable formattable) ||
+ !Coerce.String(format, out var fmt))
+ {
+ return null;
+ }
+
+ // TODO #19: formatting is culture-specific.
+ return new ScalarValue(formattable.ToString(fmt, null));
+ }
+
+ public static LogEventPropertyValue? UtcDateTime(LogEventPropertyValue? dateTime)
+ {
+ if (dateTime is ScalarValue sv)
+ {
+ if (sv.Value is DateTimeOffset dto)
+ return new ScalarValue(dto.UtcDateTime);
+
+ if (sv.Value is DateTime dt)
+ return new ScalarValue(dt.ToUniversalTime());
+ }
+
+ return null;
+ }
+
+ // ReSharper disable once UnusedMember.Global
+ public static LogEventPropertyValue? Now()
+ {
+ // DateTimeOffset.Now is the generator for LogEvent.Timestamp.
+ return new ScalarValue(DateTimeOffset.Now);
+ }
}
}
diff --git a/src/Serilog.Expressions/Serilog.Expressions.csproj b/src/Serilog.Expressions/Serilog.Expressions.csproj
index 5fbabb4..132a9ec 100644
--- a/src/Serilog.Expressions/Serilog.Expressions.csproj
+++ b/src/Serilog.Expressions/Serilog.Expressions.csproj
@@ -3,7 +3,7 @@
An embeddable mini-language for filtering, enriching, and formatting Serilog
events, ideal for use with JSON or XML configuration.
- 1.0.1
+ 1.1.0
Serilog Contributors
netstandard2.1
true
diff --git a/test/Serilog.Expressions.Tests/Cases/expression-evaluation-cases.asv b/test/Serilog.Expressions.Tests/Cases/expression-evaluation-cases.asv
index a783b3e..19cf6cf 100644
--- a/test/Serilog.Expressions.Tests/Cases/expression-evaluation-cases.asv
+++ b/test/Serilog.Expressions.Tests/Cases/expression-evaluation-cases.asv
@@ -217,7 +217,13 @@ if undefined() then 1 else 2 ⇶ 2
if 'string' then 1 else 2 ⇶ 2
if true then if false then 1 else 2 else 3 ⇶ 2
-// Typeof
+// ToString
+tostring(16, '000') ⇶ '016'
+tostring('test', '000') ⇶ undefined()
+tostring(16, undefined()) ⇶ undefined()
+tostring(16, null) ⇶ undefined()
+
+// TypeOf
typeof(undefined()) ⇶ 'undefined'
typeof('test') ⇶ 'System.String'
typeof(10) ⇶ 'System.Decimal'
@@ -226,6 +232,9 @@ typeof(null) ⇶ 'null'
typeof([]) ⇶ 'array'
typeof({}) ⇶ 'object'
+// UtcDateTime
+tostring(utcdatetime(now()), 'o') like '20%' ⇶ true
+
// Case comparison
'test' = 'TEST' ⇶ false
'tschüß' = 'TSCHÜSS' ⇶ false
diff --git a/test/Serilog.Expressions.Tests/FormatParityTests.cs b/test/Serilog.Expressions.Tests/FormatParityTests.cs
new file mode 100644
index 0000000..30ff858
--- /dev/null
+++ b/test/Serilog.Expressions.Tests/FormatParityTests.cs
@@ -0,0 +1,173 @@
+using System;
+using System.Collections.Generic;
+using System.IO;
+using System.Linq;
+using Serilog.Events;
+using Serilog.Expressions.Tests.Support;
+using Serilog.Formatting;
+using Serilog.Formatting.Compact;
+using Serilog.Formatting.Json;
+using Serilog.Parsing;
+using Serilog.Templates;
+using Xunit;
+
+namespace Serilog.Expressions.Tests
+{
+ ///
+ /// These tests track the ability of Serilog.Expressions to faithfully reproduce the JSON formats implemented in
+ /// Serilog and Serilog.Formatting.Compact. The tests jump through a few hoops to achieve byte-for-byte correctness;
+ /// in practice, valid JSON in these formats can be constructed with simpler templates.
+ ///
+ public class FormatParityTests
+ {
+ // Implements CLEF-style `@@` escaping of property names that begin with `@`.
+ // ReSharper disable once UnusedMember.Global
+ public static LogEventPropertyValue? ClefEscape(LogEventPropertyValue? logEventProperties)
+ {
+ if (!(logEventProperties is StructureValue st))
+ return null;
+
+ foreach (var check in st.Properties)
+ {
+ if (check.Name.Length > 0 && check.Name[0] == '@')
+ {
+ var properties = new List();
+
+ foreach (var member in st.Properties)
+ {
+ var property = new LogEventProperty(
+ member.Name.Length > 0 && member.Name[0] == '@' ? "@" + member.Name : member.Name,
+ member.Value);
+
+ properties.Add(property);
+ }
+
+ return new StructureValue(properties, st.TypeTag);
+ }
+ }
+
+ return logEventProperties;
+ }
+
+ // Renders a message template with old-style "quoted" strings (expression templates use the newer :lj formatting always).
+ // ReSharper disable once UnusedMember.Global
+ public static LogEventPropertyValue? ClassicRender(LogEventPropertyValue? messageTemplate, LogEventPropertyValue? properties)
+ {
+ if (!(messageTemplate is ScalarValue svt && svt.Value is string smt) ||
+ !(properties is StructureValue stp))
+ {
+ return null;
+ }
+
+ var mt = new MessageTemplateParser().Parse(smt);
+ var space = new StringWriter();
+ mt.Render(stp.Properties.ToDictionary(p => p.Name, p => p.Value), space);
+ return new ScalarValue(space.ToString());
+ }
+
+ // Constructs the Renderings property used in the old JSON format.
+ // ReSharper disable once UnusedMember.Global
+ public static LogEventPropertyValue? ClassicRenderings(LogEventPropertyValue? messageTemplate, LogEventPropertyValue? properties)
+ {
+ if (!(messageTemplate is ScalarValue svt && svt.Value is string smt) ||
+ !(properties is StructureValue stp))
+ {
+ return null;
+ }
+
+ var mt = new MessageTemplateParser().Parse(smt);
+ var tokensWithFormat = mt.Tokens
+ .OfType()
+ .Where(pt => pt.Format != null)
+ .GroupBy(pt => pt.PropertyName);
+
+ // ReSharper disable once PossibleMultipleEnumeration
+ if (!tokensWithFormat.Any())
+ return null;
+
+ var propertiesByName = stp.Properties.ToDictionary(p => p.Name, p => p.Value);
+
+ var renderings = new List();
+ // ReSharper disable once PossibleMultipleEnumeration
+ foreach (var propertyFormats in tokensWithFormat)
+ {
+ var values = new List();
+
+ foreach (var format in propertyFormats)
+ {
+ var sw = new StringWriter();
+
+ format.Render(propertiesByName, sw);
+
+ values.Add(new StructureValue(new []
+ {
+ new LogEventProperty("Format", new ScalarValue(format.Format)),
+ new LogEventProperty("Rendering", new ScalarValue(sw.ToString())),
+ }));
+ }
+
+ renderings.Add(new LogEventProperty(propertyFormats.Key, new SequenceValue(values)));
+ }
+
+ return new StructureValue(renderings);
+ }
+
+ readonly ITextFormatter
+ _clef = new CompactJsonFormatter(),
+ _renderedClef = new RenderedCompactJsonFormatter(),
+ _classic = new JsonFormatter(),
+ _clefExpression = new ExpressionTemplate(
+ "{ {@t: UtcDateTime(@t), @mt, @r, @l: if @l = 'Information' then undefined() else @l, @x, ..ClefEscape(@p)} }" + Environment.NewLine,
+ null, new StaticMemberNameResolver(typeof(FormatParityTests))),
+ _renderedClefExpression = new ExpressionTemplate(
+ "{ {@t: UtcDateTime(@t), @m: ClassicRender(@mt, @p), @i: ToString(@i, 'x8'), @l: if @l = 'Information' then undefined() else @l, @x, ..ClefEscape(@p)} }" + Environment.NewLine,
+ null, new StaticMemberNameResolver(typeof(FormatParityTests))),
+ _classicExpression = new ExpressionTemplate(
+ "{ {Timestamp: @t, Level: @l, MessageTemplate: @mt, Exception: @x, Properties: if IsDefined(@p[?]) then @p else undefined(), Renderings: ClassicRenderings(@mt, @p)} }" + Environment.NewLine,
+ null, new StaticMemberNameResolver(typeof(FormatParityTests)));
+
+ static string Render(
+ ITextFormatter formatter,
+ LogEvent logEvent)
+ {
+ var space = new StringWriter();
+ formatter.Format(logEvent, space);
+ return space.ToString();
+ }
+
+ void AssertWriteParity(
+ LogEventLevel level,
+ Exception? exception,
+ string messageTemplate,
+ params object[] propertyValues)
+ {
+ var sink = new CollectingSink();
+ using (var log = new LoggerConfiguration()
+ .MinimumLevel.Is(LevelAlias.Minimum)
+ .WriteTo.Sink(sink)
+ .CreateLogger())
+ {
+ log.Write(level, exception, messageTemplate, propertyValues);
+ }
+
+ var clef = Render(_clef, sink.SingleEvent);
+ var clefExpression = Render(_clefExpression, sink.SingleEvent);
+ Assert.Equal(clef, clefExpression);
+
+ var renderedClef = Render(_renderedClef, sink.SingleEvent);
+ var renderedClefExpression = Render(_renderedClefExpression, sink.SingleEvent);
+ Assert.Equal(renderedClef, renderedClefExpression);
+
+ var renderedClassic = Render(_classic, sink.SingleEvent);
+ var renderedClassicExpression = Render(_classicExpression, sink.SingleEvent);
+ Assert.Equal(renderedClassic, renderedClassicExpression);
+ }
+
+ [Fact]
+ public void ParityIsMaintained()
+ {
+ AssertWriteParity(LogEventLevel.Information, null, "Hello, world!");
+ AssertWriteParity(LogEventLevel.Debug, new Exception(), "Hello, {Name}, {Number:000}, {Another:#.00}", "world", 42, 3.1);
+ }
+ }
+}
diff --git a/test/Serilog.Expressions.Tests/Serilog.Expressions.Tests.csproj b/test/Serilog.Expressions.Tests/Serilog.Expressions.Tests.csproj
index 1a98f8f..5b3e4ec 100644
--- a/test/Serilog.Expressions.Tests/Serilog.Expressions.Tests.csproj
+++ b/test/Serilog.Expressions.Tests/Serilog.Expressions.Tests.csproj
@@ -1,6 +1,6 @@
- netcoreapp3.1
+ net5.0
true
../../assets/Serilog.snk
true
@@ -12,6 +12,7 @@
+