Repository: serilog/serilog

Issue:
Repeated placeholders within a message template cause selfLog to incorrectly log a message
**Description**
When passing a placeholder into a message template more than once, it updates a sink correctly but selfLog writes the line 'Positional property count does not match parameter count'.

**Reproduction**
Log.Information("{0}, {0}", "hello");

PropertyBinder.ConstructPositionalProperties() (line 62) compares the number of placeholders to the number of values provided to populate them.
In this case there is a repeated placeholder so it's comparing 2 and 1, resulting in the update to selfLog.

**Expected behavior**
Compare number of unique placeholders with provided values.

**Relevant package, tooling and runtime versions**
v2.9.0
(netCore 3.0)

**Additional context**

Repeated placeholders within a message template cause selfLog to incorrectly log a message
**Description**
When passing a placeholder into a message template more than once, it updates a sink correctly but selfLog writes the line 'Positional property count does not match parameter count'.

**Reproduction**
Log.Information("{0}, {0}", "hello");

PropertyBinder.ConstructPositionalProperties() (line 62) compares the number of placeholders to the number of values provided to populate them.
In this case there is a repeated placeholder so it's comparing 2 and 1, resulting in the update to selfLog.

**Expected behavior**
Compare number of unique placeholders with provided values.

**Relevant package, tooling and runtime versions**
v2.9.0
(netCore 3.0)

**Additional context**



Hints:
Thanks for the note 👍 

I think the check here is deliberately simplistic for performance reasons, but if we do the more expensive check only when the parameter count differs, we could at least reduce the false-positive rate, and hopefully won't incur any additional overhead in the hot path.

If anyone's interested in taking a look, we'd need some benchmarks along with this fix.
Hi, @57r480 @nblumhardt.

I have checked the function `ConstructPositionalProperties` in `PropertyBinder`, and it seems that there is a simple way to solve this situation. In this function, the variable `result` records the relations between the placeholders and the log objects, which are not repeated. Therefore, the comparison can be modified to the length of `result` and the length of log objects, and placed after the construction of `result` (Line 89).

The advantage of this solution is that there is no need to add additional logic, so it has little impact on performance. The disadvantage is that it may change the outputs of `SelfLog` in some cases, for example.
```csharp
log.Information("{0}{1}", "hello");
```
Serilog (v2.9.0) firstly outputs "Positional property count does not match parameter count" then outputs "Unassigned positional value...", while this solution outputs "Unassigned positional value...". 

Since it has a breaking change, I don't know whether it is acceptable.
Hi, @nblumhardt Did I miss something?
Hi! Sorry, @iskcal - I haven't had a chance to dig in deeply yet.
So, logging like 
```cs
Log.Warning("Processing failed for {Mode}. Check if {Mode} is enabled in 'there'", mode);
Log.Warning("Processing failed for {Mode}. Check if {0} is enabled in 'there'", mode);
```
will not be possible right now due to performance reasons? Or is it a different issue?
@STeeL835 both are invalid templates (see https://messagetemplates.org), so this is a different issue (the template being discussed in this thread is valid but not properly processed).
Thanks for the note 👍 

I think the check here is deliberately simplistic for performance reasons, but if we do the more expensive check only when the parameter count differs, we could at least reduce the false-positive rate, and hopefully won't incur any additional overhead in the hot path.

If anyone's interested in taking a look, we'd need some benchmarks along with this fix.
Hi, @57r480 @nblumhardt.

I have checked the function `ConstructPositionalProperties` in `PropertyBinder`, and it seems that there is a simple way to solve this situation. In this function, the variable `result` records the relations between the placeholders and the log objects, which are not repeated. Therefore, the comparison can be modified to the length of `result` and the length of log objects, and placed after the construction of `result` (Line 89).

The advantage of this solution is that there is no need to add additional logic, so it has little impact on performance. The disadvantage is that it may change the outputs of `SelfLog` in some cases, for example.
```csharp
log.Information("{0}{1}", "hello");
```
Serilog (v2.9.0) firstly outputs "Positional property count does not match parameter count" then outputs "Unassigned positional value...", while this solution outputs "Unassigned positional value...". 

Since it has a breaking change, I don't know whether it is acceptable.
Hi, @nblumhardt Did I miss something?
Hi! Sorry, @iskcal - I haven't had a chance to dig in deeply yet.
So, logging like 
```cs
Log.Warning("Processing failed for {Mode}. Check if {Mode} is enabled in 'there'", mode);
Log.Warning("Processing failed for {Mode}. Check if {0} is enabled in 'there'", mode);
```
will not be possible right now due to performance reasons? Or is it a different issue?
@STeeL835 both are invalid templates (see https://messagetemplates.org), so this is a different issue (the template being discussed in this thread is valid but not properly processed).

Patch:
diff --git a/src/Serilog/Capturing/PropertyBinder.cs b/src/Serilog/Capturing/PropertyBinder.cs
index da6a0cbd6..1799a40bb 100644
--- a/src/Serilog/Capturing/PropertyBinder.cs
+++ b/src/Serilog/Capturing/PropertyBinder.cs
@@ -52,9 +52,6 @@ public EventProperty[] ConstructProperties(MessageTemplate messageTemplate, obje
 
     EventProperty[] ConstructPositionalProperties(MessageTemplate template, object?[] messageTemplateParameters, PropertyToken[] positionalProperties)
     {
-        if (positionalProperties.Length != messageTemplateParameters.Length)
-            SelfLog.WriteLine("Positional property count does not match parameter count: {0}", template);
-
         var result = new EventProperty[messageTemplateParameters.Length];
         foreach (var property in positionalProperties)
         {
@@ -77,6 +74,9 @@ EventProperty[] ConstructPositionalProperties(MessageTemplate template, object?[
             }
         }
 
+        if (result.Length != messageTemplateParameters.Length)
+            SelfLog.WriteLine("Positional property count does not match parameter count: {0}", template);
+
         if (next != result.Length)
             Array.Resize(ref result, next);
 


Test patch:
diff --git a/test/Serilog.Tests/Capturing/GetablePropertyFinderTests.cs b/test/Serilog.Tests/Capturing/GettablePropertyFinderTests.cs
similarity index 98%
rename from test/Serilog.Tests/Capturing/GetablePropertyFinderTests.cs
rename to test/Serilog.Tests/Capturing/GettablePropertyFinderTests.cs
index 8ec43d58b..38a3a09df 100644
--- a/test/Serilog.Tests/Capturing/GetablePropertyFinderTests.cs
+++ b/test/Serilog.Tests/Capturing/GettablePropertyFinderTests.cs
@@ -1,6 +1,6 @@
 namespace Serilog.Tests.Capturing;
 
-public class GetablePropertyFinderTests
+public class GettablePropertyFinderTests
 {
     [Fact]
     public void GetPropertiesRecursiveIntegerTypeYieldNoResult()
diff --git a/test/Serilog.Tests/Debugging/SelfLogTests.cs b/test/Serilog.Tests/Debugging/SelfLogTests.cs
index 05c3af5fc..5c6efbf0b 100644
--- a/test/Serilog.Tests/Debugging/SelfLogTests.cs
+++ b/test/Serilog.Tests/Debugging/SelfLogTests.cs
@@ -25,6 +25,47 @@ public void MessagesAreWrittenWhenOutputIsSet()
         Assert.Equal(Messages.Count, count);
     }
 
+    [Fact]
+    public void SelfLogReportsErrorWhenPositionalParameterCountIsMismatched()
+    {
+        Messages = new();
+        SelfLog.Enable(m =>
+        {
+            Messages.Add(m);
+        });
+
+        using var logger = new LoggerConfiguration()
+            .WriteTo.Logger(new SilentLogger())
+            .CreateLogger();
+
+        // ReSharper disable once StructuredMessageTemplateProblem
+        logger.Information("{0}{1}", "hello");
+
+        SelfLog.Disable();
+
+        Assert.Single(Messages);
+    }
+
+    [Fact]
+    public void SelfLogDoesNotReportErrorWhenPositionalParameterIsRepeated()
+    {
+        Messages = new();
+        SelfLog.Enable(m =>
+        {
+            Messages.Add(m);
+        });
+
+        using var logger = new LoggerConfiguration()
+            .WriteTo.Logger(new SilentLogger())
+            .CreateLogger();
+
+        logger.Information("{0}{0}", "hello");
+
+        SelfLog.Disable();
+
+        Assert.Empty(Messages);
+    }
+
     [Fact]
     public void WritingToUndeclaredSinkWritesToSelfLog()
     {