Skip to content

JsonLayout with support for dotted recursion (#5991)#5999

Merged
snakefoot merged 18 commits intoNLog:devfrom
thatrajeevkr:feature/dottedrecursion
Jan 31, 2026
Merged

JsonLayout with support for dotted recursion (#5991)#5999
snakefoot merged 18 commits intoNLog:devfrom
thatrajeevkr:feature/dottedrecursion

Conversation

@thatrajeevkr
Copy link
Copy Markdown
Contributor

Add DottedRecursion to JsonLayout for flattening nested properties

  • Introduce DottedRecursion option to flatten nested object properties using dotted notation.
  • Implement recursive property traversal with FlattenObjectProperties and AppendFlattenedPropertyValue.
  • Maintain existing behavior for simple values, nulls, and strings when disabled.
  • Preserve compatibility with ExcludeEmptyProperties and MaxRecursionLimit.
  • Supports message-template capture types (Serialize and Stringify) safely.

Example Output :
image
With DottedRecursion = true,

image Without providing DottedRecursion or DottedRecursion = false

@welcome
Copy link
Copy Markdown

welcome bot commented Sep 11, 2025

Thanks for opening this pull request!
We will try to review this soon! Please note that pull requests with unit tests are earlier accepted 👼

@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Sep 11, 2025

Walkthrough

Adds a public JsonLayout property DottedRecursion and a lazy ObjectReflectionCache; introduces internal helpers to optionally flatten nested event properties into dotted keys during JSON rendering with recursion limits; updates cleanup and adds unit tests covering flattening and dictionary handling.

Changes

Cohort / File(s) Summary of changes
JSON layout flattening
src/NLog/Layouts/JSON/JsonLayout.cs
Added public DottedRecursion property and private lazy _objectReflectionCache with accessor; introduced internal helpers AppendPropertyValueInternal, AppendFlattenedPropertyValue, FlattenObjectProperties; routing changed to emit flattened dotted keys when DottedRecursion is true (respecting MaxRecursionLimit and exclusions); reset _objectReflectionCache in CloseLayout; adjusted stringification/serialization paths to align with flattening.
Unit tests for dotted recursion
tests/NLog.UnitTests/Layouts/JsonLayoutTests.cs
Added tests for dotted recursion with IncludeEventProperties = true and DottedRecursion = true: DottedRecursion_SimpleNestedObject_Flattens, DottedRecursion_WithMaxRecursionLimit, DottedRecursion_WithMixedTypes, DottedRecursion_Flattens_Serialize_Object, DottedRecursion_HandlesDictionaryProperties (covers nested objects, recursion limits, mixed types, serialization, and various dictionary key/value cases).

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor App
  participant Logger
  participant JsonLayout
  participant ReflectionCache as ObjectReflectionCache

  App->>Logger: Log(event with Properties)
  Logger->>JsonLayout: Render(logEvent)

  alt DottedRecursion = false
    JsonLayout->>JsonLayout: AppendJsonPropertyValue (emit nested JSON)
    JsonLayout-->>Logger: JSON (nested structure)
  else DottedRecursion = true
    JsonLayout->>JsonLayout: AppendPropertyValueInternal
    JsonLayout->>JsonLayout: AppendFlattenedPropertyValue
    loop Recursion (depth <= MaxRecursionLimit)
      JsonLayout->>ReflectionCache: Get members / enumerate properties
      ReflectionCache-->>JsonLayout: Member list
      JsonLayout->>JsonLayout: FlattenObjectProperties (emit dotted keys)
    end
    JsonLayout-->>Logger: JSON (flattened dotted keys)
  end

  Logger-->>App: Output JSON
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Areas to inspect closely:

  • Correctness and boundary handling of recursion depth in FlattenObjectProperties.
  • Interaction of flattened emission with existing stringification/serialization to avoid duplicate outputs.
  • Thread-safety and lifecycle of the lazy _objectReflectionCache, including reset in CloseLayout.
  • Unit tests for dictionary key handling and mixed-type serialization.

Poem

I hop through nested branches, dot by dot,
I peek inside caches, find each tiny spot,
I flatten leaves until the limit calls stop,
Keys stitched with dots from bottom to top,
A rabbit's log, neat lines — hop! 🐇✨

🚥 Pre-merge checks | ✅ 2 | ❌ 1
❌ Failed checks (1 warning)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 7.14% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check name Status Explanation
Description check ✅ Passed The description is directly related to the changeset, detailing the DottedRecursion feature, its implementation approach, backward compatibility, and including example outputs demonstrating the new functionality.
Title check ✅ Passed The title directly describes the main change: adding support for dotted recursion to JsonLayout. It is concise, specific, and clearly summarizes the primary feature introduced in this pull request.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@thatrajeevkr
Copy link
Copy Markdown
Contributor Author

I'll check the analysis to see what can be done.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

🧹 Nitpick comments (3)
src/NLog/Layouts/JSON/JsonLayout.cs (3)

377-384: Consider extracting the conditional logic into a helper method.

The duplicated conditional logic here could be simplified by extracting it into a dedicated helper method that decides whether to use flattened or regular serialization, improving readability and maintainability.

-                        if (DottedRecursion)
-                        {
-                            AppendFlattenedPropertyValue(prop.Name, prop.Value, prop.Format, logEvent.FormatProvider, prop.CaptureType, sb, sb.Length == orgLength);
-                        }
-                        else
-                        {
-                            AppendJsonPropertyValue(prop.Name, prop.Value, prop.Format, logEvent.FormatProvider, prop.CaptureType, sb, sb.Length == orgLength);
-                        }
+                        AppendPropertyValue(prop.Name, prop.Value, prop.Format, logEvent.FormatProvider, prop.CaptureType, sb, sb.Length == orgLength);

Add this helper method:

private void AppendPropertyValue(string propName, object? propertyValue, string? format, IFormatProvider? formatProvider, MessageTemplates.CaptureType captureType, StringBuilder sb, bool beginJsonMessage)
{
    if (DottedRecursion)
    {
        AppendFlattenedPropertyValue(propName, propertyValue, format, formatProvider, captureType, sb, beginJsonMessage);
    }
    else
    {
        AppendJsonPropertyValue(propName, propertyValue, format, formatProvider, captureType, sb, beginJsonMessage);
    }
}

549-559: Consider using pattern matching for cleaner type checking.

The IsSimpleValue method could be simplified using C# pattern matching for better readability.

 private bool IsSimpleValue(object? value)
 {
-    if (value is null) return true;
-    if (value is string) return true;
-    if (value is IConvertible convertible)
-    {
-        var typeCode = convertible.GetTypeCode();
-        return typeCode != TypeCode.Object;
-    }
-    return false;
+    return value switch
+    {
+        null => true,
+        string => true,
+        IConvertible convertible => convertible.GetTypeCode() != TypeCode.Object,
+        _ => false
+    };
 }

61-62: Make ObjectReflectionCache lazy-init thread-safe.
Replace the non‑synchronized null-coalescing init (_objectReflectionCache ?? (_objectReflectionCache = ...)) with a thread-safe pattern (e.g. Interlocked.CompareExchange like the repo uses for other caches, or Lazy with ExecutionAndPublication) to avoid races when JsonLayout is rendered concurrently.
Location: src/NLog/Layouts/JSON/JsonLayout.cs (lines 61–62).

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3128290 and bbfe6d2.

📒 Files selected for processing (1)
  • src/NLog/Layouts/JSON/JsonLayout.cs (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/NLog/Layouts/JSON/JsonLayout.cs (3)
src/NLog/Internal/ObjectReflectionCache.cs (1)
  • ObjectReflectionCache (56-59)
src/NLog/Internal/StringHelpers.cs (2)
  • StringHelpers (45-170)
  • IsNullOrEmptyString (166-169)
src/NLog/MessageTemplates/ValueFormatter.cs (3)
  • ValueFormatter (47-395)
  • ValueFormatter (90-94)
  • FormatValue (111-134)
🔇 Additional comments (1)
src/NLog/Layouts/JSON/JsonLayout.cs (1)

155-160: LGTM! Well-documented public API addition.

The new DottedRecursion property is properly documented with XML comments and follows the established patterns in the codebase.

Comment thread src/NLog/Layouts/JSON/JsonLayout.cs Outdated
Comment thread src/NLog/Layouts/JSON/JsonLayout.cs Outdated
Comment thread src/NLog/Layouts/JSON/JsonLayout.cs Outdated
Comment thread src/NLog/Layouts/JSON/JsonLayout.cs Outdated
@pull-request-size pull-request-size bot added size/L and removed size/M labels Sep 12, 2025
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (3)
tests/NLog.UnitTests/Layouts/JsonLayoutTests.cs (2)

1199-1227: Solid coverage for basic flattening; add a guard to ensure parent object isn’t rendered.

After asserting the flattened keys, also assert the parent container isn’t emitted as a nested object to prevent regressions.

             var result = jsonLayout.Render(logEventInfo);

             Assert.Contains("\"nestedObject.val\":1", result);
             Assert.Contains("\"nestedObject.val2\":\"value2\"", result);
             Assert.Contains("\"nestedObject.nestedLevel.val3\":3", result);
             Assert.Contains("\"nestedObject.nestedLevel.val4\":\"value4\"", result);
+            Assert.DoesNotContain("\"nestedObject\":{", result);

1229-1255: Strengthen the recursion-limit assertion.

Also assert that deeper flattened paths aren’t present when the limit is reached.

             var result = jsonLayout.Render(logEventInfo);

             Assert.Contains("\"level1.level2.level3\":", result);
             Assert.Contains("{\"val\":\"deepValue\"}", result);
+            Assert.DoesNotContain("\"level1.level2.level3.val\"", result);
src/NLog/Layouts/JSON/JsonLayout.cs (1)

526-537: Edge-case: dots in member names can collide.

Flattening uses '.' as a separator; if inner property names contain dots, collisions/ambiguity can occur. Consider documenting this or making the separator configurable (e.g., via an advanced option), or escaping dots when found.

Would you like a small follow-up PR to add a configurable separator or an escape strategy for '.'?

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between bbfe6d2 and 5cf9f91.

📒 Files selected for processing (2)
  • src/NLog/Layouts/JSON/JsonLayout.cs (5 hunks)
  • tests/NLog.UnitTests/Layouts/JsonLayoutTests.cs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/NLog.UnitTests/Layouts/JsonLayoutTests.cs (2)
src/NLog/Layouts/JSON/JsonLayout.cs (1)
  • JsonLayout (94-97)
src/NLog/LogEventInfo.cs (13)
  • LogEventInfo (51-844)
  • LogEventInfo (79-85)
  • LogEventInfo (93-96)
  • LogEventInfo (105-119)
  • LogEventInfo (129-134)
  • LogEventInfo (145-158)
  • LogEventInfo (169-185)
  • LogEventInfo (196-199)
  • LogEventInfo (210-219)
  • LogEventInfo (487-490)
  • LogEventInfo (499-502)
  • LogEventInfo (527-539)
  • LogEventInfo (550-553)
src/NLog/Layouts/JSON/JsonLayout.cs (3)
src/NLog/Internal/ObjectReflectionCache.cs (1)
  • ObjectReflectionCache (56-59)
src/NLog/Internal/StringHelpers.cs (2)
  • StringHelpers (45-170)
  • IsNullOrEmptyString (166-169)
src/NLog/MessageTemplates/ValueFormatter.cs (3)
  • ValueFormatter (47-395)
  • ValueFormatter (90-94)
  • FormatValue (111-134)
🔇 Additional comments (6)
src/NLog/Layouts/JSON/JsonLayout.cs (6)

61-63: Good: Lazily resolving ObjectReflectionCache.

Approach aligns with existing service resolution patterns.


155-161: API addition LGTM.

DottedRecursion is a clear opt-in flag and well documented.


382-389: Correct switch to flattened vs. regular rendering.

Conditional routing is clean and keeps non-flattened behavior intact.


435-466: Nice dedup: centralizing Serialize/Stringify handling.

This resolves prior duplication and keeps escapes consistent.


490-500: Capture-type safe behavior in flattened path.

Defer to shared handler for Serialize/Stringify; only flatten on Normal. Matches expected semantics.


502-508: Recursion guard + simple/null fast-paths are correct.

Depth check and fallback serialization look good; empty-property filtering is respected.

Also applies to: 510-517

Comment thread src/NLog/Layouts/JSON/JsonLayout.cs Outdated
Comment thread tests/NLog.UnitTests/Layouts/JsonLayoutTests.cs
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (6)
src/NLog/Layouts/JSON/JsonLayout.cs (6)

61-62: Release ObjectReflectionCache on CloseLayout

Set _objectReflectionCache = null; in CloseLayout() to avoid retaining IServiceProvider and cached metadata across layout disposal.

Apply in CloseLayout:

 protected override void CloseLayout()
 {
     _jsonConverter = null;
     _valueFormatter = null;
     _precalculateLayouts = null;
+    _objectReflectionCache = null;
     base.CloseLayout();
 }

Also confirm ResolveService<IServiceProvider>() cannot be null here; if it can, guard the ctor call.


155-161: Document scope and default for DottedRecursion

  • Add [DefaultValue(false)] for config introspection.
  • Clarify in XML doc whether this applies only to IncludeEventProperties (current implementation) to prevent confusion.
-/// <remarks>Default: <see langword="false"/></remarks>
+/// <remarks>Default: <see langword="false"/>. Applies to event properties when <see cref="IncludeEventProperties"/> is true.</remarks>
+[DefaultValue(false)]
 public bool DottedRecursion { get; set; }

377-384: Flattening only for event properties — confirm intent

DottedRecursion is applied to event properties, not GDC/Scope. If the option is intended to be global, mirror this in GDC/Scope blocks; otherwise, note the scope in docs.


521-529: Sanitize dotted names when deriving from dictionary/unknown keys

When flattening, consider applying the same dictionary-key sanitization used by the default serializer before concatenation (e.g., via a shared helper) to keep behavior consistent with SanitizeDictionaryKeys = true, and to handle dots within original keys deterministically.


532-542: Optional: broaden simple-type fast path

To reduce reflection on hot paths, consider treating Guid, DateTimeOffset, TimeSpan, and Uri as simple values directly.


1-1: Ubuntu CI IOException on /tmp/*.tmp — likely test parallelism/collision

  • Ensure file/archiving tests don’t run in parallel or use unique temp paths per test (e.g., Path.Combine(Path.GetTempPath(), Guid.NewGuid().ToString("N"))).
  • Open files with FileOptions.DeleteOnClose where possible; ensure all streams are disposed.
  • If NLog archive/compression is involved, avoid sharing the same target/filename across concurrent tests; add retries on IOException in test helpers.
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 5cf9f91 and 7c8afa1.

📒 Files selected for processing (2)
  • src/NLog/Layouts/JSON/JsonLayout.cs (5 hunks)
  • tests/NLog.UnitTests/Layouts/JsonLayoutTests.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/NLog.UnitTests/Layouts/JsonLayoutTests.cs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-12T01:22:52.382Z
Learnt from: thatrajeevkr
PR: NLog/NLog#5999
File: src/NLog/Layouts/JSON/JsonLayout.cs:162-166
Timestamp: 2025-09-12T01:22:52.382Z
Learning: When suggesting StringBuilder optimizations for NLog layouts marked as [ThreadAgnostic], avoid shared StringBuilder fields as they create thread safety issues. Prefer simple string operations or ThreadLocal<StringBuilder> if allocation profiling shows a real performance impact.

Applied to files:

  • src/NLog/Layouts/JSON/JsonLayout.cs
🧬 Code graph analysis (1)
src/NLog/Layouts/JSON/JsonLayout.cs (3)
src/NLog/Internal/ObjectReflectionCache.cs (1)
  • ObjectReflectionCache (56-59)
src/NLog/Internal/StringHelpers.cs (2)
  • StringHelpers (45-170)
  • IsNullOrEmptyString (166-169)
src/NLog/MessageTemplates/ValueFormatter.cs (3)
  • ValueFormatter (47-395)
  • ValueFormatter (90-94)
  • FormatValue (111-134)
🔇 Additional comments (5)
src/NLog/Layouts/JSON/JsonLayout.cs (5)

430-444: Serialize + MaxRecursionLimit semantics — please confirm

You bypass recursion limits only when CaptureType.Serialize && MaxRecursionLimit <= 1 (using SerializeObjectNoLimit), but use the limited serializer otherwise. Validate this matches prior behavior and expectations for Serialize with higher limits.


481-483: LGTM: centralizing capture-type handling

Delegation to AppendPropertyValueInternal keeps logic single-sourced.


485-495: LGTM: correct delegation for flattening vs. non-flattening

Reuse for Serialize/Stringify and flatten only for Normal is sound.


497-503: MaxRecursionLimit short-circuit is correct

Falling back to regular serialization at/over the limit avoids deep recursion and preserves data.


526-529: LGTM: string.Concat for dotted names

Using string.Concat avoids shared StringBuilder and aligns with thread-agnostic rendering.

Comment thread src/NLog/Layouts/JSON/JsonLayout.cs
@thatrajeevkr
Copy link
Copy Markdown
Contributor Author

@snakefoot How does this look?

@snakefoot
Copy link
Copy Markdown
Contributor

snakefoot commented Sep 14, 2025

I like the approach with parsing string basePropertyName and using recursion.

Would it be possible that DottedRecursion becomes an option-parameter on the NLog JsonSerializeOptions (similar to MaxRecursionLimit), and then move the code into DefaultJsonSerializer ? Since DefaultJsonSerializer already provides protection against cyclic object-graph references.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/NLog/Layouts/JSON/JsonLayout.cs (1)

507-527: Consider null check for serializer cast.

While the cast to Targets.DefaultJsonSerializer is checked for null (line 508), the fallback in line 527 will use the standard non-flattened approach. This is safe but might be unexpected behavior if a custom JSON converter is configured.

Consider logging a diagnostic message or documenting that dotted recursion requires the default JSON serializer:

 var serializer = ResolveService<IJsonConverter>() as Targets.DefaultJsonSerializer;
 if (serializer != null)
 {
     // ... existing dotted recursion logic ...
 }
+else
+{
+    // Dotted recursion requires DefaultJsonSerializer
+    InternalLogger.Debug("JsonLayout: DottedRecursion requires DefaultJsonSerializer, falling back to standard serialization");
+}

 AppendPropertyValueInternal(propName, propertyValue, format, formatProvider, captureType, sb, beginJsonMessage);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7c8afa1 and f7a5bf3.

📒 Files selected for processing (3)
  • src/NLog/Layouts/JSON/JsonLayout.cs (7 hunks)
  • src/NLog/Targets/DefaultJsonSerializer.cs (2 hunks)
  • src/NLog/Targets/JsonSerializeOptions.cs (1 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-12T01:22:52.382Z
Learnt from: thatrajeevkr
PR: NLog/NLog#5999
File: src/NLog/Layouts/JSON/JsonLayout.cs:162-166
Timestamp: 2025-09-12T01:22:52.382Z
Learning: When suggesting StringBuilder optimizations for NLog layouts marked as [ThreadAgnostic], avoid shared StringBuilder fields as they create thread safety issues. Prefer simple string operations or ThreadLocal<StringBuilder> if allocation profiling shows a real performance impact.

Applied to files:

  • src/NLog/Layouts/JSON/JsonLayout.cs
🧬 Code graph analysis (2)
src/NLog/Targets/DefaultJsonSerializer.cs (3)
src/NLog/Targets/JsonSerializeOptions.cs (1)
  • JsonSerializeOptions (42-113)
src/NLog/Internal/SingleItemOptimizedHashSet.cs (2)
  • SingleItemOptimizedHashSet (98-119)
  • SingleItemScopedInsert (64-83)
src/NLog/Layouts/JSON/JsonLayout.cs (1)
  • SerializeObject (75-81)
src/NLog/Layouts/JSON/JsonLayout.cs (2)
src/NLog/Targets/DefaultJsonSerializer.cs (9)
  • DefaultJsonSerializer (47-866)
  • DefaultJsonSerializer (70-73)
  • SerializeObject (80-83)
  • SerializeObject (91-146)
  • SerializeObject (154-157)
  • SerializeObject (166-169)
  • SerializeObject (190-208)
  • SerializeObjectWithDottedRecursion (175-179)
  • SerializeObjectWithDottedRecursion (231-301)
src/NLog/Targets/JsonSerializeOptions.cs (1)
  • JsonSerializeOptions (42-113)
🔇 Additional comments (14)
src/NLog/Targets/JsonSerializeOptions.cs (1)

108-112: LGTM! Clean API addition for dotted recursion feature.

The new DottedRecursion property is well-documented, follows the existing pattern for serialization options, and maintains backward compatibility with a sensible default of false.

src/NLog/Targets/DefaultJsonSerializer.cs (6)

171-179: LGTM! Well-designed entry point for dotted recursion.

The public-facing method correctly delegates to the private implementation with appropriate initialization of the wroteAny flag.


210-230: LGTM! Clean helper methods for JSON formatting.

The AppendDelimiterWhenNeeded and WriteKeyPrefix methods properly handle delimiter insertion and key formatting, respecting both the custom delimiter and standard JSON options.


231-238: Excellent recursion depth protection.

The method correctly checks against MaxRecursionLimit and falls back to standard serialization when the limit is reached, preventing stack overflow issues.


271-298: Consider extracting dotted property name construction.

The string concatenation string.Concat(basePropertyName, ".", property.Name) appears twice (lines 285 and 294). While string.Concat is efficient, extracting this to a local variable would improve readability and ensure consistency.

 foreach (var property in objectPropertyList)
 {
     if (!property.HasNameAndValue)
         continue;

-    string dottedPropertyName = string.Concat(basePropertyName, ".", property.Name);
+    string dottedPropertyName = string.Concat(basePropertyName, ".", property.Name);

     if (property.TypeCode != TypeCode.Object)
     {
         WriteKeyPrefix(destination, options, dottedPropertyName, propertyDelimiter, ref wroteAny);
         SerializeSimpleTypeCodeValue((IConvertible?)property.Value, property.TypeCode, destination, options);
     }
     else
     {
-        if (!SerializeObjectWithDottedRecursion(property.Value, destination, options, objectsInPath, depth + 1, dottedPropertyName, propertyDelimiter, ref wroteAny))
+        if (!SerializeObjectWithDottedRecursion(property.Value, destination, options, objectsInPath, depth + 1, dottedPropertyName, propertyDelimiter, ref wroteAny))
             return false;
     }
 }

Actually, the code already follows this pattern correctly. The variable is declared once and reused.


231-301: Excellent implementation of dotted recursion with proper safeguards.

The implementation correctly:

  • Protects against stack overflow with recursion depth checks
  • Handles circular references using SingleItemOptimizedHashSet
  • Processes all value types (null, primitives, DateTimeOffset, collections, complex objects)
  • Maintains proper JSON structure with appropriate key prefixes
  • Gracefully degrades to standard serialization when limits are reached

265-269: Approve — collection serialization preserves valid JSON structure.

WriteKeyPrefix emits the quoted key + colon and SerializeObject writes the object/array tokens; existing unit tests exercise dotted recursion and collection cases (JsonLayoutTests.DottedRecursion_WithMixedTypes and DefaultJsonSerializerTestsBase collection tests) and assert the expected output, so no change required.

src/NLog/Layouts/JSON/JsonLayout.cs (7)

154-172: LGTM! Well-integrated DottedRecursion property.

The property follows the established pattern, properly invalidates the JSON converter when changed, and includes appropriate XML documentation.


57-57: Thread-safe converter initialization with dotted recursion support.

The lazy initialization pattern correctly passes the DottedRecursion flag to the converter constructor, ensuring thread-safe creation.


441-441: Clean delimiter extraction for dotted properties.

The code correctly extracts the delimiter by removing the trailing quote character, maintaining consistency with the JSON formatting options.


388-396: Clean conditional routing based on DottedRecursion flag.

The code correctly routes to either flattened or standard property value rendering based on the DottedRecursion setting, maintaining backward compatibility.


476-493: Efficient empty property detection logic.

The implementation correctly checks for empty string values by detecting consecutive quotes without an escape character, avoiding unnecessary allocations.


68-72: No change required — SanitizeDictionaryKeys affects only dictionary keys

Verified: SanitizeDictionaryKeys sanitizes dictionary keys only and does not alter regular object property names, so keeping SanitizeDictionaryKeys = true (with DottedRecursion) is intentional and safe.


499-529: No change required — caller appends the closing brace.

AppendFlattenedPropertyValue appends '{' + newline + indentation before calling the serializer, and the caller appends _completeJsonMessage when sb.Length > orgLength, so the closing '}' is added even if this method returns early.

@thatrajeevkr
Copy link
Copy Markdown
Contributor Author

thatrajeevkr commented Sep 16, 2025

@snakefoot I have changed it as per your comment, please take a look. For the coverage part, I will add in a testcase to solve that.

Comment thread src/NLog/Layouts/JSON/JsonLayout.cs
Comment thread src/NLog/Layouts/JSON/JsonLayout.cs Outdated
@thatrajeevkr
Copy link
Copy Markdown
Contributor Author

As per your suggestion, created a new function in IJsonConvertor to handle dottedRecursion so that It doesnt depend on the DefaultJsonSerializer , This change makes it a standalone change and only depends on the IJsonConverter .
Necessary changes to other files referencing the IJsonConverter have also been changed.
Added a few test cases for extra coverage as well.
@snakefoot

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 4

♻️ Duplicate comments (1)
src/NLog/Layouts/JSON/JsonLayout.cs (1)

471-476: Guard JSON escaping on Stringify to avoid corruption (repeat)

PerformJsonEscapeIfNeeded assumes surrounding quotes. Guard to prevent corrupting output when quotes are omitted.

 int originalStart = sb.Length;
 ValueFormatter.FormatValue(propertyValue, format, captureType, formatProvider, sb);
-PerformJsonEscapeIfNeeded(sb, originalStart);
+if (sb.Length > originalStart && sb[originalStart] == '"' && sb[sb.Length - 1] == '"')
+{
+    PerformJsonEscapeIfNeeded(sb, originalStart);
+}
🧹 Nitpick comments (1)
tests/NLog.UnitTests/Config/ServiceRepositoryTests.cs (1)

205-209: Align nullability with interface to avoid warnings

Match the IJsonConverter signature to prevent nullable‑reference warnings and accept null when the caller passes it.

- public bool SerializeObjectDotted(object value, StringBuilder destination, NLog.Targets.JsonSerializeOptions options, string basePropertyName, string propertyDelimiter)
+ public bool SerializeObjectDotted(object? value, StringBuilder destination, NLog.Targets.JsonSerializeOptions options, string basePropertyName, string? propertyDelimiter)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between f7a5bf3 and 55c3255.

📒 Files selected for processing (6)
  • src/NLog/IJsonConverter.cs (1 hunks)
  • src/NLog/Layouts/JSON/JsonLayout.cs (8 hunks)
  • src/NLog/MessageTemplates/ValueFormatter.cs (1 hunks)
  • src/NLog/Targets/DefaultJsonSerializer.cs (2 hunks)
  • tests/NLog.UnitTests/Config/ServiceRepositoryTests.cs (1 hunks)
  • tests/NLog.UnitTests/Layouts/JsonLayoutTests.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/NLog.UnitTests/Layouts/JsonLayoutTests.cs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-12T01:22:52.382Z
Learnt from: thatrajeevkr
PR: NLog/NLog#5999
File: src/NLog/Layouts/JSON/JsonLayout.cs:162-166
Timestamp: 2025-09-12T01:22:52.382Z
Learning: When suggesting StringBuilder optimizations for NLog layouts marked as [ThreadAgnostic], avoid shared StringBuilder fields as they create thread safety issues. Prefer simple string operations or ThreadLocal<StringBuilder> if allocation profiling shows a real performance impact.

Applied to files:

  • src/NLog/Layouts/JSON/JsonLayout.cs
🧬 Code graph analysis (5)
src/NLog/IJsonConverter.cs (5)
src/NLog/Layouts/JSON/JsonLayout.cs (1)
  • SerializeObjectDotted (88-93)
src/NLog/MessageTemplates/ValueFormatter.cs (1)
  • SerializeObjectDotted (89-93)
src/NLog/Targets/DefaultJsonSerializer.cs (1)
  • SerializeObjectDotted (180-184)
tests/NLog.UnitTests/Config/ServiceRepositoryTests.cs (1)
  • SerializeObjectDotted (205-209)
src/NLog/Targets/JsonSerializeOptions.cs (1)
  • JsonSerializeOptions (42-113)
src/NLog/MessageTemplates/ValueFormatter.cs (5)
src/NLog/IJsonConverter.cs (1)
  • SerializeObjectDotted (58-58)
src/NLog/Layouts/JSON/JsonLayout.cs (1)
  • SerializeObjectDotted (88-93)
src/NLog/Targets/DefaultJsonSerializer.cs (1)
  • SerializeObjectDotted (180-184)
tests/NLog.UnitTests/Config/ServiceRepositoryTests.cs (1)
  • SerializeObjectDotted (205-209)
src/NLog/Targets/JsonSerializeOptions.cs (1)
  • JsonSerializeOptions (42-113)
tests/NLog.UnitTests/Config/ServiceRepositoryTests.cs (5)
src/NLog/IJsonConverter.cs (2)
  • SerializeObjectDotted (58-58)
  • SerializeObject (47-47)
src/NLog/Layouts/JSON/JsonLayout.cs (2)
  • SerializeObjectDotted (88-93)
  • SerializeObject (75-81)
src/NLog/MessageTemplates/ValueFormatter.cs (2)
  • SerializeObjectDotted (89-93)
  • SerializeObject (81-87)
src/NLog/Targets/DefaultJsonSerializer.cs (6)
  • SerializeObjectDotted (180-184)
  • SerializeObject (80-83)
  • SerializeObject (91-146)
  • SerializeObject (154-157)
  • SerializeObject (166-169)
  • SerializeObject (195-213)
src/NLog/Targets/JsonSerializeOptions.cs (1)
  • JsonSerializeOptions (42-113)
src/NLog/Targets/DefaultJsonSerializer.cs (4)
src/NLog/IJsonConverter.cs (2)
  • SerializeObjectDotted (58-58)
  • SerializeObject (47-47)
src/NLog/Layouts/JSON/JsonLayout.cs (2)
  • SerializeObjectDotted (88-93)
  • SerializeObject (75-81)
src/NLog/Targets/JsonSerializeOptions.cs (1)
  • JsonSerializeOptions (42-113)
src/NLog/Internal/SingleItemOptimizedHashSet.cs (2)
  • SingleItemOptimizedHashSet (98-119)
  • SingleItemScopedInsert (64-83)
src/NLog/Layouts/JSON/JsonLayout.cs (4)
src/NLog/MessageTemplates/ValueFormatter.cs (5)
  • IJsonConverter (66-72)
  • ValueFormatter (47-401)
  • ValueFormatter (96-100)
  • SerializeObjectDotted (89-93)
  • SerializeObject (81-87)
src/NLog/Targets/DefaultJsonSerializer.cs (8)
  • DefaultJsonSerializer (47-871)
  • DefaultJsonSerializer (70-73)
  • SerializeObjectDotted (180-184)
  • SerializeObject (80-83)
  • SerializeObject (91-146)
  • SerializeObject (154-157)
  • SerializeObject (166-169)
  • SerializeObject (195-213)
src/NLog/Targets/JsonSerializeOptions.cs (1)
  • JsonSerializeOptions (42-113)
src/NLog/IJsonConverter.cs (2)
  • SerializeObjectDotted (58-58)
  • SerializeObject (47-47)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: task-list-completed
🔇 Additional comments (7)
src/NLog/MessageTemplates/ValueFormatter.cs (1)

89-93: Straightforward delegate; LGTM

Delegating to serializer is correct; ValueFormatter itself shouldn’t handle dotted recursion.

src/NLog/Targets/DefaultJsonSerializer.cs (1)

171-184: Good additive API surface

Entry point for dotted serialization is clear and contained.

src/NLog/Layouts/JSON/JsonLayout.cs (5)

57-57: Converter cache correctly invalidated on option flips

Passing DottedRecursion into the converter factory and resetting cache when toggled is correct.


68-73: Serializer option wiring looks good

Propagating MaxRecursionLimit, SuppressSpaces, SanitizeDictionaryKeys, and DottedRecursion into options is consistent.


161-179: Option surface: DottedRecursion

Property and invalidation look correct.


395-403: Branching to flattened path is fine

Top‑level switch based on DottedRecursion is clear.


448-448: Delimiter derivation is neat

Reusing _beginJsonPropertyName to derive dotted delimiter keeps formatting consistent.

Comment thread src/NLog/IJsonConverter.cs Outdated
Comment thread src/NLog/Layouts/JSON/JsonLayout.cs Outdated
Comment thread src/NLog/Layouts/JSON/JsonLayout.cs Outdated
Comment thread src/NLog/Targets/DefaultJsonSerializer.cs Outdated
@NLog NLog deleted a comment from coderabbitai bot Sep 17, 2025
@NLog NLog deleted a comment from coderabbitai bot Sep 17, 2025
@NLog NLog deleted a comment from coderabbitai bot Sep 19, 2025
@NLog NLog deleted a comment from coderabbitai bot Sep 20, 2025
@NLog NLog deleted a comment from coderabbitai bot Sep 20, 2025
@NLog NLog deleted a comment from coderabbitai bot Sep 20, 2025
@NLog NLog deleted a comment from coderabbitai bot Sep 23, 2025
@thatrajeevkr thatrajeevkr force-pushed the feature/dottedrecursion branch from 55c3255 to 6d4c2c0 Compare September 29, 2025 23:09
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 55c3255 and 6d4c2c0.

📒 Files selected for processing (1)
  • src/NLog/Layouts/JSON/JsonLayout.cs (5 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-12T01:22:52.437Z
Learnt from: thatrajeevkr
PR: NLog/NLog#5999
File: src/NLog/Layouts/JSON/JsonLayout.cs:162-166
Timestamp: 2025-09-12T01:22:52.437Z
Learning: When suggesting StringBuilder optimizations for NLog layouts marked as [ThreadAgnostic], avoid shared StringBuilder fields as they create thread safety issues. Prefer simple string operations or ThreadLocal<StringBuilder> if allocation profiling shows a real performance impact.

Applied to files:

  • src/NLog/Layouts/JSON/JsonLayout.cs
🧬 Code graph analysis (1)
src/NLog/Layouts/JSON/JsonLayout.cs (3)
src/NLog/Internal/ObjectReflectionCache.cs (1)
  • ObjectReflectionCache (56-59)
src/NLog/Internal/StringHelpers.cs (2)
  • StringHelpers (45-170)
  • IsNullOrEmptyString (166-169)
src/NLog/MessageTemplates/ValueFormatter.cs (3)
  • ValueFormatter (47-395)
  • ValueFormatter (90-94)
  • FormatValue (111-134)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: task-list-completed
🔇 Additional comments (7)
src/NLog/Layouts/JSON/JsonLayout.cs (7)

155-160: LGTM!

The DottedRecursion property is well-documented and defaults to false, preserving backward compatibility.


377-385: LGTM!

The routing logic correctly switches between flattened and standard property rendering based on the DottedRecursion flag. Note that this only affects event properties, not GDC or ScopeContext properties.


430-464: LGTM!

AppendPropertyValueInternal correctly consolidates the Serialize/Stringify handling that was previously duplicated. The guard on line 455 ensures JSON escape is only applied when the formatted value is actually quoted, addressing the past review concern.


484-487: LGTM!

Simple delegation to AppendPropertyValueInternal maintains backward compatibility.


489-499: LGTM!

Correctly limits flattening to CaptureType.Normal and delegates Serialize/Stringify to the standard path. Starting recursion at depth 0 is appropriate.


501-534: LGTM! Recursion protection and threading addressed.

FlattenObjectProperties correctly:

  • Tracks recursion depth and falls back to standard serialization at MaxRecursionLimit (line 503-506)
  • Honors ExcludeEmptyProperties (line 509-510)
  • Uses thread-safe string.Concat for dotted names (line 530)
  • Passes false for beginJsonMessage in nested calls (line 532)

All issues flagged in past reviews have been resolved.


536-546: LGTM!

IsSimpleValue correctly identifies primitives and simple types using IConvertible and TypeCode, ensuring only complex objects are flattened.

Comment thread src/NLog/Layouts/JSON/JsonLayout.cs
@NLog NLog deleted a comment from coderabbitai bot Oct 10, 2025
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/NLog/Layouts/JSON/JsonLayout.cs (1)

450-465: LGTM! Proper handling of different capture types.

The method correctly delegates Stringify to the standard path (since stringification doesn't benefit from property expansion) and handles Serialize/Normal through flattening. The negative startDepth for Serialize mode effectively loosens the depth bound to allow deeper object traversal.

Consider adding a named constant for the -10 offset to make the intent more explicit:

private const int SerializeModeDepthOffset = 10;
// Then use: Math.Min(0, MaxRecursionLimit - SerializeModeDepthOffset)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3cde731 and 916c13d.

📒 Files selected for processing (1)
  • src/NLog/Layouts/JSON/JsonLayout.cs (6 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-12T01:22:52.437Z
Learnt from: thatrajeevkr
PR: NLog/NLog#5999
File: src/NLog/Layouts/JSON/JsonLayout.cs:162-166
Timestamp: 2025-09-12T01:22:52.437Z
Learning: When suggesting StringBuilder optimizations for NLog layouts marked as [ThreadAgnostic], avoid shared StringBuilder fields as they create thread safety issues. Prefer simple string operations or ThreadLocal<StringBuilder> if allocation profiling shows a real performance impact.

Applied to files:

  • src/NLog/Layouts/JSON/JsonLayout.cs
🧬 Code graph analysis (1)
src/NLog/Layouts/JSON/JsonLayout.cs (1)
src/NLog/Internal/ObjectReflectionCache.cs (1)
  • ObjectReflectionCache (56-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: task-list-completed
🔇 Additional comments (7)
src/NLog/Layouts/JSON/JsonLayout.cs (7)

61-62: LGTM! Consistent lazy initialization pattern.

The ObjectReflectionCache field follows the same lazy initialization pattern as _jsonConverter and _valueFormatter, with proper cleanup in CloseLayout(). This is thread-safe for the [ThreadAgnostic] layout.


155-160: LGTM! Well-documented property with safe defaults.

The DottedRecursion property is properly documented and defaults to false, maintaining backward compatibility with existing behavior.


289-289: LGTM! Proper cleanup added.

The _objectReflectionCache cleanup is correctly added alongside the existing cleanup for _jsonConverter and _valueFormatter, preventing reference retention after layout disposal.


374-381: LGTM! Clean conditional routing.

The routing logic properly directs event properties to the flattened or standard rendering path based on the DottedRecursion setting, maintaining backward compatibility when the feature is disabled.


446-448: LGTM! Duplication eliminated through refactoring.

The refactoring to delegate to AppendPropertyValueInternal successfully eliminates the code duplication that was flagged in previous reviews, while maintaining the method signature for compatibility.


467-500: LGTM! Shared logic properly extracted.

The extracted AppendPropertyValueInternal successfully consolidates the common handling for Serialize and Stringify capture types. The explicit quote wrapping for Stringify (lines 490, 494) properly guards the escape operation, addressing previous review concerns.


502-540: LGTM! Solid recursive flattening implementation.

The FlattenObjectProperties method properly implements dotted-key flattening with several good safeguards:

  • Recursion protection: Depth tracking against MaxRecursionLimit prevents stack overflow (line 504)
  • Thread safety: string.Concat (line 532) avoids the shared StringBuilder issues from earlier reviews
  • Empty value handling: ExcludeEmptyProperties is correctly honored (line 510)
  • Delimiter management: The isFirstChild flag with length check (lines 533-538) properly handles children that emit nothing, preventing malformed JSON

The fallback to standard serialization at max depth (line 506) and for simple values (lines 513-517, 520-524) ensures the feature degrades gracefully.

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
src/NLog/Layouts/JSON/JsonLayout.cs (2)

155-161: Public option DottedRecursion added — consider consistent scope

Looks good. Consider applying the same flattening semantics to GDC and Scope properties for consistency with event properties.

Would you like me to propose the minimal changes to also flatten GDC/Scope when DottedRecursion = true?


374-381: Flattening applies only to event properties

Currently DottedRecursion is not applied to GDC/Scope. If the option is meant to be layout-wide, extend the same logic to those sources for consistency.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 3cde731 and 916c13d.

📒 Files selected for processing (1)
  • src/NLog/Layouts/JSON/JsonLayout.cs (6 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-12T01:22:52.437Z
Learnt from: thatrajeevkr
PR: NLog/NLog#5999
File: src/NLog/Layouts/JSON/JsonLayout.cs:162-166
Timestamp: 2025-09-12T01:22:52.437Z
Learning: When suggesting StringBuilder optimizations for NLog layouts marked as [ThreadAgnostic], avoid shared StringBuilder fields as they create thread safety issues. Prefer simple string operations or ThreadLocal<StringBuilder> if allocation profiling shows a real performance impact.

Applied to files:

  • src/NLog/Layouts/JSON/JsonLayout.cs
🧬 Code graph analysis (1)
src/NLog/Layouts/JSON/JsonLayout.cs (1)
src/NLog/Internal/ObjectReflectionCache.cs (1)
  • ObjectReflectionCache (56-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: task-list-completed
🔇 Additional comments (5)
src/NLog/Layouts/JSON/JsonLayout.cs (5)

61-63: Lazy ObjectReflectionCache + disposal cleanup looks good

Initialization via ResolveService and nulling in CloseLayout are correct.


289-289: Dispose pattern updated

Clearing _objectReflectionCache in CloseLayout() is correct and prevents lingering references.


445-449: Centralized handling via AppendPropertyValueInternal

Good consolidation; reduces duplication and keeps capture-type logic in one place.


526-539: Correctly preserves beginJsonMessage when child emits nothing

The length check before flipping isFirstChild prevents malformed JSON when first child is skipped. Good.


532-532: Good choice: string.Concat for dotted names

Avoids shared StringBuilder and keeps layout thread-safe. Based on learnings.

Comment thread src/NLog/Layouts/JSON/JsonLayout.cs
@snakefoot snakefoot linked an issue Oct 23, 2025 that may be closed by this pull request
@thatrajeevkr
Copy link
Copy Markdown
Contributor Author

thatrajeevkr commented Nov 11, 2025

  • Improve handling of list-collections. Instead of outputting the List-Count, then output the List-items. (This is done for dictionary <int,int> to output in a different way since the keys cannot be int values. I have changed the testcase to show what it would be after this change)
  • Implement handling of bad-objects with cyclic object-references and getter-properties-that-throws (I looked carefully through the code to see where it could cause potential issues but currently there are safeguards in place both for cyclic references and getter properties in the below places.)

private bool SerializeObjectWithReflection(object? value, StringBuilder destination, JsonSerializeOptions options, ref SingleItemOptimizedHashSet<object> objectsInPath, int depth) { if (value is null || objectsInPath.Contains(value)) //Cycle detection { return false; }

public PropertyValue(object owner, PropertyInfo propertyInfo) { Name = propertyInfo.Name; _typecode = TypeCode.Object; try { Value = propertyInfo.GetValue(owner, null); // getter is called here } catch { Value = null; // any exception thrown by the getter is caught } }

public PropertyValue(object owner, FastPropertyLookup fastProperty) { Name = fastProperty.Name; _typecode = fastProperty.TypeCode; try { Value = fastProperty.ValueLookup(owner, ArrayHelper.Empty<object>()); // also wraps getter } catch { Value = null; // safe if the getter throws } }

Also moving the logic from DefaultJsonSerializer would require the need to handle cyclic references in a more general way in ObjectReflectionCache than right now which would increase extra complexity and require extra complex API to keep track of the current path. That could be a whole PR on its own since it would require changing and moving a lot of functionality across, do you think its required now along with this change?

@snakefoot

Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (4)
src/NLog/Layouts/JSON/JsonLayout.cs (4)

37-37: Unused import: System.Collections is not referenced in the code.

The System.Collections namespace is imported but no types from it are used. The Dictionary<int, int> on line 521 is from System.Collections.Generic, which is already imported on line 38.

Apply this diff to remove the unused import:

-    using System.Collections;
     using System.Collections.Generic;

452-467: Consider normalizing MaxRecursionLimit to prevent edge-case issues with negative values.

While MaxRecursionLimit is typically positive (default is 1), the calculation MaxRecursionLimit - 10 on line 462 can overflow if MaxRecursionLimit is near int.MinValue, and the depth check on line 506 could behave unexpectedly with negative limits. Normalizing the limit at the start of these methods would make the logic more robust.

Apply this diff to normalize the limit:

 private void AppendFlattenedPropertyValue(string propName, object? propertyValue, string? format, IFormatProvider? formatProvider, MessageTemplates.CaptureType captureType, StringBuilder sb, bool beginJsonMessage)
 {
     if (captureType == MessageTemplates.CaptureType.Stringify)
     {
         AppendPropertyValueInternal(propName, propertyValue, format, formatProvider, captureType, sb, beginJsonMessage);
     }
     else
     {
+        int effectiveMaxDepth = Math.Max(0, MaxRecursionLimit);
         // Allow flattening also for Serialize, by starting at a negative depth to effectively loosen depth bound
         int startDepth = captureType == MessageTemplates.CaptureType.Serialize
-            ? Math.Min(0, MaxRecursionLimit - 10)
+            ? Math.Min(0, effectiveMaxDepth - 10)
             : 0;
 
         FlattenObjectProperties(propName, propertyValue, sb, beginJsonMessage, startDepth);
     }
 }

And in FlattenObjectProperties:

 private void FlattenObjectProperties(string basePropertyName, object? propertyValue, StringBuilder sb, bool beginJsonMessage, int depth = 0)
 {
-    if (depth >= MaxRecursionLimit)
+    int effectiveMaxDepth = Math.Max(0, MaxRecursionLimit);
+    if (depth >= effectiveMaxDepth)
     {

521-530: Collection handling is partial but represents incremental progress.

The special case for Dictionary<int, int> outputs individual items rather than just the Count property, which is an improvement over the default object reflection behavior. However, this leaves other non-string-keyed collections (e.g., Dictionary<int, string>, List<int>) still outputting only metadata properties.

This is acceptable as an incremental improvement, but consider expanding collection support in future iterations to handle IEnumerable types more comprehensively.


526-526: Optional: Avoid string interpolation in loop for minor allocation reduction.

Line 526 uses string interpolation inside a foreach loop, which allocates a new string on each iteration. While Dictionary<int, int> iteration is unlikely to be a hot path in typical logging scenarios, you could reduce allocations by using string.Concat or a StringBuilder if profiling shows this is a concern.

Example with string.Concat:

-                    string indexedPropertyName = $"{basePropertyName}[{keyString}]";
+                    string indexedPropertyName = string.Concat(basePropertyName, "[", keyString, "]");
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 916c13d and 466d3d4.

📒 Files selected for processing (2)
  • src/NLog/Layouts/JSON/JsonLayout.cs (7 hunks)
  • tests/NLog.UnitTests/Layouts/JsonLayoutTests.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/NLog.UnitTests/Layouts/JsonLayoutTests.cs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-12T01:22:52.437Z
Learnt from: thatrajeevkr
Repo: NLog/NLog PR: 5999
File: src/NLog/Layouts/JSON/JsonLayout.cs:162-166
Timestamp: 2025-09-12T01:22:52.437Z
Learning: When suggesting StringBuilder optimizations for NLog layouts marked as [ThreadAgnostic], avoid shared StringBuilder fields as they create thread safety issues. Prefer simple string operations or ThreadLocal<StringBuilder> if allocation profiling shows a real performance impact.

Applied to files:

  • src/NLog/Layouts/JSON/JsonLayout.cs
🧬 Code graph analysis (1)
src/NLog/Layouts/JSON/JsonLayout.cs (1)
src/NLog/Internal/ObjectReflectionCache.cs (1)
  • ObjectReflectionCache (56-59)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: task-list-completed
🔇 Additional comments (6)
src/NLog/Layouts/JSON/JsonLayout.cs (6)

63-64: LGTM!

The lazy initialization of ObjectReflectionCache follows the existing pattern for _jsonConverter and _valueFormatter, and cleanup in CloseLayout is correctly added.

Also applies to: 291-291


157-162: LGTM!

The DottedRecursion property is well-documented and follows the existing property pattern. The default false preserves backward compatibility.


376-383: LGTM!

The conditional dispatch cleanly separates the flattening logic from the existing property rendering path, preserving backward compatibility when DottedRecursion is false.


447-450: LGTM!

The refactoring to delegate to AppendPropertyValueInternal eliminates code duplication and improves maintainability.


469-502: LGTM!

The extracted AppendPropertyValueInternal method correctly handles the three capture types (Serialize, Stringify, Normal) with appropriate serialization, formatting, and escaping logic.


544-544: LGTM!

The use of string.Concat on line 544 avoids unnecessary allocations, and the length-check pattern (lines 545-550) correctly preserves beginJsonMessage semantics when a child emits no output (e.g., filtered by ExcludeEmptyProperties).

Based on learnings.

Also applies to: 545-550

Comment thread src/NLog/Layouts/JSON/JsonLayout.cs Outdated
return;
}

if (propertyValue is Dictionary<int, int> dict)
Copy link
Copy Markdown
Contributor

@snakefoot snakefoot Nov 16, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure I like this approach. Then one have to recognize all Dictionary-types / List-types.

Maybe the value returned here:

var objectPropertyList = ObjectReflectionCache.LookupObjectProperties(propertyValue);

Could get a property that could signal that it was an IEnumerable, but not an expando-object (Ex. Dictionary<int, int> or List<int> etc.). And for that case, then "just" serialize as Json-collection without using dotted-recursion.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@snakefoot Since there is currently no logic in ObjectReflectionCache that does this, we would need to create a new property (can be called ShouldSerializeDirectly) that does this.

  • The new property would explicitly limit to List and Dictionary<int,int> since these are the only ones one which need to be serialised instead of flattened.
  • All the other IEnumerables will be flattened as expected (can check current testcases for the output of these)

How does that sound?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ObjectReflectionCache already recognize something that it calls expando-objects (Ex. Dictionary<string, T>). I was thinking it should be like this:

  • If expando-object or object is not IEnumerable - Dotted recursion
  • Else simple-objects (string / bool / etc.) or IEnumerable - Standard serialization.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@snakefoot yup, got the idea! Its all fixed now and serialises simple objects and Dictionary<int,int> instead of dotted recursion.

Changed how dotted recursion handles expando and enumberable objects
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

♻️ Duplicate comments (2)
src/NLog/Layouts/JSON/JsonLayout.cs (2)

452-467: Normalize MaxRecursionLimit to prevent underflow and ensure consistent behavior.

The past review comment about normalizing the recursion limit remains unaddressed. Two issues:

  1. Integer underflow risk: If MaxRecursionLimit is very negative (e.g., close to Int32.MinValue), the expression MaxRecursionLimit - 10 can underflow.
  2. Inconsistent normalization: Elsewhere in the codebase (line 76), MaxRecursionLimit is clamped with Math.Max(0, maxRecursionLimit), but here it's used directly.

Apply the suggested normalization:

 private void AppendFlattenedPropertyValue(string propName, object? propertyValue, string? format, IFormatProvider? formatProvider, MessageTemplates.CaptureType captureType, StringBuilder sb, bool beginJsonMessage)
 {
     if (captureType == MessageTemplates.CaptureType.Stringify)
     {
         AppendPropertyValueInternal(propName, propertyValue, format, formatProvider, captureType, sb, beginJsonMessage);
     }
     else
     {
+        int effectiveMaxDepth = Math.Max(0, MaxRecursionLimit);
         // Allow flattening also for Serialize, by starting at a negative depth to effectively loosen depth bound
         int startDepth = captureType == MessageTemplates.CaptureType.Serialize
-            ? Math.Min(0, MaxRecursionLimit - 10)
+            ? Math.Min(0, effectiveMaxDepth - 10)
             : 0;
 
         FlattenObjectProperties(propName, propertyValue, sb, beginJsonMessage, startDepth);
     }
 }

Also apply the same normalization in FlattenObjectProperties (see next comment).

Based on unresolved past review feedback.


504-510: Apply normalization to MaxRecursionLimit in depth check.

Continuing from the previous comment, the recursion depth check also needs normalization:

 private void FlattenObjectProperties(string basePropertyName, object? propertyValue, StringBuilder sb, bool beginJsonMessage, int depth = 0)
 {
-    if (depth >= MaxRecursionLimit)
+    int effectiveMaxDepth = Math.Max(0, MaxRecursionLimit);
+    if (depth >= effectiveMaxDepth)
     {
         AppendJsonPropertyValue(basePropertyName, propertyValue, sb, beginJsonMessage);
         return;
     }

This ensures consistent behavior when MaxRecursionLimit is negative and prevents the method from bailing out immediately.

Based on unresolved past review feedback.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 466d3d4 and 9e9fa80.

📒 Files selected for processing (2)
  • src/NLog/Layouts/JSON/JsonLayout.cs (7 hunks)
  • tests/NLog.UnitTests/Layouts/JsonLayoutTests.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/NLog.UnitTests/Layouts/JsonLayoutTests.cs
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-12T01:22:52.437Z
Learnt from: thatrajeevkr
Repo: NLog/NLog PR: 5999
File: src/NLog/Layouts/JSON/JsonLayout.cs:162-166
Timestamp: 2025-09-12T01:22:52.437Z
Learning: When suggesting StringBuilder optimizations for NLog layouts marked as [ThreadAgnostic], avoid shared StringBuilder fields as they create thread safety issues. Prefer simple string operations or ThreadLocal<StringBuilder> if allocation profiling shows a real performance impact.

Applied to files:

  • src/NLog/Layouts/JSON/JsonLayout.cs
🧬 Code graph analysis (1)
src/NLog/Layouts/JSON/JsonLayout.cs (1)
src/NLog/Internal/ObjectReflectionCache.cs (2)
  • ObjectReflectionCache (56-59)
  • TryLookupExpandoObject (133-174)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: task-list-completed
🔇 Additional comments (7)
src/NLog/Layouts/JSON/JsonLayout.cs (7)

63-64: LGTM! Consistent lazy initialization pattern.

The ObjectReflectionCache initialization follows the same lazy pattern as the existing _jsonConverter and _valueFormatter, and is properly cleaned up in CloseLayout.


157-162: LGTM! Well-documented property.

The DottedRecursion property is properly documented with XML comments and has a sensible default value of false to maintain backward compatibility.


291-291: LGTM! Proper resource cleanup.

The cleanup of _objectReflectionCache in CloseLayout correctly prevents reference retention after layout disposal, matching the pattern for other lazy-initialized fields.


376-383: LGTM! Clear conditional routing.

The branching logic correctly routes to AppendFlattenedPropertyValue when DottedRecursion is enabled and preserves existing behavior otherwise.


447-450: LGTM! Reduces code duplication.

The refactored AppendJsonPropertyValue now delegates to the shared AppendPropertyValueInternal method, eliminating duplication as addressed in previous reviews.


469-502: LGTM! Proper handling of capture types.

AppendPropertyValueInternal correctly handles all three capture types:

  • Serialize with low recursion limit bypasses it safely
  • Stringify properly escapes the formatted value
  • Normal delegates to standard serialization

521-547: LGTM! Robust flattening logic with proper safeguards.

The flattening logic correctly handles:

  • IEnumerable types that aren't expando objects (serializes directly instead of flattening)
  • Simple values and null checks
  • Thread-safe dotted name construction using string.Concat (line 539)
  • Proper isFirstChild tracking with length checks (lines 540-545) to avoid corrupted JSON when children emit nothing

The logic aligns with the maintainer's guidance to serialize non-expando collections directly while flattening expando objects and POCOs.

Comment thread src/NLog/Layouts/JSON/JsonLayout.cs Outdated
removed unnecessary header
Copy link
Copy Markdown

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/NLog/Layouts/JSON/JsonLayout.cs (1)

451-466: Consider clarifying the Serialize depth adjustment logic.

The depth calculation at lines 460-461 intentionally allows deeper recursion for Serialize capture types, but the logic Math.Min(0, MaxRecursionLimit - 10) is subtle. When MaxRecursionLimit is small (e.g., 1), this allows up to 10 flattening levels by starting at a negative depth. A brief inline comment explaining the rationale would help future maintainers.

Example:

-                // Allow flattening also for Serialize, by starting at a negative depth to effectively loosen depth bound
+                // Allow flattening also for Serialize, by starting at a negative depth to effectively loosen depth bound.
+                // When MaxRecursionLimit < 10, we start at a negative depth to guarantee at least 10 levels of flattening,
+                // since message-template Serialize indicates the object is safe to traverse deeply.
                 int startDepth = captureType == MessageTemplates.CaptureType.Serialize
                     ? Math.Min(0, MaxRecursionLimit - 10)
                     : 0;
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 9e9fa80 and c52dfcd.

📒 Files selected for processing (1)
  • src/NLog/Layouts/JSON/JsonLayout.cs (7 hunks)
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-09-12T01:22:52.437Z
Learnt from: thatrajeevkr
Repo: NLog/NLog PR: 5999
File: src/NLog/Layouts/JSON/JsonLayout.cs:162-166
Timestamp: 2025-09-12T01:22:52.437Z
Learning: When suggesting StringBuilder optimizations for NLog layouts marked as [ThreadAgnostic], avoid shared StringBuilder fields as they create thread safety issues. Prefer simple string operations or ThreadLocal<StringBuilder> if allocation profiling shows a real performance impact.

Applied to files:

  • src/NLog/Layouts/JSON/JsonLayout.cs
🧬 Code graph analysis (1)
src/NLog/Layouts/JSON/JsonLayout.cs (1)
src/NLog/Internal/ObjectReflectionCache.cs (2)
  • ObjectReflectionCache (56-59)
  • TryLookupExpandoObject (133-174)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: task-list-completed
🔇 Additional comments (4)
src/NLog/Layouts/JSON/JsonLayout.cs (4)

62-63: LGTM: ObjectReflectionCache follows established patterns.

The lazy initialization pattern matches existing fields (_jsonConverter, _valueFormatter) and proper cleanup is present in CloseLayout(). The dependency injection via ResolveService is consistent with the class design.


156-161: LGTM: DottedRecursion property is well-documented and backward-compatible.

The property has clear XML documentation and defaults to false, ensuring existing behavior is preserved unless explicitly enabled.


375-382: LGTM: Conditional routing correctly implements feature flag.

The branching logic cleanly separates the flattened and standard rendering paths, with the default path preserving existing behavior when DottedRecursion is false.


503-546: LGTM: FlattenObjectProperties implements robust recursive flattening.

The method correctly handles:

  • Depth limiting (line 505): Falls back to standard serialization when MaxRecursionLimit is reached
  • Empty property exclusion (line 511): Honors ExcludeEmptyProperties
  • Simple values (lines 514-518): Directly serializes strings, nulls, and primitive types
  • Collection vs. expando distinction (line 520): IEnumerables (lists, non-string-keyed dictionaries) serialize normally while expando objects (Dictionary<string,T>) flatten into dotted keys
  • Allocation efficiency (line 538): Uses string.Concat to avoid string interpolation allocations, per learnings
  • Delimiter tracking (lines 539-544): Only sets isFirstChild = false when output was actually produced, preventing malformed JSON

The recursive flattening logic is sound and addresses all concerns raised in prior review rounds.

Based on learnings, using string.Concat instead of string interpolation or shared StringBuilder is the correct approach for this [ThreadAgnostic] layout.

val4 = 3.14,
val5 = new DateTime(2023, 1, 1, 0, 0, 0, DateTimeKind.Utc),
val6 = Guid.NewGuid()
}
Copy link
Copy Markdown
Contributor

@snakefoot snakefoot Nov 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add array of int as property? list = new List<int> { 1, 2, 3 }

Believe with your latest change then it will output the collection and not the list-Count.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@snakefoot yes it does, I will add that change to the testacase.

@snakefoot
Copy link
Copy Markdown
Contributor

Think with the latest change to handle IEnumerable, then the pull-request looks ready to me.

Do you have any more planned changes?

added testcase for int list
@thatrajeevkr
Copy link
Copy Markdown
Contributor Author

@snakefoot Yes thats all for this, after this I will start working on the other issue (#5990)

@sonarqubecloud
Copy link
Copy Markdown

Quality Gate Passed Quality Gate passed

Issues
0 New issues
1 Accepted issue

Measures
0 Security Hotspots
94.4% Coverage on New Code
0.0% Duplication on New Code

See analysis details on SonarQube Cloud

@snakefoot
Copy link
Copy Markdown
Contributor

snakefoot commented Nov 24, 2025

Really appreciates the work you have done, and also overcoming my bad advice about trying to move the logic into the NLog JsonConverter. Like that the code is short and simple by reusing existing building-blocks, and also thank you for the unit-tests.

Seems NLog v6.0.6 is stable, so I think next version will be NLog v6.1.0 where this feature will be included.

Sounds great with #5990. Maybe start with creating a pull-request against the NLog-repo, and when ready then it can becomes its git-repo NLog.Targets.HttpClient.

@thatrajeevkr
Copy link
Copy Markdown
Contributor Author

@snakefoot Sure! great to help.

@snakefoot snakefoot changed the title Feature update for dotted recursion (#5991) JsonLayout with support for dotted recursion (#5991) Jan 31, 2026
@snakefoot snakefoot merged commit cd61ceb into NLog:dev Jan 31, 2026
5 of 6 checks passed
@welcome
Copy link
Copy Markdown

welcome bot commented Jan 31, 2026

Hooray your first pull request is merged! Thanks for the contribution! Looking for more? 👼 Please check the up-for-grabs issues - thanks!
thanks!

@snakefoot
Copy link
Copy Markdown
Contributor

snakefoot commented Jan 31, 2026

Created #6084 to add protection against cyclic object graphs for DottedRecursion.

@snakefoot snakefoot added this to the 6.1.0 milestone Jan 31, 2026
@snakefoot
Copy link
Copy Markdown
Contributor

Thank you again for the nice contribution. NLog v6.1 has now been released:

@thatrajeevkr
Copy link
Copy Markdown
Contributor Author

thatrajeevkr commented Jan 31, 2026

@snakefoot Thanks :) and I will be pushing changes for the other issue as well.

@snakefoot snakefoot added the documentation done all docs done (wiki, api docs, lists on nlog-project.org, xmldocs) label Feb 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation done all docs done (wiki, api docs, lists on nlog-project.org, xmldocs) feature json / json-layout needs documentation on wiki size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

JsonLayout IncludeEventProperties with DottedRecursion = true

2 participants