Skip to content

Conversation

@snakefoot
Copy link
Contributor

@snakefoot snakefoot commented Aug 9, 2025

When having forced NLog to use its own LogMessageTemplateFormatter, then it should always use the NLog IValueFormatter.

@snakefoot snakefoot added the enhancement Improvement on existing feature label Aug 9, 2025
@coderabbitai
Copy link

coderabbitai bot commented Aug 9, 2025

Walkthrough

The changes update message template formatting logic and its configuration resolution in NLog. They refine how value formatting and quoting is handled, especially for string and DateTime values, and provide a mechanism to override value formatting via dependency injection. Unit tests are extended to verify custom DateTime formatting behavior.

Changes

Cohort / File(s) Change Summary
Configuration Parsing Behavior
src/NLog/Config/LoggingConfigurationParser.cs
Initialization of the parseMessageTemplates variable in SetNLogElementSettings now uses LogFactory.ServiceRepository.ResolveParseMessageTemplates() instead of null, improving configuration value resolution.
Message Template Formatting Logic
src/NLog/Internal/LogMessageTemplateFormatter.cs
Refactored RenderHolePositional to always use RenderHole with either Serialize or Stringify capture types, removing direct null checks and simplifying conditional logic for value formatting.
Value Formatting and Quoting
src/NLog/MessageTemplates/ValueFormatter.cs
Enhanced FormatValue and related methods for more precise quoting and formatting decisions, reordered type handling for serialization, and improved support for platform-specific formatting. Updated FormatToString to use pattern matching for string values.
Unit Test Enhancements
tests/NLog.UnitTests/MessageTemplates/RendererTests.cs
Added test RenderDateTimeOverride to verify custom DateTime formatting using an injectable IValueFormatter. Modified RenderAndTest to accept an optional LogFactory. Introduced OverrideValueFormatter for custom formatting logic in tests.
Test Code Reorganization
tests/NLog.UnitTests/MessageTemplates/ValueFormatterTest.cs
Moved nested test data types and helper method CreateValueFormatter to the bottom of the class. Changed calls to FormatValue to pass null instead of empty string for format argument. No logic or assertions changed.

Sequence Diagram(s)

sequenceDiagram
    participant Test as Unit Test
    participant LogFactory
    participant OverrideValueFormatter
    participant LogMessageTemplateFormatter

    Test->>LogFactory: Create instance
    Test->>LogFactory: Register OverrideValueFormatter
    Test->>LogMessageTemplateFormatter: Create with LogFactory
    Test->>LogMessageTemplateFormatter: Format message with DateTime arg
    LogMessageTemplateFormatter->>OverrideValueFormatter: FormatValue(DateTime)
    OverrideValueFormatter-->>LogMessageTemplateFormatter: Formatted string (custom format)
    LogMessageTemplateFormatter-->>Test: Formatted message
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~18 minutes

Poem

In the warren where log lines flow,
A bunny tweaks how values show—
Dates now glisten, strings quote right,
And tests ensure the output’s bright.
With custom formatters in the mix,
Our logging’s learned a clever trick!
🐇✨


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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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 (5)
src/NLog/MessageTemplates/ValueFormatter.cs (1)

121-126: Stringify quoting semantics changed; confirm desired behavior for positional vs named holes

Using includeQuotes = (format is null || _legacyStringQuotes) means:

  • Named holes with Stringify (format == null) will be quoted by default.
  • Positional holes now pass empty string (not null) from LogMessageTemplateFormatter, so they won’t be quoted unless legacy flag is on.

If you want ":l" to also suppress quotes for Stringify, consider this tweak.

- bool includeQuotes = format is null || _legacyStringQuotes;
+ bool includeQuotes = (format is null || _legacyStringQuotes)
+                      && !string.Equals(format, LiteralFormatSymbol, StringComparison.Ordinal);

Please verify behavior for:

  • "{0}" with a string (no quotes), "{$0}" with a string (quotes), and "{$0:l}" (no quotes).
src/NLog/Internal/LogMessageTemplateFormatter.cs (1)

282-289: Positional holes now flow through IValueFormatter (Serialize/Stringify); subtle quoting shift

Pros:

  • Unifies formatting pipeline and enables IValueFormatter overrides (as tested).
  • Consistent handling across positional/named holes.

Gotchas:

  • Normal capture for positional holes now uses Stringify, which may quote strings if legacy flag is on (differs from old string.Format semantics). This seems intentional; please confirm legacy scenarios remain acceptable.

Consider adding a comment here explaining the empty format for Stringify is intentional to avoid quotes for positional placeholders.

tests/NLog.UnitTests/MessageTemplates/RendererTests.cs (3)

109-124: Great targeted test for overriding IValueFormatter (DateTime default format)

Solid validation of the new pipeline. Consider also testing:

  • Named hole "{time}" (non-positional) to confirm override applies there too.
  • "{$0}" and "{$time}" to check Stringify + override interplay and quoting.

I can add these test cases if you want.


148-154: RenderAndTest overload injection is clean

Optional: use a shared LogFactory across related tests to reduce overhead if many future tests will inject services.


156-178: OverrideValueFormatter implementation is correct and minimal

Minor nits (optional):

  • Accept nullable parameters to mirror IValueFormatter nullability.
  • Consider restricting the override to CaptureType.Stringify/Normal if Serialize should bypass (policy-dependent).
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7ba11a3 and ba537e7.

📒 Files selected for processing (4)
  • src/NLog/Config/LoggingConfigurationParser.cs (1 hunks)
  • src/NLog/Internal/LogMessageTemplateFormatter.cs (1 hunks)
  • src/NLog/MessageTemplates/ValueFormatter.cs (3 hunks)
  • tests/NLog.UnitTests/MessageTemplates/RendererTests.cs (3 hunks)
🔇 Additional comments (4)
src/NLog/MessageTemplates/ValueFormatter.cs (3)

171-201: Reordering to prefer IConvertible over IFormattable looks good; watch edge-cases

Benefits:

  • Avoids treating strings as IEnumerable.
  • Keeps format support via SerializeConvertibleObject when provided.

Edge-cases to validate (culture + format interactions):

  • DateTime/DateTimeOffset with and without explicit format.
  • Decimal/Double with custom numeric formats and cultures.

262-270: AppendFormat for ISpanFormattable is a solid win

Good optimization for .NET Core+ without changing semantics. No concerns.


380-394: FormatToString cleanup (pattern matching + AppendFormat path) LGTM

Clearer code; preserves formatting semantics and ISpanFormattable support.

src/NLog/Config/LoggingConfigurationParser.cs (1)

131-131: Defaulting parseMessageTemplates from ServiceRepository improves consistency

Good move to resolve a default before parsing config. Please verify precedence is unchanged when config explicitly sets PARSEMESSAGETEMPLATES (should override the resolved default).

Copy link

@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

🔭 Outside diff range comments (1)
src/NLog/Internal/LogMessageTemplateFormatter.cs (1)

299-306: Delegate null handling in RenderHole to ValueFormatter for consistency

ValueFormatter.FormatValue already handles null according to CaptureType (JSON null, stringify "", default "NULL") and will pick up any custom IValueFormatter. Remove the inline null check so all cases go through the same logic.

• File: src/NLog/Internal/LogMessageTemplateFormatter.cs
Method: RenderHole(StringBuilder sb, CaptureType captureType, string? holeFormat, IFormatProvider? formatProvider, object? value) (lines ~299–306)

Suggested diff:

-        if (value is null)
-        {
-            sb.Append("NULL");
-        }
-        else
-        {
-            ValueFormatter.FormatValue(value, holeFormat, captureType, formatProvider, sb);
-        }
+        // Delegate null handling and all formatting to ValueFormatter.FormatValue
+        ValueFormatter.FormatValue(value, holeFormat, captureType, formatProvider, sb);

Please also add or update unit tests to verify:

  • CaptureType.Serialize with nullnull
  • default capture with null"NULL"
  • CaptureType.Stringify with null""
  • custom IValueFormatter overrides null formatting as expected
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba537e7 and c976a32.

📒 Files selected for processing (5)
  • src/NLog/Config/LoggingConfigurationParser.cs (1 hunks)
  • src/NLog/Internal/LogMessageTemplateFormatter.cs (1 hunks)
  • src/NLog/MessageTemplates/ValueFormatter.cs (3 hunks)
  • tests/NLog.UnitTests/MessageTemplates/RendererTests.cs (3 hunks)
  • tests/NLog.UnitTests/MessageTemplates/ValueFormatterTest.cs (20 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/NLog/Config/LoggingConfigurationParser.cs
  • tests/NLog.UnitTests/MessageTemplates/RendererTests.cs
  • src/NLog/MessageTemplates/ValueFormatter.cs
🔇 Additional comments (2)
tests/NLog.UnitTests/MessageTemplates/ValueFormatterTest.cs (2)

263-267: Factory helper uses legacyStringQuotes: false — LGTM

Matches the new quoting/null-handling expectations in tests.


50-52: No remaining string.Empty usages in FormatValue calls

Verified that all FormatValue calls consistently use null for the format parameter; no instances of string.Empty remain. LGTM.

Comment on lines +282 to 289
if (hole.CaptureType == CaptureType.Serialize)
{
MessageTemplates.ValueFormatter.FormatToString(value, hole.Format, formatProvider, sb);
RenderHole(sb, CaptureType.Serialize, hole.Format, formatProvider, value);
}
else
{
RenderHole(sb, hole.CaptureType, hole.Format, formatProvider, value);
RenderHole(sb, CaptureType.Stringify, hole.Format ?? string.Empty, formatProvider, value);
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

Don’t force CaptureType.Stringify for positional holes; preserve the hole’s capture type and format

Forcing Stringify changes behavior (e.g., quotes strings) and diverges from the hole’s intended capture semantics. It also conflicts with the tests’ shift to treat null vs empty format distinctly.

Refactor to delegate directly to RenderHole(hole, ...) and stop coalescing format to empty string:

-        private void RenderHolePositional(StringBuilder sb, in Hole hole, IFormatProvider? formatProvider, object? value)
-        {
-            if (hole.CaptureType == CaptureType.Serialize)
-            {
-                RenderHole(sb, CaptureType.Serialize, hole.Format, formatProvider, value);
-            }
-            else
-            {
-                RenderHole(sb, CaptureType.Stringify, hole.Format ?? string.Empty, formatProvider, value);
-            }
-        }
+        private void RenderHolePositional(StringBuilder sb, in Hole hole, IFormatProvider? formatProvider, object? value)
+        {
+            // Preserve capture semantics and pass through original format (including null)
+            RenderHole(sb, hole, formatProvider, value);
+        }

This avoids a silent behavior change for positional templates. If forcing Stringify was intentional, please add/adjust tests to assert quoted string behavior for positional holes under the forced renderer.


🏁 Script executed:

#!/bin/bash
# List all occurrences of RenderHole in the formatter to inspect available overloads
grep -R "RenderHole(" -n src/NLog/Internal/LogMessageTemplateFormatter.cs

Length of output: 788


#!/bin/bash
# Display the RenderHolePositional method and related overloads for context
sed -n '250,330p' src/NLog/Internal/LogMessageTemplateFormatter.cs

#!/bin/bash
# Show the RenderHolePositional method for full context
sed -n '260,300p' src/NLog/Internal/LogMessageTemplateFormatter.cs

Preserve original CaptureType and Format in RenderHolePositional

In src/NLog/Internal/LogMessageTemplateFormatter.cs, the RenderHolePositional overload currently forces all non-Serialize holes to use CaptureType.Stringify and coalesces a null format to string.Empty, which:

  • Overrides any other capture semantics (e.g., if new CaptureType values are introduced in the future)
  • Conflates a deliberately blank format ("") with a missing one (null), against the intent of recent tests

Refactor RenderHolePositional to call the existing RenderHole(StringBuilder, in Hole, IFormatProvider?, object?) overload, preserving both the hole’s CaptureType and its raw Format (including null):

--- a/src/NLog/Internal/LogMessageTemplateFormatter.cs
+++ b/src/NLog/Internal/LogMessageTemplateFormatter.cs
@@ -279,10 +279,7 @@ namespace NLog.Internal
         private void RenderHolePositional(StringBuilder sb, in Hole hole, IFormatProvider? formatProvider, object? value)
         {
-            if (hole.CaptureType == CaptureType.Serialize)
-            {
-                RenderHole(sb, CaptureType.Serialize, hole.Format, formatProvider, value);
-            }
-            else
-            {
-                RenderHole(sb, CaptureType.Stringify, hole.Format ?? string.Empty, formatProvider, value);
-            }
+            // Preserve the hole’s original capture semantics and format (including null)
+            RenderHole(sb, hole, formatProvider, value);
         }

If the forced Stringify behavior was intentional for positional placeholders, please document this in a comment and add tests to explicitly assert quoted output under those conditions.

🤖 Prompt for AI Agents
In src/NLog/Internal/LogMessageTemplateFormatter.cs around lines 282 to 289, the
RenderHolePositional method currently overrides the hole's CaptureType to
Stringify for non-Serialize cases and replaces null Format with string.Empty,
which loses the original CaptureType and conflates null and empty formats. To
fix this, refactor RenderHolePositional to call the existing RenderHole overload
that accepts the Hole struct directly, passing the hole as is to preserve its
original CaptureType and raw Format (including null). If the current forced
Stringify behavior is intentional, add a comment explaining this and include
tests to verify the expected output.

@sonarqubecloud
Copy link

sonarqubecloud bot commented Aug 9, 2025

Copy link

@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 (5)
src/NLog/Internal/LogMessageTemplateFormatter.cs (3)

288-289: Confirm intent of using empty string for hole.Format in stringify path

You pass hole.Format ?? string.Empty for Stringify, but pass hole.Format as-is for Serialize. If ValueFormatter distinguishes null vs empty, this could diverge from named-hole behavior (where null is forwarded). Please confirm this is intentional to preserve string.Format-like behavior (e.g., no quotes for strings) for positional holes, and consider adding tests to lock this in:

  • "{0}" with string/date/time/enum (no format) should match string.Format output.
  • "{0:O}" and other formats for DateTime/decimal should honor formatProvider.

297-307: Ensure NULL rendering remains consistent with the rest of the pipeline

RenderHole appends "NULL" for nulls. Since positional holes now route through this method, null output is unified. Verify that this casing/text matches expectations across all sinks/formatters. If there’s a central setting or constant for null text in ValueFormatter, consider using it for consistency.


321-325: Typo: holeFormatVaue -> holeFormatValue

Minor readability fix.

-                    string holeFormatVaue = sb.ToString(holeStartPosition, holeWidth);
+                    string holeFormatValue = sb.ToString(holeStartPosition, holeWidth);
-                    sb.Append(holeFormatVaue);
+                    sb.Append(holeFormatValue);
tests/NLog.UnitTests/MessageTemplates/ValueFormatterTest.cs (2)

137-140: Strengthen assertions to robustly detect unintended flattening.

Current NotEqual checks could pass even if output is a flat list with different spacing. Prefer a regex-based assertion that rejects a plain comma-separated list of numbers.

-            Assert.NotEqual("1,2,3,4,5,6,7", builder.ToString());
+            Assert.DoesNotMatch(@"^\s*\d+(,\s*\d+)*\s*$", builder.ToString());
-            Assert.NotEqual("1,2", builder.ToString());
+            Assert.DoesNotMatch(@"^\s*\d+(,\s*\d+)*\s*$", builder.ToString());

Also applies to: 147-150


86-99: Consider adding a positive test for non-null format strings.

Since callers now pass null when no format is intended, add a dedicated test that passes a non-null format (e.g., numeric "D4" or DateTime "yyyy-MM-dd") to validate that explicit formats are still honored.

Example you could add:

[Fact]
public void FormatValue_AppliesExplicitFormatString()
{
    var value = 12;
    var builder = new StringBuilder();
    var ok = CreateValueFormatter().FormatValue(value, "D4", CaptureType.Normal, CultureInfo.InvariantCulture, builder);
    Assert.True(ok);
    Assert.Equal("0012", builder.ToString());
}

Happy to provide a PR-ready patch if helpful.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between ba537e7 and c976a32.

📒 Files selected for processing (5)
  • src/NLog/Config/LoggingConfigurationParser.cs (1 hunks)
  • src/NLog/Internal/LogMessageTemplateFormatter.cs (1 hunks)
  • src/NLog/MessageTemplates/ValueFormatter.cs (3 hunks)
  • tests/NLog.UnitTests/MessageTemplates/RendererTests.cs (3 hunks)
  • tests/NLog.UnitTests/MessageTemplates/ValueFormatterTest.cs (20 hunks)
🚧 Files skipped from review as they are similar to previous changes (3)
  • src/NLog/Config/LoggingConfigurationParser.cs
  • tests/NLog.UnitTests/MessageTemplates/RendererTests.cs
  • src/NLog/MessageTemplates/ValueFormatter.cs
🔇 Additional comments (3)
src/NLog/Internal/LogMessageTemplateFormatter.cs (1)

280-290: Aligning positional holes with IValueFormatter looks good

Switching to RenderHole with CaptureType.Serialize/Stringify centralizes null-handling and unifies formatting for positional holes with named holes. This matches the PR goal and simplifies logic.

tests/NLog.UnitTests/MessageTemplates/ValueFormatterTest.cs (2)

263-267: Relocating helper types and factory method improves readability.

Moving CreateValueFormatter and the test helper types to the bottom keeps the test cases front-and-center. No functional change; nested type full names remain stable for assertions (e.g., RecursiveTest).

Also applies to: 268-382, 384-418


50-50: All string.Empty usages in FormatValue calls have been removed
No occurrences of string.Empty as the format parameter remain—our search through the codebase returned zero matches. The updated invocations correctly use null per the new ValueFormatter semantics. Changes are approved.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement Improvement on existing feature size/L

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant