- Notifications
You must be signed in to change notification settings - Fork1.4k
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
base:dev
Are you sure you want to change the base?
Uh oh!
There was an error while loading.Please reload this page.
Conversation
coderabbitaibot commentedNov 22, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
WalkthroughReplace 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
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 endEstimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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.
Example instruction:
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. Comment |
There was a problem hiding this 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
📒 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)
Uh oh!
There was an error while loading.Please reload this page.
ea6180a to92ffecbCompareThere was a problem hiding this 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/_messagePropertiesCountandMessagePropertiesexposureThe new array+count representation and
ResetMessagePropertiespipeline look consistent, but there are a couple of subtle invariants that are implicit:
MessagePropertiesonly 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 onMessagePropertiesto 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 thatMessagePropertiesonly exposes “tight” arrays. Otherwise, consider returning a trimmed copy or anArraySegment-style view to avoid silently hiding the small inline set.ResetMessagePropertiesassumesnewMessagePropertiesCountis ≤newMessageProperties?.Lengthand matches the populated prefix; today all call sites appear to passLength, but adding aDebug.Assertwould 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 guardrailsThe
this[object key]setter,Add(object key, object? value), andSkipDictionaryAllocationoptimization all look logically consistent:
- They only use the inline array when
_eventPropertiesis null and there is capacity (_messagePropertiesCount < _messageProperties.Length).- They correctly update existing entries in-place and append new ones with
CaptureType.Unknown.- Bounds are guarded via the
SkipDictionaryAllocationcheck before indexing intomessageProperties[_messagePropertiesCount++].To make this easier to maintain, you might:
- Add a quick
Debug.Assert(_messagePropertiesCount <= (_messageProperties?.Length ?? 0));inSkipDictionaryAllocationor after writes, to lock in the intended invariant.- Consider a brief comment on why
CaptureType.Unknownis used here (distinguishing ad-hoc event properties from template-derived ones and tying into theresetMessagePropertieslogic).Functionality-wise this path looks good.
Also applies to: 237-257, 268-271
372-413:Lookup and uniqueness heuristics for message properties
TryLookupMessagePropertyValueandVerifyUniqueMessageTemplateParametersFastare well-shaped for the new array+count model:
- The
> 10threshold for switching to dictionary-based lookup when only_messagePropertiesexist 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
<= 10parameters.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:MessagePropertiesfast-path now only kicks in for “tight” arraysThe new implementation of
HasImmutableProperties:
- Uses
properties.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 the
PropertyDictionaryEnumeratorotherwise.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 == 3and_messagePropertiesCountis 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 a
MessagePropertiesSpan-like view over the first_messagePropertiesCountitems 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 reparseThe
MessageTemplateParametersgetter now:
- Prefers
_properties.MessagePropertieswhenLength > 0, wrapping it in aMessageTemplateParametersinstance.- Falls back to parsing
_messageand_parameterswhenParametersis non-null.- Returns
MessageTemplateParameters.Emptyotherwise.Because
PropertiesDictionary.MessagePropertiesonly 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 when
MessageTemplateParametersis accessed.- For constructor-supplied
MessageTemplateParameter[]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 from
PropertiesDictionary.MessagePropertiesso this getter can reuse the captured parameters in the small case too.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 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+InsertMessagePropertiesIntoEmptyDictionarybehavior with mixed event vs. message propertiesThe reset logic for message properties and the
InsertMessagePropertiesIntoEmptyDictionaryhelper 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 have
CaptureType.Unknown,BuildEventPropertieswill mark them as non-message properties (IsMessageProperty = false) and (viaresetMessageProperties) can cause_messagePropertiesto be cleared in theEventPropertiesgetter. 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.- In
InsertMessagePropertiesIntoEmptyDictionary, 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 onIsMessagePropertybeing correct for all existing entries.Given how many tests in
LoggerTestsexercise 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_messagePropertiesCountandIsMessagePropertyThe updated
PropertyDictionaryEnumeratorfixes 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
_eventPropertiesexists,MoveNextValidMessageParameteronly yields entries whose dictionary value hasIsMessageProperty == true, andMoveNextValidEventPropertyyields only non-message entries, preserving the “message properties first, then remaining event properties” contract.- The
Resetimplementation resets_messagePropertiesIndexbased on the current_messagePropertiesCount, so reuse after aReset()is safe.The only behavioral nuance is that if
_eventPropertiesis createdafter an enumerator instance is constructed, that enumerator will never see the new dictionary entries (its_eventEnumeratorwas 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 notingThe interplay between:
Message/Parameterssetters,ResetMessageTemplateParameters(),- and
ResetFormattedMessage(bool parseMessageTemplateParameters)looks coherent:
- When there are existing parsed message-template parameters (
HasMessageTemplateParametersis 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-
Unknowncapture types) are preserved whenMessageorParameterschange unless you explicitly clear/reset them.Two edge behaviors to re-check against tests:
- For events created with constructor-supplied
IList<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.- For the ≤3-inline optimization,
_properties.MessageProperties.Lengthwill be 0, soResetMessageTemplateParametersreturnstrue, allowing parsing when you later changeMessage/Parameterseven 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/CreatePropertiesInternalintegration withPropertiesDictionaryThe migration of these helpers to
MessageTemplateParameter[]?seems correct:
- New events with parsed template parameters go through
CreatePropertiesInternal(templateParameters)which constructsPropertiesDictionary(templateParameters)and letsPropertiesDictionaryown_messagePropertiesand_messagePropertiesCount.- Subsequent passes from the formatter with updated parameters hit the
else if (templateParameters != null)branch inTryCreatePropertiesInternaland delegate toproperties.ResetMessageProperties(templateParameters, templateParameters.Length), which keeps existing event properties and refreshes only the message-derived portion.- The
templateParameters is null && HasMessageTemplateParametersbranch keeps the old “lazy capture from message” semantics.One thing to keep in mind is that
TryCreatePropertiesInternalnow silently ignores calls with a non-null but empty parameter array when_propertiesis 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 thenullreturn explicitly.Overall, the integration with the new
PropertiesDictionaryAPI looks good.Also applies to: 431-445
699-719:Immutability checks updated to array model; semantics preserved
IsLogEventThreadAgnosticImmutableandHasImmutablePropertieshave been updated to use:
properties.MessageProperties.Lengthinstead of a listCount, and- the new
HasImmutableProperties(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 checks
properties.Count == _parameters?.Length && properties.Count == properties.MessageProperties.Length, which aligns with the newMessagePropertiessemantics.- When that doesn’t hold, you still iterate all properties (message + event) via
GetPropertyEnumerator()and runIsSafeToDeferFormattingas before.Given the new invariants in
PropertiesDictionary, 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.Messagelook correct*The revised assertions for
ConditionalTrace*/ConditionalDebug*tests:
- Switch expected strings from hard-coded full diagnostic text to interpolated strings that append
argException.Messageafter the formatted error description.- Still respect culture-dependent formatting of numeric arguments (
404,5etc.) viaNLCulture.- Keep the overall
${logger}|${message}${exception:message}layout expectation intact.The use of string interpolation plus
argException.Messagealigns 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
There was a problem hiding this 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.The
oldMessagePropertiesparameter 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 have
CaptureType.Unknownworks 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
📒 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 use
argException.Messageinterpolation 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 from
IList<MessageTemplateParameter>toMessageTemplateParameter[]with explicit count tracking. The constructor correctly delegates toResetMessagePropertiesto ensure the array and count are initialized together.
497-626:LGTM - Enumerator correctly handles array bounds.The
PropertyDictionaryEnumeratorproperly 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:
- Index bounds are checked against
_messagePropertiesCount(lines 592, 602)- Index is only set to values within valid range (lines 598, 606)
- 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.
259b573 tod9b3a62CompareThere was a problem hiding this 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.When
prepareForInsertis false (line 172), the dictionary capacity equalsmessagePropertiesCountwithout extra room. If the caller immediately inserts additional properties after promotion, the dictionary may need to resize.In
ResetMessagePropertiesline 137,prepareForInsertis 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
📒 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 accepts
MessageTemplateParameter[]?and correctly uses.Lengthfor null-safety checks and passes the explicit count toResetMessageProperties.
431-445:LGTM: Constructor accepts array-based parameters.The signature change from
IListto 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 uses
MessageProperties.Lengthinstead ofCountfor the array-based implementation.
720-746:Nice improvement: foreach loop for iteration.Switching from index-based loop to
foreachon 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 uses
messageProperties.Lengthto check for empty arrays and properly accesses the first element'sCaptureType.
468-481:Verify MessageProperties behavior when array is partially filled.Line 468 checks
MessageProperties.Length > 0. However, in PropertiesDictionary.cs line 124, theMessagePropertiesproperty 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.Lengthis 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._messagePropertiesCountdirectly.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 of
MessageTemplateParameter[]? _messagePropertiesandint _messagePropertiesCountenables the optimization to avoid dictionary allocation for ≤3 properties. Constructor correctly initializes viaResetMessagePropertieswith the explicit count.
108-122:LGTM: GetEventProperties handles migration from array to dictionary.The logic correctly checks
_messagePropertiesCountfor emptiness and resets the array-backed storage whenresetMessagePropertiesis 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 > 0will see 0 even though properties existVerify 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 accepts
MessageTemplateParameter[]?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 uses
oldMessagePropertiesCountinstead of array length, ensuring only valid elements are processed.
198-232:LGTM: Indexer and Add optimized for array-backed storage.Both methods check
SkipDictionaryAllocation()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_messagePropertiesCountto 0.
349-390:LGTM: Lookup uses explicit count for bounds.
TryLookupMessagePropertyValuecorrectly checks_messagePropertiesCountfor 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 uses
parameterList.Lengthfor 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 on
CaptureType.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.The
PropertyDictionaryEnumeratorcorrectly:
- Initializes
_messagePropertiesIndexbased on_messagePropertiesCount > 0(line 508)- Uses
_messagePropertiesCountfor 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 uses
Interlocked.CompareExchangefor_eventProperties. This inconsistency is notable.However, the codebase provides no explicit concurrency guarantees for
PropertiesDictionaryorLogEventInfo. TheResetMessagePropertiesmethod and other code paths lack locks entirely, suggesting single-threaded design. Yet the publicIDictionaryinterface andInterlockedusage at lines 114 and 436 (LogEventInfo) create ambiguity about intended thread-safety semantics.Confirm whether
PropertiesDictionaryinstances should support concurrentProperties[key]mutations from multiple threads. If yes, synchronize_messagePropertiesconsistently. If single-threaded, the concerns are moot and the review warrants dismissal.
d9b3a62 tobe601ecComparesnakefoot commentedNov 23, 2025 • edited
Loading Uh oh!
There was an error while loading.Please reload this page.
edited
Uh oh!
There was an error while loading.Please reload this page.
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. |
be601ec to26d9b6cCompare26d9b6c tobf06f98CompareThere was a problem hiding this 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
📒 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
Uh oh!
There was an error while loading.Please reload this page.
Uh oh!
There was an error while loading.Please reload this page.
bf06f98 to8937426CompareThere was a problem hiding this 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.The
MessagePropertiesgetter returns_messagePropertieswhen_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 onMessageTemplateParameterswill 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
📒 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 from
IList<MessageTemplateParameter>?toMessageTemplateParameter[]?and corresponding logic updates (usingLengthinstead ofCount, passingtemplateParameters.LengthtoResetMessageProperties) align with the array-based refactor.
431-445:LGTM!The signature update to
MessageTemplateParameter[]?is consistent with the broader refactor.
468-471:LGTM!The getter correctly uses
Lengthfor the array-basedMessageProperties.
715-716:LGTM!The update to use
MessageProperties.Lengthis correct.
724-727:LGTM!The changes to use
Lengthandforeachover the array are appropriate for the new array-based storage.
844-845:LGTM!The updates to use
MessageProperties.Lengthare 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!The
IsEmptycheck 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 reworked
ResetMessagePropertiesmethod correctly handles the transitions between array-backed and dictionary-backed storage, including name collision checks and property removal.
155-164:LGTM!The updated iteration using
oldMessagePropertiesCountis correct.
166-180:LGTM!The updated initialization logic correctly uses the explicit count parameter for capacity calculations and insertion.
193-193:LGTM!The
Countproperty correctly returns_messagePropertiesCountwhen 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 via
SkipDictionaryAllocationcorrectly ensure the array has space.
223-232:LGTM!The
Addmethod correctly uses the same optimization path as the indexer setter, withallowUpdate=falseto throw on duplicate keys.
408-411:LGTM!The
SkipDictionaryAllocationguard correctly determines when array-backed storage can be used without allocating a dictionary.
241-246:LGTM!The
Clearmethod correctly resets all storage fields.
329-344:LGTM!The
TryGetValuecorrectly delegates toTryLookupMessagePropertyValuewhen 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 uses
Lengthfor 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 on
CaptureType.
501-506:LGTM!The enumerator constructor correctly initializes based on
_messagePropertiesCount.
508-523:LGTM!The
Currentproperty correctly accesses the array at the current index.
525-540:LGTM!The
CurrentParameterproperty correctly constructsMessageTemplateParameterfrom both array-backed and dictionary-backed storage.
542-558:LGTM!The
CurrentPropertyproperty correctly handles both storage types.
562-575:LGTM!The
MoveNextlogic correctly traverses message properties first, then event properties.
577-585:LGTM!The helper correctly skips message properties in the event dictionary.
587-612:LGTM!The
MoveNextValidMessageParametercorrectly iterates through the array-backed message properties up to_messagePropertiesCount.
619-623:LGTM!The
Resetmethod now correctly re-initializes_eventEnumeratorfrom_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.
Uh oh!
There was an error while loading.Please reload this page.
03bc819 to69d2b95CompareThere was a problem hiding this 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 writes
messageProperties[_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 input
messagePropertiesarray 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
📒 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 from
CounttoLengthchecks (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 from
IList<MessageTemplateParameter>toMessageTemplateParameter[]with explicit_messagePropertiesCount(lines 82-83) enables the optimization. The constructor (lines 89-95) correctly initializes viaResetMessagePropertieswhen the array is non-empty.
124-124:MessageProperties guard logic is dense but correct.The four-part guard correctly prevents exposing event properties (those with
CaptureType.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) and
Add(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.The
resetMessagePropertiesout parameter (lines 115-119, 182) that nulls_messagePropertiesduring 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 toTryLookupMessagePropertyValuewhen the dictionary hasn't been allocated. The linear search (lines 351-392) uses_messagePropertiesCountfor bounds and includes the optimization to promote to dictionary when count exceeds 10 (line 359).
506-628:Enumerator correctly iterates the populated array portion.
PropertyDictionaryEnumeratoruses_messagePropertiesCountthroughout (lines 510, 519, 536, 553, 594-616) to iterate only the populated slots. TheReset()method (626-627) properly reinitializes both_messagePropertiesIndexand_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.
69d2b95 toa9f0f23Comparea9f0f23 to3fb00bdCompareThere was a problem hiding this 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.The
MessagePropertiesproperty accesses_messageProperties[0].CaptureTypewhen 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.
AddOrUpdateMessagePropertieswrites 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
📒 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 from
IList<MessageTemplateParameter>toMessageTemplateParameter[]is correctly applied. The conditional checks now useLengthinstead ofCount, andResetMessagePropertiesis 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 over
messagePropertiesat 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 of
SmallArraySize = 3,_messagePropertiesarray, and_messagePropertiesCounttracking provides a clean foundation for the small-array optimization. The constructors correctly initialize the array-backed storage and check againstSmallArraySizebefore 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, andInitializeEventPropertiesDictionarycorrectly use_messagePropertiesCountfor 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 and
Addmethod 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.
TryGetValuedelegates toTryLookupMessagePropertyValuewhen_eventPropertiesis null, and the lookup correctly uses_messagePropertiesCountas 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 have
CaptureType.Unknown(event properties added via indexer) and setresetMessagePropertiesto 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 usesmessagePropertiesCountas the boundary.
502-631:LGTM: PropertyDictionaryEnumerator correctly tracks array count.The enumerator fields (lines 505–506) include
_messagePropertiesIndexinitialized to -1 when_messagePropertiesCount > 0(line 512).MoveNextValidMessageParameter(lines 594–619) uses_dictionary._messagePropertiesCountas the iteration boundary (lines 596, 607), andReset(lines 626–630) properly reinitializes both_messagePropertiesIndexand_eventEnumerator.tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (4)
41-73:LGTM: Create benchmarks correctly exercise array-backed storage and dictionary promotion.The
CreateOneProperty,CreateTwoProperty,CreateThreeProperty, andCreateFourPropertybenchmarks instantiatePropertiesDictionaryand 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.The
LookupOneProperty,LookupTwoProperty,LookupThreeProperty, andLookupFourPropertybenchmarks add properties and loop 1000 times callingTryGetValuefor 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.The
NotFoundLookup*benchmarks loop 1000 times searching for a missing key ("Hello5"). This exercises the early-exit paths inTryLookupMessagePropertyValueand 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.The
Enumerate*benchmarks loop 1000 times callingGetPropertyEnumeratorand 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).
3fb00bd tof8e3d6cCompareThere was a problem hiding this 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 consistentThe new layout hits the PR goals without obvious regressions:
SmallArraySize = 3plus the updated constructors let you avoid allocating aDictionaryfor the common “≤3 properties” case while still honoring an explicitinitialCapacityfor larger cases.MessagePropertiesonly exposes_messagePropertieswhen it’s not the 3-slot inline buffer withCaptureType.Unknownin the first slot, so event-only inline properties no longer leak intoLogEventInfo.MessageTemplateParameters.ResetMessagePropertiescorrectly 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+InsertMessagePropertiesIntoEmptyDictionarymaintainIsMessagePropertyconsistently and useresetMessagePropertiesto 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 near
SmallArraySizeorMessagePropertiesexplaining “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 preconditionThe combination of
SkipDictionaryAllocation,TryLookupMessagePropertyValue, andAddOrUpdateMessagePropertiesbehaves as intended:
- Inserts and indexer sets stay in the inline buffer while
_eventProperties == nulland_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 and
IgnoreCasePropertyKeykeys, while non-string keys always go straight to the dictionary path, matching how inserts are routed.TryLookupMessagePropertyValueswitches to dictionary-backed lookup once there are more than 10 message properties, which lines up with the uniqueness fast-path threshold.
AddOrUpdateMessagePropertiesdepends 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
📒 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 overheadThe two Render overloads correctly mirror the previous IList-based logic while switching to
MessageTemplateParameter[]?:
- Positional-only templates stay on the fast path without allocating the parameter array.
- Mixed/templated cases still “rewind” and rebuild with sequential
holeIndex, and the span overload usessb.ClearBuilder()appropriately.- Using the same
holeIndexaccounting in both overloads ensuresVerifyMessageTemplateParameterssees 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
VerifyMessageTemplateParametersnow trims the backing array toholeIndex, eliminating unused/default slots when there are fewer holes thanparameters.Length. This avoids leaking dummy entries intoPropertiesDictionaryand keeps_messagePropertiesCountaligned 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 storageThis test nicely validates that:
MessageTemplateParametersfor a positional template are derived from_parameters(and markedCaptureType.Normal), not fromProperties.- Inline
Properties["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 refactorThe updated
PropertyDictionaryEnumeratorlogic looks consistent and fixes earlier pitfalls:
_messagePropertiesIndexis initialized from_messagePropertiesCount, so message-template parameters (when present) are always yielded first.MoveNextValidMessageParameterrespects_messagePropertiesCountand, when a dictionary exists, only advances to entries whose backingPropertyValueis markedIsMessageProperty, avoiding duplication with the event-property phase.MoveNextValidEventPropertythen skips any dictionary entries flagged as message properties, yielding only true event properties.Reset()restores both_messagePropertiesIndexand_eventEnumeratorfrom the dictionary’s current state, soKeyCollection/ValueCollectionenumerations after a reset see the same sequence as an initial enumerator.Given this,
Count,Keys,Values, andforeachover 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 scenariosThe 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, using
GetPropertyEnumerator()so the custom enumerator is also covered.- The simple
ifchecks 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.
f8e3d6c to924795eCompareThere was a problem hiding this 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 into
messageProperties[_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
📒 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 to
AddOrUpdateMessageProperties, enabling in-place updates for the small-array optimization. TheallowUpdate: trueparameter 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 duplicate
MessageTemplatePositionaltest methods intests/NLog.UnitTests/LogMessageFormatterTests.csrequires direct file inspection to confirm or refute.To complete this verification:
- Check if multiple
MessageTemplatePositionalmethod 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
924795e to219f411CompareThere was a problem hiding this 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
MessagePropertiesandResetMessagePropertiestogether ensure that:
- For true message-template parameters,
_messagePropertiesis a tight array (Length == count) with non-UnknownCaptureType, andMessagePropertiesreturns it.- For the small-array event-only path:
_messagePropertiesis a fixed 3-slot array withCaptureType.Unknownin slot 0.MessagePropertiesreturnsArrayHelper.Empty<MessageTemplateParameter>(), soLogEventInfo.MessageTemplateParametersnever sees these as template metadata.This addresses the earlier concern about leaking event-only slots via
MessagePropertieswhile keeping the accessor cheap. A minor optional refinement would be to also guard on_messagePropertiesCount > 0in 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
📒 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 semanticsThe migration from IList to
MessageTemplateParameter[]inTryCreatePropertiesInternal,CreatePropertiesInternal,HasMessageTemplateParameters,MessageTemplateParameters, and the immutability helpers appears internally consistent:
TryCreatePropertiesInternalcorrectly:
- Creates
_propertiesfrom a non-emptytemplateParametersarray.- Resets message properties on an existing dictionary via
ResetMessageProperties(templateParameters, templateParameters.Length).- Defers to
HasMessageTemplateParametersonly whentemplateParametersis null, avoiding unnecessary work.
CreatePropertiesInternalusesPropertiesDictionary(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 > 0prevents repeated template probing once formatting is done, and the use ofILogMessageFormatter.HasProperties(this)matches the intended “cheap probe, full parse later” pattern.
MessageTemplateParametersnow correctly depends on_properties?.MessagePropertiesreturning the filteredMessageTemplateParameter[](which, per the newPropertiesDictionary.MessagePropertiesimplementation, excludes event-only small-array slots), and only falls back to reparsing_message+_parameterswhen no captured metadata exists.
IsLogEventThreadAgnosticImmutableandHasImmutablePropertiesuseproperties.MessageProperties.Lengthrather than a listCount, and the “fast path”properties.Count == _parameters?.Length && properties.Count == properties.MessageProperties.Lengthstill correctly identifies the pure message-template case.
ResetMessageTemplateParameters’ final conditionreturn _properties.MessageProperties.Length == 0;nicely leverages the new filtering logic onMessagePropertiesto 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-backed
PropertiesDictionarybehavior.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 soundUsing
SmallArraySize = 3with_messageProperties+_messagePropertiesCount, and only allocating_eventPropertieswheninitialCapacity > SmallArraySizeor on promotion, matches the PR objective of avoiding aDictionaryfor ≤3 properties:
PropertiesDictionary(MessageTemplateParameter[]? messageParameters)andPropertiesDictionary(int initialCapacity)correctly steer between array and dictionary paths.IsEmptyandCount => _eventProperties?.Count ?? _messagePropertiesCountkeep 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
GetEventPropertiesandInitializeEventPropertiesDictionarycorrectly:
- Promote from the small-array representation by seeding the
Dictionary<object, PropertyValue>from_messagePropertieswith an appropriate capacity (messagePropertiesCountormessagePropertiesCount + 2whenprepareForInsert).- Use
resetMessagePropertiesfromInsertMessagePropertiesIntoEmptyDictionaryto distinguish:
- “True” message-template properties (
IsMessageProperty = true, capture type ≠Unknown).- Small-array event-only slots (all
CaptureType.Unknown), which are migrated and then cleared via_messageProperties = null; _messagePropertiesCount = 0;.
ResetMessagePropertiesthen builds on this by:
- Creating a fresh dictionary when needed (existing event properties, duplicates, or old message properties present).
- Removing prior template-derived entries via
RemoveOldMessageProperties.- Re-inserting new message-template parameters with correct
IsMessagePropertyflags.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 scenariosThe new code in
TryLookupMessagePropertyValue,AddOrUpdateMessageProperties, andSkipDictionaryAllocationlooks 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.SkipDictionaryAllocationensures:
- Up to 3 string keys are stored in the
_messagePropertiesarray without overflowing (_messagePropertiesCount < _messageProperties.Length).- Any non-string key, or the 4th+ property, immediately promotes to the dictionary path.
AddOrUpdateMessageProperties:
- Reuses the array slot when
allowUpdateis 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 calling
GetEventProperties(true)and then adding/updating in the dictionary.I don’t see an
IndexOutOfRangeExceptionpath 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
VerifyUniqueMessageTemplateParametersFastandInsertMessagePropertiesIntoEmptyDictionarycover the tricky cases:
VerifyUniqueMessageTemplateParametersFast:
- Returns
truefor 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:
- Uses
resetMessagePropertiesto detect arrays whereall entries areCaptureType.Unknown(event-only), marking those dictionary values as non-message properties (IsMessageProperty = false).- On
ArgumentExceptionfromDictionary.Add, only rewrites the name for clashes where the existing entry is a message property, usingGenerateUniquePropertyNameand updating theMessageTemplateParameterin-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 correctlyThe updated
PropertyDictionaryEnumerator:
- Initializes
_messagePropertiesIndexbased on_dictionary._messagePropertiesCountand_eventEnumeratorfrom_dictionary._eventProperties.- Uses:
MoveNextValidMessageParameterto emit message-template entries first:
- Iterates over
_messagePropertiesCount.- For dictionary-backed cases, only yields entries whose
PropertyValue.IsMessagePropertyistrue, avoiding duplication.MoveNextValidEventPropertyto then enumerate only non-message entries from the dictionary (IsMessageProperty == false).Current,CurrentParameter, andCurrentPropertyconsistently index into_dictionary._messagePropertiesor_eventEnumeratordepending on_messagePropertiesIndex.HasValue.Reset()now restores both_messagePropertiesIndexand_eventEnumeratorto their initial states, so post-reset iteration covers the same items as the original enumeration.Given
Countis_eventProperties?.Count ?? _messagePropertiesCount, the enumerator will yield exactlyCountelements 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 behaviorThis test nicely validates the key invariant for positional templates:
MessageTemplateParametersremains:
- Valid and positional.
- Aligned with
logEventInfo.Parameters("Foo",123,Guid), even afterProperties["0"],["1"],["2"]are overridden.Propertiesreflects only the overridden values (HelloWorld1/2/3) with aCountof 3.That matches the new design where message-template metadata stays intact in
_messageProperties, while event properties (including overrides) flow throughPropertiesDictionaryvia the small-array/dictionary paths.tests/NLog.Benchmarks/Internal/PropertiesDictionaryBenchmark.cs (1)
39-345:Benchmarks effectively cover small-array vs. dictionary behaviorThe benchmark suite looks well-structured:
Create*Propertyscenarios measure construction + initial inserts across 1–5 keys, naturally driving the small-array path for ≤3 and promotion for ≥4.Lookup*PropertyandNotFoundLookup*Propertyperform 1000 iterations against hot dictionaries, which is appropriate for timing steady-state lookups and miss patterns.Enumerate*PropertyusesGetPropertyEnumerator()andMoveNext()in a tight loop, which directly exercises the updatedPropertyDictionaryEnumeratorover both small-array and dictionary-backed cases.I don’t see correctness issues here; any micro-optimizations (e.g., removing the
if (Current.Key is null)branch) would be purely cosmetic for your purposes.



Uh oh!
There was an error while loading.Please reload this page.
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).