Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings
/NLogPublic

PropertiesDictionary - Initial 3 properties without dictionary#6046

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to ourterms of service andprivacy statement. We’ll occasionally send you account related emails.

Already on GitHub?Sign in to your account

Open
snakefoot wants to merge2 commits intoNLog:dev
base:dev
Choose a base branch
Loading
fromsnakefoot:property_dictionary

Conversation

@snakefoot
Copy link
Contributor

@snakefootsnakefoot commentedNov 22, 2025
edited
Loading

Not sure I like the increased complexity to reduce memory-allocation for LogEvents with 3 properties or less.

But still curious of the performance gain, by not doing dictionary allocation for LogEvents with just EventId / EventName (When not coming from NLog.Extensions.Logging that already performs this optimization).

@coderabbitai
Copy link

coderabbitaibot commentedNov 22, 2025
edited
Loading

Walkthrough

Replace IList-backed message-template parameters with array-backed MessageTemplateParameter[] and explicit counts across formatter, properties dictionary, log event, and logger; update signatures and length checks, refactor mutation/enumeration paths, add benchmarks, and introduce a duplicated positional-template unit test.

Changes

Cohort / File(s)Summary
Template Formatter Updates
src/NLog/Internal/LogMessageTemplateFormatter.cs
Render overloads andVerifyMessageTemplateParameters now useMessageTemplateParameter[]?; internal handling switched from.Count to.Length; array truncation and parameter initialization adapted; builder rewind usessb.ClearBuilder() on supported targets.
Properties Dictionary Refactoring
src/NLog/Internal/PropertiesDictionary.cs
_messageProperties converted toMessageTemplateParameter[]? with new_messagePropertiesCount; constructor andResetMessageProperties accept array+count;MessageProperties exposes an array; indexer, add/clear, enumeration, lookup, and enumerator rewritten to use array + explicit count; addedAddOrUpdateMessageProperties andSkipDictionaryAllocation fast-paths.
LogEvent Adjustments
src/NLog/LogEventInfo.cs
Internal methods now acceptMessageTemplateParameter[]?; checks and loops use.Length; resets and property-initialization flows updated to pass array and explicit counts; MessageTemplateParameters/immutability checks updated.
Logger Minor Update
src/NLog/Logger.cs
Local template-parameter variable types inRenderPreformattedLogEvent changed toMessageTemplates.MessageTemplateParameter[]?; downstream handling updated accordingly.
Benchmarks Added
tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs
New benchmark suite exercisingPropertiesDictionary creation, lookup (found/not-found), and enumeration for 1–5 properties and 1000-iteration lookup loops using BenchmarkDotNet.
Unit Tests Added (duplicate)
tests/NLog.UnitTests/LogMessageFormatterTests.cs
New testMessageTemplatePositional added twice in the diff; validates positional template parsing and captured parameters for"{0} {1} {2}".

Sequence Diagram(s)

sequenceDiagram  participant Renderer as LogMessageTemplateFormatter  participant Logger as Logger  participant Event as LogEventInfo / PropertiesDictionary  Note over Renderer,Logger: Render now returns array-backed MessageTemplateParameter[]? via out param  Logger->>Renderer: Render(template, formatProvider, params, sb, out parametersArray)  alt parametersArray != null    Renderer-->>Logger: formattedMessage + parametersArray    Logger->>Event: CreatePropertiesInternal(parametersArray, parametersArray.Length)    Event-->>Logger: PropertiesDictionary (array + _messagePropertiesCount)  else parametersArray == null    Renderer-->>Logger: formattedMessage + null    Logger->>Event: fallback: set Parameters or skip property creation  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

  • Pay special attention to:
    • src/NLog/Internal/PropertiesDictionary.cs — array/count invariants, in-place mutation fast-path, enumerator boundary checks, and indexer mutation paths.
    • src/NLog/Internal/LogMessageTemplateFormatter.cs — parameter array truncation/initialization and conditional use ofsb.ClearBuilder().
    • Call sites insrc/NLog/LogEventInfo.cs andsrc/NLog/Logger.cs — correct propagation of arrays and counts.
    • tests/NLog.UnitTests/LogMessageFormatterTests.cs — duplicateMessageTemplatePositional test likely unintended.
    • New benchmarks file for build integration and BenchmarkDotNet attributes.

Poem

🐰 I swapped my lists for tidy rows,
Holes counted neat where the moonlight shows.
Arrays snug in tunnels, counts held tight,
I hop through code from byte to byte.
Benchmarks hum — carrots in sight.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning)
Check nameStatusExplanationResolution
Docstring Coverage⚠️ WarningDocstring coverage is 18.60% which is insufficient. The required threshold is 80.00%.You can run@coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (2 passed)
Check nameStatusExplanation
Title check✅ PassedThe title accurately describes the main change: optimizing PropertiesDictionary to avoid dictionary allocation for 3 or fewer properties.
Description check✅ PassedThe description is related to the changeset, expressing uncertainty about complexity trade-offs and curiosity about performance gains for small property counts.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Tip

📝 Customizable high-level summaries are now available in beta!

You can now customize how CodeRabbit generates thehigh-level summary in your pull requests — including its content, structure, tone, and formatting.

  • Provide your own instructions using thehigh_level_summary_instructions setting.
  • Format the summary however you like (bullet lists, tables, multi-section layouts, contributor stats, etc.).
  • Usehigh_level_summary_in_walkthrough to move the summary from the description to the walkthrough section.

Example instruction:

"Divide the high-level summary into five sections:

  1. 📝 Description — Summarize the main change in 50–60 words, explaining what was done.
  2. 📓 References — List relevant issues, discussions, documentation, or related PRs.
  3. 📦 Dependencies & Requirements — Mention any new/updated dependencies, environment variable changes, or configuration updates.
  4. 📊 Contributor Summary — Include a Markdown table showing contributions:
    | Contributor | Lines Added | Lines Removed | Files Changed |
  5. ✔️ Additional Notes — Add any extra reviewer context.
    Keep each section concise (under 200 words) and use bullet or numbered lists for clarity."

Note: This feature is currently in beta for Pro-tier users, and pricing will be announced later.


Thanks for usingCodeRabbit! 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.

Copy link

@coderabbitaicoderabbitaibot 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 between1b90d2a and54f7c93.

📒 Files selected for processing (5)
  • src/NLog/Internal/LogMessageTemplateFormatter.cs (3 hunks)
  • src/NLog/Internal/PropertiesDictionary.cs (18 hunks)
  • src/NLog/LogEventInfo.cs (7 hunks)
  • src/NLog/Logger.cs (1 hunks)
  • src/NLog/MessageTemplates/MessageTemplateParameters.cs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/NLog/LogEventInfo.cs (3)
src/NLog/Internal/PropertiesDictionary.cs (3)
  • PropertiesDictionary (89-95)
  • PropertiesDictionary (100-106)
  • ResetMessageProperties (129-156)
src/NLog/MessageTemplates/MessageTemplateParameter.cs (2)
  • MessageTemplateParameter (105-111)
  • MessageTemplateParameter (120-126)
src/NLog/MessageTemplates/MessageTemplateParameters.cs (3)
  • MessageTemplateParameters (45-198)
  • MessageTemplateParameters (81-94)
  • MessageTemplateParameters (99-103)
src/NLog/Logger.cs (2)
src/NLog/Internal/LogMessageTemplateFormatter.cs (1)
  • MessageTemplateParameter (267-278)
src/NLog/MessageTemplates/MessageTemplateParameter.cs (2)
  • MessageTemplateParameter (105-111)
  • MessageTemplateParameter (120-126)
src/NLog/MessageTemplates/MessageTemplateParameters.cs (4)
src/NLog/Internal/PropertiesDictionary.cs (3)
  • IList (517-528)
  • IList (543-546)
  • IList (548-551)
src/NLog/Internal/LogMessageTemplateFormatter.cs (1)
  • MessageTemplateParameter (267-278)
src/NLog/MessageTemplates/MessageTemplateParameter.cs (2)
  • MessageTemplateParameter (105-111)
  • MessageTemplateParameter (120-126)
src/NLog/Internal/ArrayHelper.cs (1)
  • ArrayHelper (36-53)
src/NLog/Internal/PropertiesDictionary.cs (3)
src/NLog/LogEventInfo.cs (2)
  • PropertiesDictionary (414-429)
  • PropertiesDictionary (431-445)
src/NLog/MessageTemplates/MessageTemplateParameter.cs (2)
  • MessageTemplateParameter (105-111)
  • MessageTemplateParameter (120-126)
src/NLog/Internal/ArrayHelper.cs (1)
  • ArrayHelper (36-53)
src/NLog/Internal/LogMessageTemplateFormatter.cs (2)
src/NLog/MessageTemplates/MessageTemplateParameter.cs (2)
  • MessageTemplateParameter (105-111)
  • MessageTemplateParameter (120-126)
src/NLog/MessageTemplates/TemplateEnumerator.cs (1)
  • TemplateEnumerator (65-72)

Copy link

@coderabbitaicoderabbitaibot 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/PropertiesDictionary.cs (4)

82-84:Clarify invariants around_messageProperties /_messagePropertiesCount andMessageProperties exposure

The new array+count representation andResetMessageProperties pipeline look consistent, but there are a couple of subtle invariants that are implicit:

  • MessageProperties only returns the backing array whenLength == _messagePropertiesCount; when using the 3-slot optimization (new MessageTemplateParameter[3]) with 1–2 entries, callers see an empty array even thoughCount > 0. This changes observable behavior for any code relying onMessageProperties to reflectall message-derived properties (e.g.,LogEventInfo.MessageTemplateParameters), and forces a fallback re-parse via_message/_parameters. If that’s intentional, it might be worth a short comment here to document thatMessageProperties only exposes “tight” arrays. Otherwise, consider returning a trimmed copy or anArraySegment-style view to avoid silently hiding the small inline set.
  • ResetMessageProperties assumesnewMessagePropertiesCount is ≤newMessageProperties?.Length and matches the populated prefix; today all call sites appear to passLength, but adding aDebug.Assert would make the expectation explicit and guard future changes.

Given the complexity here, making those invariants explicit will help prevent subtle regressions later.

Also applies to: 89-96, 108-109, 127-156, 169-183


201-232:Skip-dictionary fast-path for small property sets looks correct; consider tiny guardrails

Thethis[object key] setter,Add(object key, object? value), andSkipDictionaryAllocation optimization all look logically consistent:

  • They only use the inline array when_eventProperties is null and there is capacity (_messagePropertiesCount < _messageProperties.Length).
  • They correctly update existing entries in-place and append new ones withCaptureType.Unknown.
  • Bounds are guarded via theSkipDictionaryAllocation check before indexing intomessageProperties[_messagePropertiesCount++].

To make this easier to maintain, you might:

  • Add a quickDebug.Assert(_messagePropertiesCount <= (_messageProperties?.Length ?? 0)); inSkipDictionaryAllocation or after writes, to lock in the intended invariant.
  • Consider a brief comment on whyCaptureType.Unknown is used here (distinguishing ad-hoc event properties from template-derived ones and tying into theresetMessageProperties logic).

Functionality-wise this path looks good.

Also applies to: 237-257, 268-271


372-413:Lookup and uniqueness heuristics for message properties

TryLookupMessagePropertyValue andVerifyUniqueMessageTemplateParametersFast are well-shaped for the new array+count model:

  • The> 10 threshold for switching to dictionary-based lookup when only_messageProperties exist gives an O(1) path for large templates without penalizing the small-inline case.
  • The uniqueness check now operates directly on the array length and only does O(n²) work for<= 10 parameters.

Given these heuristics are core to the “no dictionary for ≤ 3” optimization, it would be good to:

  • Add a short comment on the choice of the “10” threshold (same value appears in both methods) so future tweaks don’t accidentally diverge.
  • Ensure there are tests around boundary counts (10 vs. 11) to confirm we don’t regress on either performance or behavior when arrays flip from linear scan to dictionary-backed lookup.

No correctness issues spotted here.

Also applies to: 420-443


720-746:HasImmutableProperties:MessageProperties fast-path now only kicks in for “tight” arrays

The new implementation ofHasImmutableProperties:

  • Usesproperties.MessageProperties (array) and only takes the fast path whenproperties.Count == messageProperties.Length, which effectively means “all properties come from a tight message-properties array”.
  • Falls back to thePropertyDictionaryEnumerator otherwise.

This avoids walking trailing capacity from the small inline buffer, but it also means the fast path won’t be used for the ≤3-properties optimization (where_messageProperties.Length == 3 and_messagePropertiesCount is 1–2). That’s a small perf regression in exchange for correctness and simplicity, which is probably fine.

If you care about squeezing that last bit of performance out of the ≤3 case, you could:

  • Expose aMessagePropertiesSpan-like view over the first_messagePropertiesCount items and base the check on that, instead of hiding the partial array behind an empty return.

From a correctness standpoint, this change is safe.

src/NLog/LogEventInfo.cs (1)

464-481:MessageTemplateParameters: behavior with ≤3 inline properties will now reparse

TheMessageTemplateParameters getter now:

  • Prefers_properties.MessageProperties whenLength > 0, wrapping it in aMessageTemplateParameters instance.
  • Falls back to parsing_message and_parameters whenParameters is non-null.
  • ReturnsMessageTemplateParameters.Empty otherwise.

BecausePropertiesDictionary.MessageProperties only returns a non-empty array when the internal array is “tight” (Length == _messagePropertiesCount), the ≤3-inline optimization path (where a 3-element buffer may hold only 1–2 populated entries) will always fall into the fallback branch and reparse from_message/_parameters.

That’s functionally correct and keeps the public behavior consistent, but it does mean:

  • You don’t benefit from memoizing parsed parameters in the small-inline case; instead you pay the parse cost again whenMessageTemplateParameters is accessed.
  • For constructor-suppliedMessageTemplateParameter[] you get the fully memoized behavior as before (arrays are tight there).

If this is acceptable from a perf/complexity perspective, it’s fine as-is; otherwise, consider exposing a trimmed view fromPropertiesDictionary.MessageProperties so this getter can reuse the captured parameters in the small case too.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between54f7c93 andea6180a.

📒 Files selected for processing (3)
  • src/NLog/Internal/PropertiesDictionary.cs (16 hunks)
  • src/NLog/LogEventInfo.cs (7 hunks)
  • tests/NLog.UnitTests/LoggerTests.cs (4 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
tests/NLog.UnitTests/LoggerTests.cs (1)
tests/NLog.UnitTests/NLogTestBase.cs (2)
  • AssertDebugLastMessage (82-86)
  • AssertDebugLastMessage (88-91)
src/NLog/LogEventInfo.cs (4)
src/NLog/Internal/PropertiesDictionary.cs (3)
  • PropertiesDictionary (89-95)
  • PropertiesDictionary (100-106)
  • ResetMessageProperties (129-156)
src/NLog/Internal/LogMessageTemplateFormatter.cs (1)
  • MessageTemplateParameter (267-278)
src/NLog/MessageTemplates/MessageTemplateParameter.cs (2)
  • MessageTemplateParameter (105-111)
  • MessageTemplateParameter (120-126)
src/NLog/MessageTemplates/MessageTemplateParameters.cs (3)
  • MessageTemplateParameters (45-198)
  • MessageTemplateParameters (81-94)
  • MessageTemplateParameters (99-103)
src/NLog/Internal/PropertiesDictionary.cs (4)
src/NLog/Internal/LogMessageTemplateFormatter.cs (1)
  • MessageTemplateParameter (267-278)
src/NLog/MessageTemplates/MessageTemplateParameter.cs (2)
  • MessageTemplateParameter (105-111)
  • MessageTemplateParameter (120-126)
src/NLog/LogEventInfo.cs (2)
  • PropertiesDictionary (414-429)
  • PropertiesDictionary (431-445)
src/NLog/Internal/ArrayHelper.cs (1)
  • ArrayHelper (36-53)
⏰ 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/Internal/PropertiesDictionary.cs (2)

129-156:Double-checkResetMessageProperties +InsertMessagePropertiesIntoEmptyDictionary behavior with mixed event vs. message properties

The reset logic for message properties and theInsertMessagePropertiesIntoEmptyDictionary helper is subtle but looks sound overall (event properties preserved, old message properties removed based onIsMessageProperty, new ones inserted with optional renaming).

Two corner cases are worth re-verifying against tests:

  • When old message-template parameters all haveCaptureType.Unknown,BuildEventProperties will mark them as non-message properties (IsMessageProperty = false) and (viaresetMessageProperties) can cause_messageProperties to be cleared in theEventProperties getter. LaterResetMessageProperties(null, 0) willnot remove those entries, so they effectively become permanent event properties. That’s probably desired, but it’s a behavior change compared to “pure” template properties.
  • InInsertMessagePropertiesIntoEmptyDictionary, if a new message parameter collides with an existingevent property (whereIsMessageProperty == false), the catch block intentionally does nothing, so the message parameter is not added and not renamed, while the event property remains authoritative. That matches the “event properties override template properties” rule, but it relies onIsMessageProperty being correct for all existing entries.

Given how many tests inLoggerTests exercise property overriding, I’d just confirm these two flows are covered and behave as intended.

Also applies to: 158-167, 448-481


497-508:PropertyDictionaryEnumerator correctly respects_messagePropertiesCount andIsMessageProperty

The updatedPropertyDictionaryEnumerator fixes the earlier pattern of blindly iterating the full array:

  • It gates message-parameter enumeration using_dictionary._messagePropertiesCount, so it never walks uninitialized slots in the 3-element buffer.
  • When_eventProperties exists,MoveNextValidMessageParameter only yields entries whose dictionary value hasIsMessageProperty == true, andMoveNextValidEventProperty yields only non-message entries, preserving the “message properties first, then remaining event properties” contract.
  • TheReset implementation resets_messagePropertiesIndex based on the current_messagePropertiesCount, so reuse after aReset() is safe.

The only behavioral nuance is that if_eventProperties is createdafter an enumerator instance is constructed, that enumerator will never see the new dictionary entries (its_eventEnumerator was captured earlier). That’s acceptable for .NET enumerator semantics (modifying a collection during enumeration is undefined) but might be worth a one-line remark if you expect concurrent modification.

Otherwise, this resolves the past “trailing nulls from array enumerator” concern.

Also applies to: 516-517, 533-534, 550-552, 591-597, 602-603, 623-624

src/NLog/LogEventInfo.cs (3)

313-321:Message/Parameters setters + reset logic: behavior looks consistent; corner cases worth noting

The interplay between:

  • Message /Parameters setters,
  • ResetMessageTemplateParameters(),
  • andResetFormattedMessage(bool parseMessageTemplateParameters)

looks coherent:

  • When there are existing parsed message-template parameters (HasMessageTemplateParameters is true) you clear them via_properties.ResetMessageProperties() and indicate that parsing should happen again.
  • When properties exist but werenot provided as constructor input (MessageProperties.Length == 0 || first.CaptureType == Unknown), you allow parsing on the next change as well.
  • Constructor-supplied template parameters (non-Unknown capture types) are preserved whenMessage orParameters change unless you explicitly clear/reset them.

Two edge behaviors to re-check against tests:

  1. For events created with constructor-suppliedIList<MessageTemplateParameter> and later message changes, the current logic leaves the original template parameters in place (and won’t reparse). That seems intentional, but it’s a semantic guarantee now.
  2. For the ≤3-inline optimization,_properties.MessageProperties.Length will be 0, soResetMessageTemplateParameters returnstrue, allowing parsing when you later changeMessage/Parameters even though the internal inline array is non-empty. This is behaviorally correct but may cause some redundant parsing.

Given how many test cases exercise message/parameter mutations, this overall design looks okay, just slightly more subtle now.

Also applies to: 327-335, 818-829, 831-844


414-429:Array-basedTryCreatePropertiesInternal /CreatePropertiesInternal integration withPropertiesDictionary

The migration of these helpers toMessageTemplateParameter[]? seems correct:

  • New events with parsed template parameters go throughCreatePropertiesInternal(templateParameters) which constructsPropertiesDictionary(templateParameters) and letsPropertiesDictionary own_messageProperties and_messagePropertiesCount.
  • Subsequent passes from the formatter with updated parameters hit theelse if (templateParameters != null) branch inTryCreatePropertiesInternal and delegate toproperties.ResetMessageProperties(templateParameters, templateParameters.Length), which keeps existing event properties and refreshes only the message-derived portion.
  • ThetemplateParameters is null && HasMessageTemplateParameters branch keeps the old “lazy capture from message” semantics.

One thing to keep in mind is thatTryCreatePropertiesInternal now silently ignores calls with a non-null but empty parameter array when_properties is null (returnsnull, no dictionary created). That matches previous behavior, but if future callers rely on “at least get an empty properties dictionary when template parsing has run”, they’ll need to handle thenull return explicitly.

Overall, the integration with the newPropertiesDictionary API looks good.

Also applies to: 431-445


699-719:Immutability checks updated to array model; semantics preserved

IsLogEventThreadAgnosticImmutable andHasImmutableProperties have been updated to use:

  • properties.MessageProperties.Length instead of a listCount, and
  • the newHasImmutableProperties(PropertiesDictionary) helper that first tries the message-properties fast path then falls back to the enumerator.

Key points:

  • The “all properties come from the message template” fast-path now checksproperties.Count == _parameters?.Length && properties.Count == properties.MessageProperties.Length, which aligns with the newMessageProperties semantics.
  • When that doesn’t hold, you still iterate all properties (message + event) viaGetPropertyEnumerator() and runIsSafeToDeferFormatting as before.

Given the new invariants inPropertiesDictionary, this keeps the immutability logic correct; the only impact is that the trivial fast path will trigger a bit less often (notably for the ≤3 inline case). That’s a reasonable tradeoff.

Also applies to: 720-746

tests/NLog.UnitTests/LoggerTests.cs (1)

1363-1368:Updated Conditional tests to assert onException.Message look correct*

The revised assertions forConditionalTrace* /ConditionalDebug* tests:

  • Switch expected strings from hard-coded full diagnostic text to interpolated strings that appendargException.Message after the formatted error description.
  • Still respect culture-dependent formatting of numeric arguments (404,5 etc.) viaNLCulture.
  • Keep the overall${logger}|${message}${exception:message} layout expectation intact.

The use of string interpolation plusargException.Message aligns the tests with the new behavior of the formatter and is syntactically fine (the escaped apostrophe\' is harmless, though not strictly needed). No functional test issues here.

Also applies to: 1536-1541, 1708-1713, 1881-1886

Copy link

@coderabbitaicoderabbitaibot 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/Internal/PropertiesDictionary.cs (4)

127-127:Clarify MessageProperties property behavior.

The property returns an empty array when_messageProperties.Length != _messagePropertiesCount, which means callers get an empty array when the backing array has extra capacity. This seems counterintuitive—callers might expect to access the actual message properties rather than receiving an empty array.

If this is intentional (e.g., to indicate "not available in direct array form, use the dictionary interface"), consider adding a comment explaining the rationale. Alternatively, consider returning a truncated view:

publicMessageTemplateParameter[]MessageProperties{get{if(_messagePropertiesCount==0||_messagePropertiesisnull)returnArrayHelper.Empty<MessageTemplateParameter>();if(_messageProperties.Length==_messagePropertiesCount)return_messageProperties;// Return truncated viewvarresult=newMessageTemplateParameter[_messagePropertiesCount];Array.Copy(_messageProperties,result,_messagePropertiesCount);returnresult;}}

158-167:Consider updating parameter type for consistency.

TheoldMessageProperties parameter is still typed asIList<MessageTemplateParameter>, but the caller at line 145 passes aMessageTemplateParameter[]. While this works (arrays implement IList), it's inconsistent with the broader refactoring toward array-based storage.

For consistency and to avoid implicit boxing/interface calls, consider updating the signature:

-private static void RemoveOldMessageProperties(Dictionary<object, PropertyValue> eventProperties, IList<MessageTemplateParameter> oldMessageProperties, int oldMessagePropertiesCount)+private static void RemoveOldMessageProperties(Dictionary<object, PropertyValue> eventProperties, MessageTemplateParameter[] oldMessageProperties, int oldMessagePropertiesCount)

215-249:Consider extracting common logic to reduce duplication.

The array-based optimization logic in the indexer setter (lines 215-228) and Add method (lines 237-249) is nearly identical, differing only in how existing keys are handled:

  • Indexer: Updates the existing entry (line 222)
  • Add: Throws ArgumentException (line 244)

To reduce duplication and improve maintainability, consider extracting the common logic:

privateboolTryAddOrUpdateArrayProperty(stringpropertyName,object?value,boolallowUpdate,outbooladded){if(!SkipDictionaryAllocation()){added=false;returnfalse;}varmessageProperties=_messageProperties??(_messageProperties=newMessageTemplateParameter[3]);for(inti=0;i<_messagePropertiesCount;++i){if(messageProperties[i].Name.Equals(propertyName)){if(!allowUpdate){thrownewArgumentException($"An item with the same key{propertyName} has already been added.",nameof(propertyName));}messageProperties[i]=newMessageTemplateParameter(propertyName,value,messageProperties[i].Format,messageProperties[i].CaptureType);added=false;returntrue;}}messageProperties[_messagePropertiesCount++]=newMessageTemplateParameter(propertyName,value,null,CaptureType.Unknown);added=true;returntrue;}

Then both the indexer and Add can use this helper.


450-461:Consider simplifying the resetMessageProperties calculation.

The logic to determine if all parameters haveCaptureType.Unknown works correctly but can be simplified using a loop:

-resetMessageProperties = messagePropertiesCount > 0 && messageProperties[0].CaptureType == CaptureType.Unknown;-if (resetMessageProperties)-{-    for (int i = 1; i < messagePropertiesCount; ++i)-    {-        if (messageProperties[i].CaptureType != CaptureType.Unknown)-        {-            resetMessageProperties = false;-            break;-        }-    }-}+resetMessageProperties = messagePropertiesCount > 0;+for (int i = 0; i < messagePropertiesCount; ++i)+{+    if (messageProperties[i].CaptureType != CaptureType.Unknown)+    {+        resetMessageProperties = false;+        break;+    }+}

This eliminates the special handling of the first element and makes the logic clearer.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and betweenea6180a and92ffecb.

📒 Files selected for processing (3)
  • src/NLog/Internal/PropertiesDictionary.cs (16 hunks)
  • src/NLog/LogEventInfo.cs (7 hunks)
  • tests/NLog.UnitTests/LoggerTests.cs (4 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/NLog/LogEventInfo.cs
🧰 Additional context used
🧬 Code graph analysis (2)
tests/NLog.UnitTests/LoggerTests.cs (1)
tests/NLog.UnitTests/NLogTestBase.cs (2)
  • AssertDebugLastMessage (82-86)
  • AssertDebugLastMessage (88-91)
src/NLog/Internal/PropertiesDictionary.cs (4)
src/NLog/Internal/LogMessageTemplateFormatter.cs (1)
  • MessageTemplateParameter (267-278)
src/NLog/MessageTemplates/MessageTemplateParameter.cs (2)
  • MessageTemplateParameter (105-111)
  • MessageTemplateParameter (120-126)
src/NLog/LogEventInfo.cs (2)
  • PropertiesDictionary (414-429)
  • PropertiesDictionary (431-445)
src/NLog/Internal/ArrayHelper.cs (1)
  • ArrayHelper (36-53)
⏰ 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). (2)
  • GitHub Check: task-list-completed
  • GitHub Check: task-list-completed
🔇 Additional comments (3)
tests/NLog.UnitTests/LoggerTests.cs (1)

1364-1364:LGTM - Test expectations updated for dynamic exception messages.

The test assertions now useargException.Message interpolation instead of hardcoded strings, making the tests more maintainable and resilient to exception formatting changes. This pattern is consistently applied across all ConditionalTrace and ConditionalDebug test methods.

Also applies to: 1367-1367, 1537-1537, 1540-1540, 1709-1709, 1712-1712, 1882-1882, 1885-1885

src/NLog/Internal/PropertiesDictionary.cs (2)

82-95:LGTM - Clean migration to array-based storage.

The field declarations and constructor properly transition fromIList<MessageTemplateParameter> toMessageTemplateParameter[] with explicit count tracking. The constructor correctly delegates toResetMessageProperties to ensure the array and count are initialized together.


497-626:LGTM - Enumerator correctly handles array bounds.

ThePropertyDictionaryEnumerator properly iterates only over the populated portion of the array (indices 0 to_messagePropertiesCount-1), avoiding the null-entry issue mentioned in past reviews. Key safety measures:

  1. Index bounds are checked against_messagePropertiesCount (lines 592, 602)
  2. Index is only set to values within valid range (lines 598, 606)
  3. Defensive fallback to empty array handles edge cases (lines 516, 533, 550)

The complex logic at lines 595-609 correctly handles the case where the dictionary exists and might have overridden some message properties.

@snakefootsnakefootforce-pushed theproperty_dictionary branch 8 times, most recently from259b573 tod9b3a62CompareNovember 22, 2025 16:36
Copy link

@coderabbitaicoderabbitaibot 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/Internal/PropertiesDictionary.cs (1)

166-180:Review capacity calculation when promoting to dictionary.

WhenprepareForInsert is false (line 172), the dictionary capacity equalsmessagePropertiesCount without extra room. If the caller immediately inserts additional properties after promotion, the dictionary may need to resize.

InResetMessageProperties line 137,prepareForInsert is always false when calling this method with old properties. IfnewMessagePropertiesCount > oldMessagePropertiesCount, consider passing a larger initial capacity to avoid resizing.

Consider this adjustment to provide extra capacity when promoting and new properties are larger:

 eventProperties = _eventProperties = oldMessagePropertiesCount == 0 ?     new Dictionary<object, PropertyValue>(newMessagePropertiesCount, PropertyKeyComparer.Default) :-    InitializeEventPropertiesDictionary(false, oldMessageProperties, oldMessagePropertiesCount, out var _);+    InitializeEventPropertiesDictionary(newMessagePropertiesCount > oldMessagePropertiesCount, oldMessageProperties, oldMessagePropertiesCount, out var _);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between259b573 andd9b3a62.

📒 Files selected for processing (2)
  • src/NLog/Internal/PropertiesDictionary.cs (16 hunks)
  • src/NLog/LogEventInfo.cs (5 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/NLog/LogEventInfo.cs (3)
src/NLog/Internal/PropertiesDictionary.cs (3)
  • PropertiesDictionary (89-95)
  • PropertiesDictionary (100-106)
  • ResetMessageProperties (126-153)
src/NLog/Internal/LogMessageTemplateFormatter.cs (1)
  • MessageTemplateParameter (267-278)
src/NLog/MessageTemplates/MessageTemplateParameter.cs (2)
  • MessageTemplateParameter (105-111)
  • MessageTemplateParameter (120-126)
src/NLog/Internal/PropertiesDictionary.cs (4)
src/NLog/Internal/LogMessageTemplateFormatter.cs (1)
  • MessageTemplateParameter (267-278)
src/NLog/MessageTemplates/MessageTemplateParameter.cs (2)
  • MessageTemplateParameter (105-111)
  • MessageTemplateParameter (120-126)
src/NLog/LogEventInfo.cs (2)
  • PropertiesDictionary (414-429)
  • PropertiesDictionary (431-445)
src/NLog/Internal/ArrayHelper.cs (1)
  • ArrayHelper (36-53)
⏰ 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 (18)
src/NLog/LogEventInfo.cs (6)

414-429:LGTM: Array-based signature change.

The method signature now acceptsMessageTemplateParameter[]? and correctly uses.Length for null-safety checks and passes the explicit count toResetMessageProperties.


431-445:LGTM: Constructor accepts array-based parameters.

The signature change fromIList to array is consistent with the rest of the PR. The PropertiesDictionary constructor handles the array correctly.


714-718:LGTM: Updated to use array Length.

The comparison correctly usesMessageProperties.Length instead ofCount for the array-based implementation.


720-746:Nice improvement: foreach loop for iteration.

Switching from index-based loop toforeach on line 726 is cleaner and avoids manual bounds management when iterating over all populated elements.


831-845:LGTM: Array Length check for template parameters.

The condition correctly usesmessageProperties.Length to check for empty arrays and properly accesses the first element'sCaptureType.


468-481:Verify MessageProperties behavior when array is partially filled.

Line 468 checksMessageProperties.Length > 0. However, in PropertiesDictionary.cs line 124, theMessageProperties property returns an empty array when_messagePropertiesCount < _messageProperties.Length (i.e., when the backing array is partially filled). This means:

  • If there are 2 properties in a 3-element array,MessageProperties.Length is 0 (empty array returned)
  • This check would fail, falling through to line 472 to re-parse the message

Confirm that this behavior is intentional and that re-parsing is acceptable in the partially-filled case, or consider using_properties._messagePropertiesCount directly.

Run this script to check how MessageTemplateParameters handles the MessageProperties property:

src/NLog/Internal/PropertiesDictionary.cs (12)

82-94:LGTM: Array-backed storage with explicit count.

The introduction ofMessageTemplateParameter[]? _messageProperties andint _messagePropertiesCount enables the optimization to avoid dictionary allocation for ≤3 properties. Constructor correctly initializes viaResetMessageProperties with the explicit count.


108-122:LGTM: GetEventProperties handles migration from array to dictionary.

The logic correctly checks_messagePropertiesCount for emptiness and resets the array-backed storage whenresetMessageProperties is true (lines 115-119), ensuring properties migrate to the dictionary when needed.


124-124:Clarify MessageProperties behavior for partially-filled arrays.

The property returns the backing array only when_messagePropertiesCount == _messageProperties.Length (fully populated), otherwise it returns an empty array. This means:

  • A 3-element array with 2 properties returns an empty array
  • Consumers checking.Length > 0 will see 0 even though properties exist

Verify that all consumers correctly handle this "all-or-nothing" behavior. The usage in LogEventInfo.cs line 468 would fail this check and fall back to re-parsing, which may be inefficient.

Consider documenting this behavior or exposing a MessagePropertiesCount property for consumers that need to know the actual count.


126-153:LGTM: ResetMessageProperties signature updated for arrays.

The method correctly acceptsMessageTemplateParameter[]? with an explicitnewMessagePropertiesCount, handles migration from old to new properties, and updates internal fields (lines 151-152).


155-164:LGTM: Loop bounds respect explicit count.

The iteration usesoldMessagePropertiesCount instead of array length, ensuring only valid elements are processed.


198-232:LGTM: Indexer and Add optimized for array-backed storage.

Both methods checkSkipDictionaryAllocation() before usingAddOrUpdateMessageProperties, avoiding dictionary allocation when the array has capacity. The fallback toGetEventProperties(true) ensures dictionary-backed storage when needed.


241-246:LGTM: Clear resets both dictionary and array state.

Correctly nullifies_eventProperties,_messageProperties, and resets_messagePropertiesCount to 0.


349-390:LGTM: Lookup uses explicit count for bounds.

TryLookupMessagePropertyValue correctly checks_messagePropertiesCount for emptiness (line 351) and uses it for loop bounds (lines 367, 378), ensuring only valid array slots are accessed.


421-444:LGTM: Array-based uniqueness check.

The method correctly usesparameterList.Length for bounds and iterates over array elements to verify uniqueness.


449-482:LGTM: Insertion handles CaptureType and name conflicts.

The method determines whether to reset message properties based onCaptureType.Unknown (lines 451-462), iterates overmessagePropertiesCount (line 464), and mutates the array when generating unique names (line 478). The logic is sound.


498-627:LGTM: Enumerator uses explicit count for iteration.

ThePropertyDictionaryEnumerator correctly:

  • Initializes_messagePropertiesIndex based on_messagePropertiesCount > 0 (line 508)
  • Uses_messagePropertiesCount for bounds checks (lines 592-593, 603)
  • Accesses array elements safely within the valid range

This addresses the issue mentioned in past review comments about trailing nulls.


392-414:Verify thread-safety usage intent: is PropertiesDictionary expected to handle concurrent access from multiple threads on the same LogEventInfo instance?

The review identifies real synchronization gaps: line 394's lazy initialization and line 408's non-atomic increment lack protection, while line 114 usesInterlocked.CompareExchange for_eventProperties. This inconsistency is notable.

However, the codebase provides no explicit concurrency guarantees forPropertiesDictionary orLogEventInfo. TheResetMessageProperties method and other code paths lack locks entirely, suggesting single-threaded design. Yet the publicIDictionary interface andInterlocked usage at lines 114 and 436 (LogEventInfo) create ambiguity about intended thread-safety semantics.

Confirm whetherPropertiesDictionary instances should support concurrentProperties[key] mutations from multiple threads. If yes, synchronize_messageProperties consistently. If single-threaded, the concerns are moot and the review warrants dismissal.

@snakefoot
Copy link
ContributorAuthor

snakefoot commentedNov 23, 2025
edited
Loading

Before - MethodMeanErrorStdDevGen0Allocated
CreateOneProperty43.52 ns0.152 ns0.135 ns0.0433273 B
CreateTwoProperty66.58 ns0.243 ns0.215 ns0.0433273 B
CreateThreeProperty85.26 ns0.169 ns0.158 ns0.0433273 B
CreateFourProperty136.99 ns0.290 ns0.271 ns0.0918578 B
CreateFiveProperty158.78 ns0.231 ns0.216 ns0.0918578 B
LookupOneProperty23,990.80 ns52.102 ns43.508 ns0.0305273 B
LookupTwoProperty48,801.43 ns210.317 ns186.440 ns-272 B
LookupThreeProperty73,063.29 ns375.157 ns332.567 ns-272 B
LookupFourProperty96,316.16 ns227.295 ns201.491 ns-577 B
LookupFiveProperty121,360.47 ns508.705 ns450.954 ns-576 B
NotFoundLookupOneProperty14,401.77 ns52.404 ns49.018 ns0.0305273 B
NotFoundLookupTwoProperty15,038.36 ns71.324 ns66.717 ns0.0305273 B
NotFoundLookupThreeProperty14,926.46 ns70.973 ns62.916 ns0.0305273 B
NotFoundLookupFourProperty14,703.16 ns41.193 ns36.516 ns0.0916578 B
NotFoundLookupFiveProperty15,117.90 ns54.292 ns50.785 ns0.0916578 B
EnumerateOneProperty66,770.49 ns188.173 ns166.810 ns-272 B
EnumerateTwoProperty98,869.82 ns179.068 ns167.500 ns-272 B
EnumerateThreeProperty134,075.09 ns398.143 ns372.424 ns-272 B
EnumerateFourProperty167,784.73 ns334.449 ns312.844 ns-576 B
EnumerateFiveProperty212,978.27 ns212.324 ns198.608 ns-576 B
After - MethodMeanErrorStdDevGen0Allocated
CreateOneProperty13.71 ns0.086 ns0.076 ns0.0255160 B
CreateTwoProperty24.22 ns0.041 ns0.036 ns0.0255160 B
CreateThreeProperty39.77 ns0.785 ns0.771 ns0.0255160 B
CreateFourProperty165.54 ns2.125 ns3.244 ns0.0865546 B
CreateFiveProperty184.79 ns0.760 ns0.674 ns0.0865546 B
LookupOneProperty3,470.90 ns6.757 ns6.321 ns0.0229160 B
LookupTwoProperty12,947.58 ns140.115 ns131.064 ns0.0153160 B
LookupThreeProperty27,341.81 ns208.510 ns195.040 ns-160 B
LookupFourProperty96,395.69 ns448.333 ns419.371 ns-545 B
LookupFiveProperty124,658.29 ns603.521 ns564.534 ns-544 B
NotFoundLookupOneProperty5,523.97 ns11.628 ns10.307 ns0.0229160 B
NotFoundLookupTwoProperty9,787.75 ns194.713 ns208.341 ns0.0153160 B
NotFoundLookupThreeProperty15,325.80 ns300.231 ns379.696 ns-160 B
NotFoundLookupFourProperty15,751.95 ns132.918 ns124.332 ns0.0610546 B
NotFoundLookupFiveProperty14,648.94 ns34.737 ns32.493 ns0.0763546 B
EnumerateOneProperty31,429.33 ns90.667 ns84.810 ns-160 B
EnumerateTwoProperty43,504.88 ns144.365 ns120.551 ns-160 B
EnumerateThreeProperty57,493.62 ns161.838 ns143.465 ns-160 B
EnumerateFourProperty164,889.42 ns262.788 ns232.954 ns-544 B
EnumerateFiveProperty201,244.33 ns476.434 ns445.656 ns-544 B

Overall it is an improvement, but Dictionary-initialization at the 4th element is slower, guessing the Dictionary internal resize logic is faster at copying values into the larger arrays.

Interesting that NotFoundLookup is also improved. Guess string-hashcode adds a cost, but surprised that it is more expensive than 2 string-equals.

Copy link

@coderabbitaicoderabbitaibot 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

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between26d9b6c andbf06f98.

📒 Files selected for processing (3)
  • src/NLog/Internal/PropertiesDictionary.cs (17 hunks)
  • src/NLog/LogEventInfo.cs (5 hunks)
  • tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/NLog/LogEventInfo.cs (4)
src/NLog/Internal/PropertiesDictionary.cs (3)
  • PropertiesDictionary (89-95)
  • PropertiesDictionary (100-106)
  • ResetMessageProperties (126-153)
src/NLog/Internal/LogMessageTemplateFormatter.cs (1)
  • MessageTemplateParameter (267-278)
src/NLog/MessageTemplates/MessageTemplateParameter.cs (2)
  • MessageTemplateParameter (105-111)
  • MessageTemplateParameter (120-126)
src/NLog/MessageTemplates/MessageTemplateParameters.cs (3)
  • MessageTemplateParameters (45-198)
  • MessageTemplateParameters (81-94)
  • MessageTemplateParameters (99-103)
tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (2)
src/NLog/Internal/PropertiesDictionary.cs (6)
  • PropertiesDictionary (89-95)
  • PropertiesDictionary (100-106)
  • TryGetValue (329-344)
  • MoveNext (562-575)
  • MoveNext (706-706)
  • MoveNext (782-782)
src/NLog/LogEventInfo.cs (2)
  • PropertiesDictionary (414-429)
  • PropertiesDictionary (431-445)
src/NLog/Internal/PropertiesDictionary.cs (3)
src/NLog/MessageTemplates/MessageTemplateParameter.cs (2)
  • MessageTemplateParameter (105-111)
  • MessageTemplateParameter (120-126)
src/NLog/LogEventInfo.cs (2)
  • PropertiesDictionary (414-429)
  • PropertiesDictionary (431-445)
src/NLog/Internal/ArrayHelper.cs (1)
  • ArrayHelper (36-53)
⏰ 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

Copy link

@coderabbitaicoderabbitaibot 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 (1)
src/NLog/Internal/PropertiesDictionary.cs (1)

124-124:Event properties can leak as message-template parameters.

TheMessageProperties getter returns_messageProperties when_messagePropertiesCount == _messageProperties.Length, but doesn't verify that these are actual message-template parameters (vs. plain event properties stored withCaptureType.Unknown). This means code that relies onMessageTemplateParameters will see faux template parameters after adding the third plain event property, which is a behavioral regression.

Apply the guard suggested in the earlier review:

-public MessageTemplateParameter[] MessageProperties => (_messageProperties is null || _messagePropertiesCount != _messageProperties.Length) ? ArrayHelper.Empty<MessageTemplateParameter>() : _messageProperties;+public MessageTemplateParameter[] MessageProperties+{+    get+    {+        if (_messageProperties is null || _messagePropertiesCount == 0)+            return ArrayHelper.Empty<MessageTemplateParameter>();++        if (_messagePropertiesCount != _messageProperties.Length)+            return ArrayHelper.Empty<MessageTemplateParameter>();++        if (_messageProperties[0].CaptureType == CaptureType.Unknown)+            return ArrayHelper.Empty<MessageTemplateParameter>();++        return _messageProperties;+    }+}
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and betweenbf06f98 and8937426.

📒 Files selected for processing (3)
  • src/NLog/Internal/PropertiesDictionary.cs (17 hunks)
  • src/NLog/LogEventInfo.cs (5 hunks)
  • tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (2)
src/NLog/Internal/PropertiesDictionary.cs (6)
  • PropertiesDictionary (89-95)
  • PropertiesDictionary (100-106)
  • TryGetValue (329-344)
  • MoveNext (562-575)
  • MoveNext (706-706)
  • MoveNext (782-782)
src/NLog/LogEventInfo.cs (2)
  • PropertiesDictionary (414-429)
  • PropertiesDictionary (431-445)
src/NLog/Internal/PropertiesDictionary.cs (3)
src/NLog/MessageTemplates/MessageTemplateParameter.cs (2)
  • MessageTemplateParameter (105-111)
  • MessageTemplateParameter (120-126)
src/NLog/LogEventInfo.cs (2)
  • PropertiesDictionary (414-429)
  • PropertiesDictionary (431-445)
src/NLog/Internal/ArrayHelper.cs (1)
  • ArrayHelper (36-53)
⏰ 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 (31)
src/NLog/LogEventInfo.cs (6)

414-429:LGTM!

The signature change fromIList<MessageTemplateParameter>? toMessageTemplateParameter[]? and corresponding logic updates (usingLength instead ofCount, passingtemplateParameters.Length toResetMessageProperties) align with the array-based refactor.


431-445:LGTM!

The signature update toMessageTemplateParameter[]? is consistent with the broader refactor.


468-471:LGTM!

The getter correctly usesLength for the array-basedMessageProperties.


715-716:LGTM!

The update to useMessageProperties.Length is correct.


724-727:LGTM!

The changes to useLength andforeach over the array are appropriate for the new array-based storage.


844-845:LGTM!

The updates to useMessageProperties.Length are correct.

src/NLog/Internal/PropertiesDictionary.cs (24)

82-83:LGTM!

The shift to array-backed storage with explicit count tracking is the foundation of the memory optimization in this PR.


89-95:LGTM!

The constructor signature change and initialization logic align with the array-based refactor.


108-108:LGTM!

TheIsEmpty check correctly uses_messagePropertiesCount.


110-122:LGTM!

The updated initialization logic correctly handles the promotion from array to dictionary and clears the array-backed storage when the dictionary takes ownership.


126-153:LGTM!

The reworkedResetMessageProperties method correctly handles the transitions between array-backed and dictionary-backed storage, including name collision checks and property removal.


155-164:LGTM!

The updated iteration usingoldMessagePropertiesCount is correct.


166-180:LGTM!

The updated initialization logic correctly uses the explicit count parameter for capacity calculations and insertion.


193-193:LGTM!

TheCount property correctly returns_messagePropertiesCount when using array-backed storage.


199-220:LGTM!

The indexer setter optimization to avoid dictionary allocation for ≤3 properties is the key memory-saving feature of this PR. The boundary checks viaSkipDictionaryAllocation correctly ensure the array has space.


223-232:LGTM!

TheAdd method correctly uses the same optimization path as the indexer setter, withallowUpdate=false to throw on duplicate keys.


408-411:LGTM!

TheSkipDictionaryAllocation guard correctly determines when array-backed storage can be used without allocating a dictionary.


241-246:LGTM!

TheClear method correctly resets all storage fields.


329-344:LGTM!

TheTryGetValue correctly delegates toTryLookupMessagePropertyValue when using array-backed storage.


346-387:LGTM!

The lookup logic correctly uses linear search for small arrays and promotes to dictionary for larger counts. The threshold of 10 properties balances simplicity against performance.


418-441:LGTM!

The uniqueness verification correctly usesLength for array iteration.


446-479:LGTM!

The insertion logic correctly handles name collisions and determines whether properties are true message-template parameters or event properties based onCaptureType.


501-506:LGTM!

The enumerator constructor correctly initializes based on_messagePropertiesCount.


508-523:LGTM!

TheCurrent property correctly accesses the array at the current index.


525-540:LGTM!

TheCurrentParameter property correctly constructsMessageTemplateParameter from both array-backed and dictionary-backed storage.


542-558:LGTM!

TheCurrentProperty property correctly handles both storage types.


562-575:LGTM!

TheMoveNext logic correctly traverses message properties first, then event properties.


577-585:LGTM!

The helper correctly skips message properties in the event dictionary.


587-612:LGTM!

TheMoveNextValidMessageParameter correctly iterates through the array-backed message properties up to_messagePropertiesCount.


619-623:LGTM!

TheReset method now correctly re-initializes_eventEnumerator from_dictionary._eventProperties, addressing the issue from the past review comment.

tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (1)

39-272:Comprehensive benchmark coverage.

The benchmarks appropriately exercise creation, lookup, not-found lookup, and enumeration scenarios for 1–4 properties, providing the performance data that supports the PR's optimization goals.

@snakefootsnakefootforce-pushed theproperty_dictionary branch 3 times, most recently from03bc819 to69d2b95CompareNovember 23, 2025 19:18
Copy link

@coderabbitaicoderabbitaibot 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

♻️ Duplicate comments (1)
src/NLog/Internal/PropertiesDictionary.cs (1)

394-411:Still missing boundary check before array write.

Line 410 writesmessageProperties[_messagePropertiesCount++] without verifying_messagePropertiesCount < messageProperties.Length. WhileSkipDictionaryAllocation() should prevent overflow, this method remains fragile if called incorrectly.

Per the previous review, consider adding a guard:

 private void AddOrUpdateMessageProperties(string propertyName, object? value, bool allowUpdate) {     var messageProperties = _messageProperties ?? (_messageProperties = new MessageTemplateParameter[3]);++    if (_messagePropertiesCount >= messageProperties.Length)+    {+        // Caller should have promoted to dictionary before this point+        throw new InvalidOperationException($"Array capacity exceeded. Maximum {messageProperties.Length} properties without dictionary allocation.");+    }+     for (int i = 0; i < _messagePropertiesCount; ++i)

or document the precondition with an assertion/comment stating that callers must ensure space exists.

🧹 Nitpick comments (1)
src/NLog/Internal/PropertiesDictionary.cs (1)

451-484:Document in-place array mutation at line 480.

The method modifies the inputmessageProperties array when generating unique names for duplicates (line 480). While this is safe in the current call sites (the array is always owned byPropertiesDictionary), it's a subtle contract that could surprise future maintainers.

Consider adding a comment above the method:

 /// <summary> /// Attempt to insert the message-template-parameters into an empty dictionary+/// Note: This method may modify the messageProperties array in-place when resolving duplicate names. /// </summary> private static void InsertMessagePropertiesIntoEmptyDictionary(...)

or at line 480:

+                        // Mutate the original array to reflect the renamed parameter                         messageProperties[i] = new MessageTemplateParameter(uniqueName, messageProperty.Value, messageProperty.Format, messageProperty.CaptureType);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and betweenccc371c and69d2b95.

📒 Files selected for processing (4)
  • src/NLog/Internal/PropertiesDictionary.cs (17 hunks)
  • src/NLog/LogEventInfo.cs (5 hunks)
  • tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (1 hunks)
  • tests/NLog.UnitTests/LogMessageFormatterTests.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/NLog.UnitTests/LogMessageFormatterTests.cs
🧰 Additional context used
🧬 Code graph analysis (3)
src/NLog/LogEventInfo.cs (1)
src/NLog/Internal/PropertiesDictionary.cs (3)
  • PropertiesDictionary (89-95)
  • PropertiesDictionary (100-106)
  • ResetMessageProperties (126-158)
tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (1)
src/NLog/Internal/PropertiesDictionary.cs (6)
  • PropertiesDictionary (89-95)
  • PropertiesDictionary (100-106)
  • TryGetValue (334-349)
  • MoveNext (567-580)
  • MoveNext (711-711)
  • MoveNext (787-787)
src/NLog/Internal/PropertiesDictionary.cs (3)
src/NLog/MessageTemplates/MessageTemplateParameter.cs (2)
  • MessageTemplateParameter (105-111)
  • MessageTemplateParameter (120-126)
src/NLog/LogEventInfo.cs (2)
  • PropertiesDictionary (414-429)
  • PropertiesDictionary (431-445)
src/NLog/Internal/ArrayHelper.cs (1)
  • ArrayHelper (36-53)
🔇 Additional comments (8)
src/NLog/LogEventInfo.cs (1)

414-844:LGTM! Consistent alignment with array-backed parameter storage.

The method signature updates (lines 414, 431) and the switch fromCount toLength checks (lines 419, 469, 715, 724, 844) correctly align with the PropertiesDictionary refactoring to useMessageTemplateParameter[] with explicit count tracking. The call toResetMessageProperties (line 426) properly passes both the array and length.

src/NLog/Internal/PropertiesDictionary.cs (6)

82-95:LGTM! Core array-backed storage.

The switch fromIList<MessageTemplateParameter> toMessageTemplateParameter[] with explicit_messagePropertiesCount (lines 82-83) enables the optimization. The constructor (lines 89-95) correctly initializes viaResetMessageProperties when the array is non-empty.


124-124:MessageProperties guard logic is dense but correct.

The four-part guard correctly prevents exposing event properties (those withCaptureType.Unknown) as message-template parameters, addressing the concern from previous reviews. The condition ensures only fully-populated, template-derived properties are returned.


204-237:Indexer and Add correctly use SkipDictionaryAllocation.

Both the indexer (lines 204-225) andAdd (lines 228-237) checkSkipDictionaryAllocation() before callingAddOrUpdateMessageProperties, ensuring the small-array path is used when space remains. The fallback toGetEventProperties(true) handles dictionary promotion.


126-185:LGTM! State transition logic handles dual storage correctly.

ResetMessageProperties (lines 126-158) properly manages transitions between array-only and dictionary-backed storage. The helper methodsRemoveOldMessageProperties (160-169) andInitializeEventPropertiesDictionary (171-185) correctly use the explicit count parameter to operate on the populated portion of the array.

TheresetMessageProperties out parameter (lines 115-119, 182) that nulls_messageProperties during dictionary promotion is a necessary side effect of the optimization.


336-392:LGTM! Lookup paths correctly handle array-backed storage.

TryGetValue (336-348) properly delegates toTryLookupMessagePropertyValue when the dictionary hasn't been allocated. The linear search (lines 351-392) uses_messagePropertiesCount for bounds and includes the optimization to promote to dictionary when count exceeds 10 (line 359).


506-628:Enumerator correctly iterates the populated array portion.

PropertyDictionaryEnumerator uses_messagePropertiesCount throughout (lines 510, 519, 536, 553, 594-616) to iterate only the populated slots. TheReset() method (626-627) properly reinitializes both_messagePropertiesIndex and_eventEnumerator, addressing the issue from previous reviews.

tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (1)

39-272:LGTM! Benchmarks correctly measure the optimization.

The 16 benchmarks comprehensively cover the key scenarios:

  • Create (1-4 properties): Measures allocation + assignment cost
  • Lookup (1-4 properties): Measures TryGetValue performance with 1000 iterations
  • NotFoundLookup (1-4 properties): Measures miss cost when key doesn't exist
  • Enumerate (1-4 properties): Measures enumerator overhead

The benchmarks properly exercise the array-backed path (1-3 properties) versus dictionary promotion (4 properties), directly supporting the PR's performance goals. The early-exit conditions prevent dead-code elimination.

Copy link

@coderabbitaicoderabbitaibot 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 (2)
src/NLog/Internal/PropertiesDictionary.cs (2)

126-126:Add defensive null-check before array access.

TheMessageProperties property accesses_messageProperties[0].CaptureType when the array length equalsSmallArraySize, but doesn't explicitly verify_messagePropertiesCount > 0. While the current code flow ensures the array is never allocated without adding at least one element, this creates a fragile dependency that could break if allocation logic changes elsewhere.

Consider adding an explicit guard:

-public MessageTemplateParameter[] MessageProperties => (_messageProperties is null || (_messageProperties.Length == SmallArraySize && _messageProperties[0].CaptureType == CaptureType.Unknown)) ? ArrayHelper.Empty<MessageTemplateParameter>() : _messageProperties;+public MessageTemplateParameter[] MessageProperties => (_messageProperties is null || (_messagePropertiesCount == 0) || (_messageProperties.Length == SmallArraySize && _messageProperties[0].CaptureType == CaptureType.Unknown)) ? ArrayHelper.Empty<MessageTemplateParameter>() : _messageProperties;

396-418:Boundary check still missing in array mutation.

AddOrUpdateMessageProperties writes tomessageProperties[_messagePropertiesCount++] (line 412) without verifying_messagePropertiesCount < messageProperties.Length. WhileSkipDictionaryAllocation() prevents overflow in the current call sites, this method remains fragile to future changes or incorrect callers.

As noted in the past review comment, add an explicit guard or precondition comment:

 private void AddOrUpdateMessageProperties(string propertyName, object? value, bool allowUpdate) {     var messageProperties = _messageProperties ?? (_messageProperties = new MessageTemplateParameter[SmallArraySize]);++    // Precondition: _messagePropertiesCount < messageProperties.Length must hold (enforced by SkipDictionaryAllocation caller)+    System.Diagnostics.Debug.Assert(_messagePropertiesCount < messageProperties.Length, "Array capacity exceeded");+     for (int i = 0; i < _messagePropertiesCount; ++i)

Or throw a clear exception:

 private void AddOrUpdateMessageProperties(string propertyName, object? value, bool allowUpdate) {     var messageProperties = _messageProperties ?? (_messageProperties = new MessageTemplateParameter[SmallArraySize]);+    if (_messagePropertiesCount >= messageProperties.Length)+        throw new InvalidOperationException("Cannot add more properties; array capacity exceeded. Call site must check SkipDictionaryAllocation first.");+     for (int i = 0; i < _messagePropertiesCount; ++i)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and betweena9f0f23 and3fb00bd.

📒 Files selected for processing (4)
  • src/NLog/Internal/PropertiesDictionary.cs (18 hunks)
  • src/NLog/LogEventInfo.cs (5 hunks)
  • tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (1 hunks)
  • tests/NLog.UnitTests/LogMessageFormatterTests.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • tests/NLog.UnitTests/LogMessageFormatterTests.cs
🧰 Additional context used
🧬 Code graph analysis (3)
src/NLog/LogEventInfo.cs (2)
src/NLog/Internal/PropertiesDictionary.cs (3)
  • PropertiesDictionary (91-97)
  • PropertiesDictionary (102-108)
  • ResetMessageProperties (128-160)
src/NLog/MessageTemplates/MessageTemplateParameters.cs (3)
  • MessageTemplateParameters (45-198)
  • MessageTemplateParameters (81-94)
  • MessageTemplateParameters (99-103)
src/NLog/Internal/PropertiesDictionary.cs (3)
src/NLog/MessageTemplates/MessageTemplateParameter.cs (2)
  • MessageTemplateParameter (105-111)
  • MessageTemplateParameter (120-126)
src/NLog/LogEventInfo.cs (2)
  • PropertiesDictionary (414-429)
  • PropertiesDictionary (431-445)
src/NLog/Internal/ArrayHelper.cs (1)
  • ArrayHelper (36-53)
tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (2)
src/NLog/Internal/PropertiesDictionary.cs (6)
  • PropertiesDictionary (91-97)
  • PropertiesDictionary (102-108)
  • TryGetValue (336-351)
  • MoveNext (569-582)
  • MoveNext (713-713)
  • MoveNext (789-789)
src/NLog/LogEventInfo.cs (2)
  • PropertiesDictionary (414-429)
  • PropertiesDictionary (431-445)
⏰ 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 (12)
src/NLog/LogEventInfo.cs (2)

414-429:LGTM: Signature changes are consistent.

The migration fromIList<MessageTemplateParameter> toMessageTemplateParameter[] is correctly applied. The conditional checks now useLength instead ofCount, andResetMessageProperties is properly invoked with both the array and its length.


715-731:LGTM: Safe iteration guarded by length check.

The logic at line 715 ensures that iteration overmessageProperties at line 727 only occurs when all properties originate from the message template (properties.Count == messageProperties.Length), preventing iteration over mixed or partially-filled arrays. This is correct.

src/NLog/Internal/PropertiesDictionary.cs (6)

52-52:LGTM: Array-backed storage foundation is well-structured.

The introduction ofSmallArraySize = 3,_messageProperties array, and_messagePropertiesCount tracking provides a clean foundation for the small-array optimization. The constructors correctly initialize the array-backed storage and check againstSmallArraySize before allocating a dictionary.

Also applies to: 84-85, 91-96, 104-108, 110-110


128-160:LGTM: Array-to-dictionary promotion logic is sound.

ResetMessageProperties,RemoveOldMessageProperties, andInitializeEventPropertiesDictionary correctly use_messagePropertiesCount for iteration boundaries and handle the transition from array-backed to dictionary-backed storage. The capacity calculation at line 177 (messagePropertiesCount + 2) provides reasonable headroom for the promoted dictionary.

Also applies to: 162-171, 173-187


205-227:LGTM: Indexer and Add leverage array-backed fast path.

The indexer andAdd method correctly gate onSkipDictionaryAllocation() to use the array-backed mutation path for up to 3 properties, then fall back to dictionary allocation. This aligns with the PR's optimization goals.

Also applies to: 230-239


336-394:LGTM: TryGetValue and TryLookupMessagePropertyValue optimize for small arrays.

TryGetValue delegates toTryLookupMessagePropertyValue when_eventProperties is null, and the lookup correctly uses_messagePropertiesCount as the iteration boundary (lines 371, 382). The threshold check at line 361 (_messagePropertiesCount > 10) intelligently promotes to dictionary for larger counts.


453-486:LGTM: InsertMessagePropertiesIntoEmptyDictionary handles CaptureType.Unknown correctly.

Lines 455–466 detect when all properties haveCaptureType.Unknown (event properties added via indexer) and setresetMessageProperties to true, signaling that the array should be cleared after dictionary promotion. This prevents event properties from masquerading as message-template properties. The iteration at line 468 correctly usesmessagePropertiesCount as the boundary.


502-631:LGTM: PropertyDictionaryEnumerator correctly tracks array count.

The enumerator fields (lines 505–506) include_messagePropertiesIndex initialized to -1 when_messagePropertiesCount > 0 (line 512).MoveNextValidMessageParameter (lines 594–619) uses_dictionary._messagePropertiesCount as the iteration boundary (lines 596, 607), andReset (lines 626–630) properly reinitializes both_messagePropertiesIndex and_eventEnumerator.

tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (4)

41-73:LGTM: Create benchmarks correctly exercise array-backed storage and dictionary promotion.

TheCreateOneProperty,CreateTwoProperty,CreateThreeProperty, andCreateFourProperty benchmarks instantiatePropertiesDictionary and add 1–4 properties. This exercises the small-array path (1–3 properties) and the dictionary-promotion threshold (4 properties), aligning with the PR's optimization goals.


75-139:LGTM: Lookup benchmarks measure repeated access performance.

TheLookupOneProperty,LookupTwoProperty,LookupThreeProperty, andLookupFourProperty benchmarks add properties and loop 1000 times callingTryGetValue for existing keys. This measures the lookup performance improvements reported in the PR objectives (e.g., 30,178→5,065 ns for 1 property).


141-193:LGTM: NotFoundLookup benchmarks measure negative-lookup performance.

TheNotFoundLookup* benchmarks loop 1000 times searching for a missing key ("Hello5"). This exercises the early-exit paths inTryLookupMessagePropertyValue and measures the improvements noted in the PR objectives (e.g., 19,132→4,837 ns for 1 property).


195-271:LGTM: Enumerate benchmarks measure iteration performance.

TheEnumerate* benchmarks loop 1000 times callingGetPropertyEnumerator and iterating viaMoveNext. The null-check onCurrent.Key (lines 206, 225, 245, 266) prevents the compiler from optimizing away the loop body. These benchmarks measure the enumeration improvements reported in the PR objectives (e.g., 70,127→36,365 ns for 1 property).

Copy link

@coderabbitaicoderabbitaibot 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 (2)
src/NLog/Internal/PropertiesDictionary.cs (2)

52-52:Inline-then-dictionary design and MessageProperties gating look consistent

The new layout hits the PR goals without obvious regressions:

  • SmallArraySize = 3 plus the updated constructors let you avoid allocating aDictionary for the common “≤3 properties” case while still honoring an explicitinitialCapacity for larger cases.
  • MessageProperties only exposes_messageProperties when it’s not the 3-slot inline buffer withCaptureType.Unknown in the first slot, so event-only inline properties no longer leak intoLogEventInfo.MessageTemplateParameters.
  • ResetMessageProperties correctly distinguishes:
    • pure “message-template only” dictionaries (can stay array-backed if unique and no previous message props),
    • dictionaries with existing event properties or duplicate parameter names (force dictionary, remove old message props, then insert new ones),
    • and the “clear template metadata” case (newMessageProperties == null).
  • InitializeEventPropertiesDictionary +InsertMessagePropertiesIntoEmptyDictionary maintainIsMessageProperty consistently and useresetMessageProperties to drop the inline buffer when it only held event properties, which aligns enumeration and lookup semantics with the new storage model.

The capture-type–based sentinel is a subtle but coherent contract; a brief comment nearSmallArraySize orMessageProperties explaining “all-Unknown in the small array means event-only inline slots” would help future maintainers avoid misusingCaptureType.Unknown, but functionally this looks sound.

Also applies to: 84-86, 91-109, 110-127, 173-188, 450-486


200-201:Small-array add/update and lookup behavior is correct but relies on a precondition

The combination ofSkipDictionaryAllocation,TryLookupMessagePropertyValue, andAddOrUpdateMessageProperties behaves as intended:

  • Inserts and indexer sets stay in the inline buffer while_eventProperties == null and_messagePropertiesCount < _messageProperties.Length, and promote to a dictionary on the 4th insert with a sensible initial capacity.
  • The small-array lookup path only handles string andIgnoreCasePropertyKey keys, while non-string keys always go straight to the dictionary path, matching how inserts are routed.
  • TryLookupMessagePropertyValue switches to dictionary-backed lookup once there are more than 10 message properties, which lines up with the uniqueness fast-path threshold.

AddOrUpdateMessageProperties depends onSkipDictionaryAllocation() to guarantee_messagePropertiesCount < messageProperties.Length; a short precondition comment orDebug.Assert(_messagePropertiesCount < messageProperties.Length) at the top of that method would make this contract explicit and guard against future accidental misuse.

Also applies to: 336-375, 396-419

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between3fb00bd andf8e3d6c.

📒 Files selected for processing (6)
  • src/NLog/Internal/LogMessageTemplateFormatter.cs (3 hunks)
  • src/NLog/Internal/PropertiesDictionary.cs (17 hunks)
  • src/NLog/LogEventInfo.cs (5 hunks)
  • src/NLog/Logger.cs (1 hunks)
  • tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (1 hunks)
  • tests/NLog.UnitTests/LogMessageFormatterTests.cs (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (2)
  • src/NLog/Logger.cs
  • src/NLog/LogEventInfo.cs
🧰 Additional context used
🧬 Code graph analysis (2)
tests/NLog.UnitTests/LogMessageFormatterTests.cs (1)
src/NLog/LogEventInfo.cs (13)
  • LogEventInfo (51-846)
  • 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 (488-491)
  • LogEventInfo (500-503)
  • LogEventInfo (528-540)
  • LogEventInfo (551-554)
src/NLog/Internal/PropertiesDictionary.cs (4)
src/NLog/MessageTemplates/MessageTemplateParameter.cs (2)
  • MessageTemplateParameter (105-111)
  • MessageTemplateParameter (120-126)
src/NLog/LogEventInfo.cs (2)
  • PropertiesDictionary (414-429)
  • PropertiesDictionary (431-445)
src/NLog/Targets/TargetWithContext.cs (1)
  • Dictionary (307-310)
src/NLog/Internal/ArrayHelper.cs (1)
  • ArrayHelper (36-53)
⏰ 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). (2)
  • GitHub Check: task-list-completed
  • GitHub Check: task-list-completed
🔇 Additional comments (5)
src/NLog/Internal/LogMessageTemplateFormatter.cs (2)

138-200:Array-based Render overloads preserve semantics and avoid IList overhead

The two Render overloads correctly mirror the previous IList-based logic while switching toMessageTemplateParameter[]?:

  • Positional-only templates stay on the fast path without allocating the parameter array.
  • Mixed/templated cases still “rewind” and rebuild with sequentialholeIndex, and the span overload usessb.ClearBuilder() appropriately.
  • Using the sameholeIndex accounting in both overloads ensuresVerifyMessageTemplateParameters sees a consistent count.

I don’t see any new correctness or allocation pitfalls here; the behavior should be equivalent with better locality.

Also applies to: 203-264


267-278:Truncation of message-template parameters is correct and safer

VerifyMessageTemplateParameters now trims the backing array toholeIndex, eliminating unused/default slots when there are fewer holes thanparameters.Length. This avoids leaking dummy entries intoPropertiesDictionary and keeps_messagePropertiesCount aligned with the actual data.

The implementation is straightforward and allocation is bounded by the number of real holes, so this looks solid.

tests/NLog.UnitTests/LogMessageFormatterTests.cs (1)

228-246:Good coverage of positional templates vs inline Properties storage

This test nicely validates that:

  • MessageTemplateParameters for a positional template are derived from_parameters (and markedCaptureType.Normal), not fromProperties.
  • InlineProperties["0".."2"] (via the small-array path) remain intact and are not treated as template metadata.

It’s a precise regression test for the new PropertiesDictionary behavior around positional placeholders.

src/NLog/Internal/PropertiesDictionary.cs (1)

502-583:Enumerator correctly interleaves message and event properties after the refactor

The updatedPropertyDictionaryEnumerator logic looks consistent and fixes earlier pitfalls:

  • _messagePropertiesIndex is initialized from_messagePropertiesCount, so message-template parameters (when present) are always yielded first.
  • MoveNextValidMessageParameter respects_messagePropertiesCount and, when a dictionary exists, only advances to entries whose backingPropertyValue is markedIsMessageProperty, avoiding duplication with the event-property phase.
  • MoveNextValidEventProperty then skips any dictionary entries flagged as message properties, yielding only true event properties.
  • Reset() restores both_messagePropertiesIndex and_eventEnumerator from the dictionary’s current state, soKeyCollection/ValueCollection enumerations after a reset see the same sequence as an initial enumerator.

Given this,Count,Keys,Values, andforeach over the dictionary should all observe a coherent view of the combined properties.

Also applies to: 594-619, 626-630

tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (1)

39-273:Benchmarks appropriately cover inline vs dictionary scenarios

The benchmark suite exercises exactly the scenarios this PR targets:

  • Creation benchmarks for 1–4 properties show the allocation/promotion behavior.
  • Lookup / not-found lookup / enumeration benches run repeated operations on a fresh dictionary per method, usingGetPropertyEnumerator() so the custom enumerator is also covered.
  • The simpleif checks inside the loops are enough to prevent the JIT from optimizing the work away without distorting the microbenchmark too much.

Looks good as a basis for comparing the small-array representation against the always-dictionary baseline.

Copy link

@coderabbitaicoderabbitaibot 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/Internal/PropertiesDictionary.cs (1)

384-417:Consider defensive boundary check or precondition comment.

Line 402 writes intomessageProperties[_messagePropertiesCount++] without an explicit bounds check. WhileSkipDictionaryAllocation() (line 416) enforces the precondition_messagePropertiesCount < _messageProperties.Length, the method itself is fragile to future refactoring.

Consider adding a precondition comment or defensive check:

 private void AddOrUpdateMessageProperties(object key, object? value, bool allowUpdate) {+    // Precondition: SkipDictionaryAllocation() ensures array capacity before calling     if (SkipDictionaryAllocation() && key is string propertyName && propertyName.Length > 0)     {         var messageProperties = _messageProperties ?? (_messageProperties = new MessageTemplateParameter[SmallArraySize]);

Or add an explicit guard:

         var messageProperties = _messageProperties ?? (_messageProperties = new MessageTemplateParameter[SmallArraySize]);+        if (_messagePropertiesCount >= messageProperties.Length)+        {+            // Should not happen if SkipDictionaryAllocation is used correctly+            throw new InvalidOperationException("Array capacity exceeded.");+        }         for (int i = 0; i < _messagePropertiesCount; ++i)
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and betweenf8e3d6c and924795e.

📒 Files selected for processing (4)
  • src/NLog/Internal/PropertiesDictionary.cs (17 hunks)
  • src/NLog/LogEventInfo.cs (5 hunks)
  • tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (1 hunks)
  • tests/NLog.UnitTests/LogMessageFormatterTests.cs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
tests/NLog.UnitTests/LogMessageFormatterTests.cs (1)
src/NLog/LogEventInfo.cs (13)
  • LogEventInfo (51-846)
  • 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 (488-491)
  • LogEventInfo (500-503)
  • LogEventInfo (528-540)
  • LogEventInfo (551-554)
src/NLog/LogEventInfo.cs (3)
src/NLog/Internal/PropertiesDictionary.cs (3)
  • PropertiesDictionary (91-97)
  • PropertiesDictionary (102-108)
  • ResetMessageProperties (128-160)
src/NLog/MessageTemplates/MessageTemplateParameter.cs (2)
  • MessageTemplateParameter (105-111)
  • MessageTemplateParameter (120-126)
src/NLog/MessageTemplates/MessageTemplateParameters.cs (3)
  • MessageTemplateParameters (45-198)
  • MessageTemplateParameters (81-94)
  • MessageTemplateParameters (99-103)
tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (2)
src/NLog/Internal/PropertiesDictionary.cs (6)
  • PropertiesDictionary (91-97)
  • PropertiesDictionary (102-108)
  • TryGetValue (324-339)
  • MoveNext (568-581)
  • MoveNext (712-712)
  • MoveNext (788-788)
src/NLog/LogEventInfo.cs (2)
  • PropertiesDictionary (414-429)
  • PropertiesDictionary (431-445)
⏰ 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 (3)
src/NLog/Internal/PropertiesDictionary.cs (1)

206-221:LGTM! Indexer setter enables array-backed mutations.

The new indexer setter correctly routes toAddOrUpdateMessageProperties, enabling in-place updates for the small-array optimization. TheallowUpdate: true parameter ensures existing properties can be overwritten without throwing.

tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (1)

39-272:LGTM! Comprehensive benchmarks for array-backed optimization.

The benchmarks appropriately cover the key operations (create, lookup, not-found lookup, enumerate) across the 1–4 property range where the small-array optimization applies. The consistent patterns across all 16 methods make the performance comparisons reliable. These benchmarks align well with the PR objectives demonstrating allocation reduction and performance improvements for 1–3 properties.

tests/NLog.UnitTests/LogMessageFormatterTests.cs (1)

228-246:Unable to verify duplicate test method due to repository access issues.

The verification cannot be completed as the repository failed to clone. The original concern about potential duplicateMessageTemplatePositional test methods intests/NLog.UnitTests/LogMessageFormatterTests.cs requires direct file inspection to confirm or refute.

To complete this verification:

  • Check if multipleMessageTemplatePositional method declarations exist in the file (search for method name and line numbers)
  • If duplicates are found, confirm whether they have identical or different signatures
  • Remove any duplicate methods to prevent compilation errors or test confusion

@sonarqubecloud
Copy link

Copy link

@coderabbitaicoderabbitaibot 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/Internal/PropertiesDictionary.cs (1)

126-161:Hiding event-only small-array slots from MessageTemplateParameters

MessageProperties andResetMessageProperties together ensure that:

  • For true message-template parameters,_messageProperties is a tight array (Length == count) with non-UnknownCaptureType, andMessageProperties returns it.
  • For the small-array event-only path:
    • _messageProperties is a fixed 3-slot array withCaptureType.Unknown in slot 0.
    • MessageProperties returnsArrayHelper.Empty<MessageTemplateParameter>(), soLogEventInfo.MessageTemplateParameters never sees these as template metadata.

This addresses the earlier concern about leaking event-only slots viaMessageProperties while keeping the accessor cheap. A minor optional refinement would be to also guard on_messagePropertiesCount > 0 in the expression for extra robustness, but with the current invariants it’s not functionally required.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between924795e and219f411.

📒 Files selected for processing (4)
  • src/NLog/Internal/PropertiesDictionary.cs (17 hunks)
  • src/NLog/LogEventInfo.cs (5 hunks)
  • tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (1 hunks)
  • tests/NLog.UnitTests/LogMessageFormatterTests.cs (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (1)
src/NLog/Internal/PropertiesDictionary.cs (6)
  • PropertiesDictionary (91-97)
  • PropertiesDictionary (102-108)
  • TryGetValue (324-339)
  • MoveNext (568-581)
  • MoveNext (712-712)
  • MoveNext (788-788)
tests/NLog.UnitTests/LogMessageFormatterTests.cs (2)
src/NLog/LogEventInfo.cs (13)
  • LogEventInfo (51-846)
  • 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 (488-491)
  • LogEventInfo (500-503)
  • LogEventInfo (528-540)
  • LogEventInfo (551-554)
src/NLog/Internal/PropertiesDictionary.cs (3)
  • Contains (244-256)
  • Contains (658-670)
  • Contains (743-746)
⏰ 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 (8)
src/NLog/LogEventInfo.cs (1)

414-445:Array-based template-parameter plumbing looks consistent with new PropertiesDictionary semantics

The migration from IList toMessageTemplateParameter[] inTryCreatePropertiesInternal,CreatePropertiesInternal,HasMessageTemplateParameters,MessageTemplateParameters, and the immutability helpers appears internally consistent:

  • TryCreatePropertiesInternal correctly:

    • Creates_properties from a non-emptytemplateParameters array.
    • Resets message properties on an existing dictionary viaResetMessageProperties(templateParameters, templateParameters.Length).
    • Defers toHasMessageTemplateParameters only whentemplateParameters is null, avoiding unnecessary work.
  • CreatePropertiesInternal usesPropertiesDictionary(int initialCapacity) when there is no template array, andPropertiesDictionary(MessageTemplateParameter[] templateParameters) when there is, neatly aligning with the small-array vs. dictionary promotion inPropertiesDictionary.

  • HasMessageTemplateParameters’ guard on_formattedMessage is null && _parameters?.Length > 0 prevents repeated template probing once formatting is done, and the use ofILogMessageFormatter.HasProperties(this) matches the intended “cheap probe, full parse later” pattern.

  • MessageTemplateParameters now correctly depends on_properties?.MessageProperties returning the filteredMessageTemplateParameter[] (which, per the newPropertiesDictionary.MessageProperties implementation, excludes event-only small-array slots), and only falls back to reparsing_message +_parameters when no captured metadata exists.

  • IsLogEventThreadAgnosticImmutable andHasImmutableProperties useproperties.MessageProperties.Length rather than a listCount, and the “fast path”properties.Count == _parameters?.Length && properties.Count == properties.MessageProperties.Length still correctly identifies the pure message-template case.

  • ResetMessageTemplateParameters’ final conditionreturn _properties.MessageProperties.Length == 0; nicely leverages the new filtering logic onMessageProperties to decide when it is safe to re-parse templates.

I don’t see correctness or threading regressions introduced by these changes; they align with the new array-backedPropertiesDictionary behavior.

Also applies to: 447-459, 465-481, 700-719, 721-747, 832-845

src/NLog/Internal/PropertiesDictionary.cs (5)

52-86:Small-array backing and promotion thresholds are sound

UsingSmallArraySize = 3 with_messageProperties +_messagePropertiesCount, and only allocating_eventProperties wheninitialCapacity > SmallArraySize or on promotion, matches the PR objective of avoiding aDictionary for ≤3 properties:

  • PropertiesDictionary(MessageTemplateParameter[]? messageParameters) andPropertiesDictionary(int initialCapacity) correctly steer between array and dictionary paths.
  • IsEmpty andCount => _eventProperties?.Count ?? _messagePropertiesCount keep the public surface consistent regardless of backing store.

This is a clean, low-overhead way to introduce the small-array optimization without exposing it to callers.

Also applies to: 91-110, 196-201


112-140:Promotion to dictionary with capacity hint and cleanup of inline slots

GetEventProperties andInitializeEventPropertiesDictionary correctly:

  • Promote from the small-array representation by seeding theDictionary<object, PropertyValue> from_messageProperties with an appropriate capacity (messagePropertiesCount ormessagePropertiesCount + 2 whenprepareForInsert).
  • UseresetMessageProperties fromInsertMessagePropertiesIntoEmptyDictionary to distinguish:
    • “True” message-template properties (IsMessageProperty = true, capture type ≠Unknown).
    • Small-array event-only slots (allCaptureType.Unknown), which are migrated and then cleared via_messageProperties = null; _messagePropertiesCount = 0;.

ResetMessageProperties then builds on this by:

  • Creating a fresh dictionary when needed (existing event properties, duplicates, or old message properties present).
  • Removing prior template-derived entries viaRemoveOldMessageProperties.
  • Re-inserting new message-template parameters with correctIsMessageProperty flags.

Overall, the promotion and reset logic looks correct and should avoid both capacity thrash and stale template entries.

Also applies to: 162-188


341-382:Lookup and small-array add/update paths behave correctly for both event and template scenarios

The new code inTryLookupMessagePropertyValue,AddOrUpdateMessageProperties, andSkipDictionaryAllocation looks solid:

  • TryLookupMessagePropertyValue:
    • Early-outs when_messagePropertiesCount == 0.
    • Uses dictionary lookup for large message property sets (_messagePropertiesCount > 10), otherwise falls back to cheap linear scans with ordinal or case-insensitive comparisons.
  • SkipDictionaryAllocation ensures:
    • Up to 3 string keys are stored in the_messageProperties array without overflowing (_messagePropertiesCount < _messageProperties.Length).
    • Any non-string key, or the 4th+ property, immediately promotes to the dictionary path.
  • AddOrUpdateMessageProperties:
    • Reuses the array slot whenallowUpdate is true and the key already exists, otherwise throws on duplicate forAdd(...).
    • Correctly migrates existing small-array entries into a dictionary when the array is full, by callingGetEventProperties(true) and then adding/updating in the dictionary.

I don’t see anIndexOutOfRangeException path remaining here; the array-bound precondition is now enforced bySkipDictionaryAllocation.

Also applies to: 384-418


424-447:Uniqueness check and insertion deal well with duplicates and event-only arrays

VerifyUniqueMessageTemplateParametersFast andInsertMessagePropertiesIntoEmptyDictionary cover the tricky cases:

  • VerifyUniqueMessageTemplateParametersFast:
    • Returnstrue for 0–1 parameters.
    • Forces dictionary usage for >10 parameters.
    • Performs an O(n²) duplicate scan only for small lists, which is acceptable given the threshold.
  • InsertMessagePropertiesIntoEmptyDictionary:
    • UsesresetMessageProperties to detect arrays whereall entries areCaptureType.Unknown (event-only), marking those dictionary values as non-message properties (IsMessageProperty = false).
    • OnArgumentException fromDictionary.Add, only rewrites the name for clashes where the existing entry is a message property, usingGenerateUniquePropertyName and updating theMessageTemplateParameter in-place.

This keeps event-only usage distinct while preserving clear semantics for template-derived parameters, including duplicate names.

Also applies to: 452-485


501-529:Enumerator now aligns with Count and supports reset correctly

The updatedPropertyDictionaryEnumerator:

  • Initializes_messagePropertiesIndex based on_dictionary._messagePropertiesCount and_eventEnumerator from_dictionary._eventProperties.
  • Uses:
    • MoveNextValidMessageParameter to emit message-template entries first:
      • Iterates over_messagePropertiesCount.
      • For dictionary-backed cases, only yields entries whosePropertyValue.IsMessageProperty istrue, avoiding duplication.
    • MoveNextValidEventProperty to then enumerate only non-message entries from the dictionary (IsMessageProperty == false).
  • Current,CurrentParameter, andCurrentProperty consistently index into_dictionary._messageProperties or_eventEnumerator depending on_messagePropertiesIndex.HasValue.
  • Reset() now restores both_messagePropertiesIndex and_eventEnumerator to their initial states, so post-reset iteration covers the same items as the original enumeration.

GivenCount is_eventProperties?.Count ?? _messagePropertiesCount, the enumerator will yield exactlyCount elements in all configurations (small-array only, dictionary-only, or mixed). This fixes the earlier reset/enumerator issues without introducing new ones.

Also applies to: 531-547, 548-565, 568-581, 593-619, 625-629

tests/NLog.UnitTests/LogMessageFormatterTests.cs (1)

228-246:Positional-template test correctly exercises property override behavior

This test nicely validates the key invariant for positional templates:

  • MessageTemplateParameters remains:
    • Valid and positional.
    • Aligned withlogEventInfo.Parameters ("Foo",123,Guid), even afterProperties["0"],["1"],["2"] are overridden.
  • Properties reflects only the overridden values (HelloWorld1/2/3) with aCount of 3.

That matches the new design where message-template metadata stays intact in_messageProperties, while event properties (including overrides) flow throughPropertiesDictionary via the small-array/dictionary paths.

tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (1)

39-345:Benchmarks effectively cover small-array vs. dictionary behavior

The benchmark suite looks well-structured:

  • Create*Property scenarios measure construction + initial inserts across 1–5 keys, naturally driving the small-array path for ≤3 and promotion for ≥4.
  • Lookup*Property andNotFoundLookup*Property perform 1000 iterations against hot dictionaries, which is appropriate for timing steady-state lookups and miss patterns.
  • Enumerate*Property usesGetPropertyEnumerator() andMoveNext() in a tight loop, which directly exercises the updatedPropertyDictionaryEnumerator over both small-array and dictionary-backed cases.

I don’t see correctness issues here; any micro-optimizations (e.g., removing theif (Current.Key is null) branch) would be purely cosmetic for your purposes.

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

Reviewers

@coderabbitaicoderabbitai[bot]coderabbitai[bot] left review comments

Assignees

No one assigned

Projects

None yet

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

1 participant

@snakefoot

[8]ページ先頭

©2009-2025 Movatter.jp