Skip to content

Support nullable where TKey : notnull for dictionary#6068

Merged
snakefoot merged 1 commit intoNLog:devfrom
snakefoot:net8_fixes
Dec 29, 2025
Merged

Support nullable where TKey : notnull for dictionary#6068
snakefoot merged 1 commit intoNLog:devfrom
snakefoot:net8_fixes

Conversation

@snakefoot
Copy link
Contributor

No description provided.

@coderabbitai
Copy link

coderabbitai bot commented Dec 29, 2025

Walkthrough

Adds conditional non-null generic constraints on several internal collections, tightens null handling for layout and internal logging, and updates some build-condition symbols from NET to NET9_0_OR_GREATER. No public API signature changes.

Changes

Cohort / File(s) Summary
Generic type constraints
src/NLog/Internal/MruCache.cs, src/NLog/Internal/ThreadSafeDictionary.cs, src/NLog/Internal/SortHelpers.cs
Added conditional where TKey : notnull constraints under `#if NETSTANDARD2_1_OR_GREATER
Conditional compilation adjustments
src/NLog/Internal/StringBuilderExt.cs, src/NLog/Logger-V1Compat.cs, src/NLog/Logger.cs
Replaced NET with NET9_0_OR_GREATER in specific preprocessor guards to change which targets enable optimized/attribute paths (span-based formatting and overload resolution attributes).
Null-safety & guard
src/NLog/Common/InternalLogger.cs, src/NLog/Layouts/Typed/Layout.cs
Short-circuited file-writing when internal log file path is null/empty; added null-coalescing to Layout<T>.ToString() to return "null" if inner ToString() yields null.

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~20 minutes

Poem

🐰 In code I hop where constraints are tight,

Keys must not vanish into night.
Guards skip the write when paths are bare,
NET9 wakes optimizations rare,
A happy rabbit nibbles bugs with care.

Pre-merge checks and finishing touches

❌ Failed checks (1 warning, 1 inconclusive)
Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 50.00% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
Description check ❓ Inconclusive No description was provided by the author, making it impossible to assess whether the description relates to the changeset. Add a pull request description explaining the purpose of adding notnull constraints and the rationale for the changes.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title accurately describes the main change—adding notnull constraint support for dictionary keys across multiple related files.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

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

❤️ Share

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

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/NLog/Logger-V1Compat.cs (1)

2043-2055: Fix typo in preprocessor symbol: NETNET9_0_OR_GREATER should be NET9_0_OR_GREATER

At the Info(IFormatProvider? formatProvider, string message, string? argument) overload, the #if directive contains a typo:

#if NETSTANDARD2_1_OR_GREATER || NETNET9_0_OR_GREATER

NETNET9_0_OR_GREATER is not a valid SDK-defined symbol and should be NET9_0_OR_GREATER to match all other occurrences in the codebase.

-#if NETSTANDARD2_1_OR_GREATER || NETNET9_0_OR_GREATER
+#if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER
 [OverloadResolutionPriority(-1)]
 #endif
src/NLog/Logger.cs (1)

571-604: Remove params from ReadOnlySpan overloads – type mismatch with WriteToTargetsWithSpan

Both new overloads declare args as params ReadOnlySpan<object?>, which makes the parameter type ReadOnlySpan<object?>[] (array of spans). However, they pass args directly to WriteToTargetsWithSpan, which expects a single in ReadOnlySpan<object?>:

private void WriteToTargetsWithSpan(..., string message, in ReadOnlySpan<object?> args)

This is a compile-time type mismatch. Remove params from both overloads (lines 579 and 596) to make the parameter a single span:

- public void Log(LogLevel level, [Localizable(false)][StructuredMessageTemplate] string message, params ReadOnlySpan<object?> args)
+ public void Log(LogLevel level, [Localizable(false)][StructuredMessageTemplate] string message, ReadOnlySpan<object?> args)

and

- public void Log(LogLevel level, Exception? exception, [Localizable(false)][StructuredMessageTemplate] string message, params ReadOnlySpan<object?> args)
+ public void Log(LogLevel level, Exception? exception, [Localizable(false)][StructuredMessageTemplate] string message, ReadOnlySpan<object?> args)
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d88f1d3 and 0dd369a.

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

Applied to files:

  • src/NLog/Internal/StringBuilderExt.cs
🧬 Code graph analysis (1)
src/NLog/Layouts/Typed/Layout.cs (2)
src/NLog/Layouts/Typed/ValueTypeLayoutInfo.cs (2)
  • ToString (250-253)
  • ToString (272-275)
src/NLog/Targets/TargetWithContext.cs (1)
  • ToString (727-730)
🔇 Additional comments (7)
src/NLog/Common/InternalLogger.cs (1)

495-496: LGTM! Good defensive guard.

This early return prevents file operations when _logFile is null or empty, avoiding unnecessary work (string construction, lock acquisition) and potential exceptions. While the subscription logic (line 133-134) already guards against null/empty paths, this additional check protects against edge cases such as race conditions or path expansion returning an invalid result.

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

145-161: Private helper constraint aligns correctly with dictionary usage

Conditionally adding where TKey : notnull on CreateBucketDictionaryWithValue under NETSTANDARD2_1_OR_GREATER || NET is consistent with the rest of the file’s bucket APIs and BCL Dictionary<TKey, TValue> expectations; no issues here.

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

415-441: NET9_0_OR_GREATER extension of the optimized path looks correct

Switching the 4-digit zero-padding optimization to #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER preserves existing behavior on older TFMs and simply enables the single Append path on NET 9+; the conditional structure stays well-formed.

src/NLog/Logger-V1Compat.cs (1)

2747-2752: Warn(float, IFormatProvider) overload attribute condition is consistent

The added OverloadResolutionPriority(-1) behind #if NETSTANDARD2_1_OR_GREATER || NET9_0_OR_GREATER for Warn(IFormatProvider? formatProvider, string message, float argument) matches the pattern used for other numeric overloads and looks correct.

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

41-45: Internal dictionary constraint tightening is reasonable

Adding where TKey : notnull for ThreadSafeDictionary<TKey, TValue> under NETSTANDARD2_1_OR_GREATER || NET brings its key constraint in line with Dictionary<TKey, TValue> on newer TFMs and matches how keys are already used internally; looks good as long as call sites are updated to use non-nullable key types where necessary.

src/NLog/Layouts/Typed/Layout.cs (1)

376-376: LGTM! Defensive null-safety improvement consistent with codebase patterns.

The addition of null-coalescing operators ensures ToString() never returns null, aligning with similar patterns in ValueTypeLayoutInfo and TargetWithContext. While the underlying implementations of _layoutValue types (LayoutTypeValue and FuncMethodValue) already include fallbacks to GetType().ToString(), this explicit guard is good defensive programming.

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

42-44: LGTM! Appropriate compile-time constraint for dictionary key safety.

The conditional notnull constraint on TKey improves type safety by enforcing at compile-time that dictionary keys cannot be nullable reference types. The condition NETSTANDARD2_1_OR_GREATER || NET is correct, as the notnull constraint is available in C# 8.0+ (supported by .NET Standard 2.1 and all .NET Core 3.0+ / .NET 5+ versions).

Since this is an internal class, there's no public API impact, and the constraint aligns with dictionary best practices (null keys would fail at runtime anyway).

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 0

🧹 Nitpick comments (1)
src/NLog/Common/InternalLogger.cs (1)

493-507: Make _logFile check and usage consistent under concurrency by using a local

The new null/empty guard is good, but _logFile can still change between the check and File.AppendText(_logFile) if LogFile is modified concurrently. Capturing it once avoids a stale read and makes the guard fully effective.

Proposed refinement
-        if (string.IsNullOrEmpty(_logFile))
-            return;
+        var logFile = _logFile;
+        if (string.IsNullOrEmpty(logFile))
+            return;
@@
-                    using (var textWriter = File.AppendText(_logFile))
+                    using (var textWriter = File.AppendText(logFile))
📜 Review details

Configuration used: Repository UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 0dd369a and 8fa8885.

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

Applied to files:

  • src/NLog/Internal/StringBuilderExt.cs
  • src/NLog/Layouts/Typed/Layout.cs
🧬 Code graph analysis (1)
src/NLog/Layouts/Typed/Layout.cs (1)
src/NLog/Layouts/Typed/ValueTypeLayoutInfo.cs (2)
  • ToString (250-253)
  • ToString (272-275)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
  • GitHub Check: task-list-completed
🔇 Additional comments (5)
src/NLog/Layouts/Typed/Layout.cs (1)

373-377: Non-null ToString() result for both fixed and dynamic layouts

Returning "null" when FixedObjectValue or _layoutValue.ToString() is null makes Layout<T>.ToString() consistently non-null and aligns with similar patterns (e.g., ValueTypeLayoutInfo). Looks good.

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

415-441: Preprocessor guard for 4-digit append optimization looks consistent

The Append4DigitsZeroPadded conditional now enables the single-Append path only for NETSTANDARD2_1_OR_GREATER and NET9_0_OR_GREATER, falling back to the per-digit appends otherwise. This is behaviorally safe and consistent with the PR’s target-conditional pattern.

src/NLog/Logger-V1Compat.cs (2)

2041-2056: OverloadResolutionPriority condition aligned for Info(IFormatProvider, string, string?)

The conditional around [OverloadResolutionPriority(-1)] now matches the other Info overloads, ensuring consistent overload resolution behavior on the intended targets. No further changes needed.


2747-2758: OverloadResolutionPriority condition aligned for Warn(IFormatProvider, string, float)

Same here: the preprocessor condition for applying [OverloadResolutionPriority(-1)] is now consistent with neighboring Warn overloads, fixing the prior symbol typo.

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

41-58: Conditional where TKey : notnull on MruCache is appropriate

Adding where TKey : notnull for newer targets tightens key nullability in line with Dictionary<TKey, TValue> expectations, without affecting runtime behavior on older TFMs. Looks good.

@sonarqubecloud
Copy link

@snakefoot snakefoot merged commit 35dde21 into NLog:dev Dec 29, 2025
5 of 6 checks passed
@snakefoot snakefoot added this to the 6.1.0 milestone Jan 31, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant