Repository: serilog/serilog

Issue:
Destructure `IReadOnlyDictionary<...>` the same way as `IDictionary<...>`
**Is your feature request related to a problem? Please describe.**
`IReadOnlyDictionary` is destructured as a collection of `KeyValuePair`, which, at least from my point of view, is unwanted.

**Describe the solution you'd like**
`IReadOnlyDictionary` is destructured the same way as `IDictionary`, meaning `{ "Key1Name": "Value1", "Key2Name": "Value2" }`

**Describe alternatives you've considered**
No alternatives.

**Additional context**
```
        using var logger = new LoggerConfiguration().WriteTo.Console().CreateLogger();
        var dictionary = new Dictionary<string, object>
        {
            ["Text"  ] = "alpha",
            ["Number"] = 3.14,
            ["Object"] = new { Child = "new" },
        };


        logger.Information("Dictionary: {@Dictionary}", dictionary);
        // Would be nice to have the same results also for this:
        logger.Information("IReadonlyDictionary: {@IReadonlyDictionary}", dictionary.AsReadOnly()); 
```
Results in:
```
Dictionary: {"Text": "alpha", "Number": 3.14, "Object": {"Child": "new"}}
IReadonlyDictionary: [{"Key": "Text", "Value": "alpha", "$type": "KeyValuePair`2"}, {"Key": "Number", "Value": 3.14, "$type": "KeyValuePair`2"}, {"Key": "Object", "Value": {"Child": "new"}, "$type": "KeyValuePair`2"}]
```


Hints:
Hi @pavel-faltynek - thanks for the note.

The destructuring mechanism works on implementation type, not through interfaces, so this is deliberate though tricky.

Does the `AsReadOnly()` method have a well-known concrete implementation type?

Patch:
diff --git a/src/Serilog/Capturing/PropertyValueConverter.cs b/src/Serilog/Capturing/PropertyValueConverter.cs
index 52afdd36c..b1024c434 100644
--- a/src/Serilog/Capturing/PropertyValueConverter.cs
+++ b/src/Serilog/Capturing/PropertyValueConverter.cs
@@ -346,12 +346,15 @@ string TruncateIfNecessary(string text)
 
     static bool TryGetDictionary(object value, Type valueType, [NotNullWhen(true)] out IDictionary? dictionary)
     {
-        if (valueType.IsConstructedGenericType &&
-            valueType.GetGenericTypeDefinition() == typeof(Dictionary<,>) &&
-            IsValidDictionaryKeyType(valueType.GenericTypeArguments[0]))
+        if (value is IDictionary && valueType.IsConstructedGenericType)
         {
-            dictionary = (IDictionary)value;
-            return true;
+            var definition = valueType.GetGenericTypeDefinition();
+            if ((definition == typeof(Dictionary<,>) || definition == typeof(System.Collections.ObjectModel.ReadOnlyDictionary<,>)) &&
+                IsValidDictionaryKeyType(valueType.GenericTypeArguments[0]))
+            {
+                dictionary = (IDictionary)value;
+                return true;
+            }
         }
 
         dictionary = null;


Test patch:
diff --git a/test/Serilog.Tests/Formatting/Json/JsonFormatterTests.cs b/test/Serilog.Tests/Formatting/Json/JsonFormatterTests.cs
index 5138276b1..2584bf84a 100644
--- a/test/Serilog.Tests/Formatting/Json/JsonFormatterTests.cs
+++ b/test/Serilog.Tests/Formatting/Json/JsonFormatterTests.cs
@@ -1,4 +1,5 @@
 using Newtonsoft.Json;
+using System.Collections.ObjectModel;
 
 namespace Serilog.Tests.Formatting.Json;
 
@@ -188,6 +189,22 @@ public void DictionariesAreDestructuredViaDictionaryValue()
         Assert.Equal(1.2, (double)f.Properties.ADictionary.nums[0]);
     }
 
+    [Fact]
+    public void ReadonlyDictionariesAreDestructuredViaDictionaryValue()
+    {
+        var dict = new ReadOnlyDictionary<string, object>(new Dictionary<string, object>
+        {
+            { "hello", "world" },
+            { "nums", new[] { 1.2 } }
+        });
+
+        var e = DelegatingSink.GetLogEvent(l => l.Information("Value is {ADictionary}", dict));
+        var f = FormatJson(e);
+
+        Assert.Equal("world", (string)f.Properties.ADictionary["hello"]);
+        Assert.Equal(1.2, (double)f.Properties.ADictionary.nums[0]);
+    }
+
     [Fact]
     public void PropertyTokensWithFormatStringsAreIncludedAsRenderings()
     {