Repository navigation
Conversation
WalkthroughA new Changes
Estimated code review effort🎯 3 (Moderate) | ⏱️ ~25 minutes Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (4)
src/NLog/LayoutRenderers/ShortDateLayoutRenderer.cs (1)
92-92: Clean up the inline comment and add a focused regression case.The behavior is correct, but
Utc?Locallooks like a malformed arrow. Since this fixes anUnspecified-kind date shift, please consider adding a test that setsLogEventInfo.TimeStamptoDateTimeKind.UnspecifiedwithUniversalTime=falseand asserts no conversion.🧹 Proposed comment cleanup
- timestamp = timestamp.ToLocalTime(); // Only convert Utc?Local; Local/Unspecified are already local + timestamp = timestamp.ToLocalTime(); // Only convert UTC to Local; Local/Unspecified are already local🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/NLog/LayoutRenderers/ShortDateLayoutRenderer.cs` at line 92, Update the inline comment on the timestamp conversion in ShortDateLayoutRenderer.cs to be clear (e.g., "Only convert when Kind == Utc; Local and Unspecified are already local") instead of the malformed "Utc?Local", and add a focused regression unit test that constructs a LogEventInfo with TimeStamp having DateTimeKind.Unspecified and UniversalTime=false then renders with ShortDateLayoutRenderer (or calls the method that contains timestamp = timestamp.ToLocalTime()) and asserts the timestamp is not converted (remains treated as local). Ensure the test references the ShortDateLayoutRenderer rendering method so it fails if Unspecified gets converted.src/NLog/LayoutRenderers/DateTimeOffsetLayoutRenderer.cs (1)
69-69: Consider markingFormatnullable or asserting non-null.
Formatis typed as non-nullablestringbut can be set tonullvia XML/config or reflection, in which caseDateTimeOffset.ToString((string)null, provider)falls back to the"G"format — which silently ignores the documented defaultyyyy/MM/dd HH:mm:ss.fff. If the project uses nullable reference types, annotating asstring?(or guarding inGetValue/Append) would make the contract explicit. Not a blocker since this mirrors existing date renderers.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/NLog/LayoutRenderers/DateTimeOffsetLayoutRenderer.cs` at line 69, The Format property on DateTimeOffsetLayoutRenderer is declared as string but can be null via config/reflection; update the contract and guards: either change the property declaration to string? Format to allow null, or leave as string and add a null-forcing assertion; additionally ensure GetValue/Append (methods in DateTimeOffsetLayoutRenderer) guard against null by using the documented default format "yyyy/MM/dd HH:mm:ss.fff" when Format is null before calling DateTimeOffset.ToString(format, provider). Make the change so the code explicitly handles/configures nulls rather than relying on DateTimeOffset.ToString((string)null) fallback.tests/NLog.UnitTests/LayoutRenderers/DateTests.cs (1)
202-297: LGTM — raw-value and rendering tests are well-targeted.Tests cover both
DateTimeKind.LocalandDateTimeKind.Utcpaths, derive expected offsets fromTimeZoneInfo.Local(so they remain timezone-agnostic on CI), and verify rendered output parity with${date}. One minor gap worth considering as a follow-up: a test forDateTimeKind.Unspecified(which the renderer explicitly documents as "treat as local") would lock in that behavior.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/NLog.UnitTests/LayoutRenderers/DateTests.cs` around lines 202 - 297, Add a unit test that covers DateTimeKind.Unspecified being treated as local: create a new test method (e.g. DateTimeOffsetLayoutRenderer_TryGetRawValue_ReturnsDateTimeOffset_UnspecifiedKind) that uses SimpleLayout l = "${datetimeoffset}", sets logEventInfo.TimeStamp to a DateTime with DateTimeKind.Unspecified, calls l.TryGetRawValue(logEventInfo, out var value), asserts success and that value is a DateTimeOffset, and compares it to an expected DateTimeOffset constructed with the same timestamp and TimeZoneInfo.Local.GetUtcOffset(timestamp) (mirroring the Local-kind test pattern).tests/NLog.UnitTests/LayoutRenderers/LongDateTests.cs (1)
215-311: LGTM — parity with${longdate}is correctly asserted.Tests appropriately use the renderer instance directly (rather than
SimpleLayout) for the raw-value checks and cover both kinds plus rendered-string parity againstLongDateLayoutRendererusing theyyyy-MM-dd HH:mm:ss.ffffformat. Consider adding theDateTimeKind.Unspecifiedcase here too for symmetry.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@tests/NLog.UnitTests/LayoutRenderers/LongDateTests.cs` around lines 215 - 311, Add a symmetric test for DateTimeKind.Unspecified by creating a new fact (e.g., DateTimeOffsetLayoutRenderer_TryGetRawValue_LongDateFormat_UnspecifiedKind) that mirrors DateTimeOffsetLayoutRenderer_TryGetRawValue_LongDateFormat_LocalKind: instantiate DateTimeOffsetLayoutRenderer, set Format = "yyyy-MM-dd HH:mm:ss.ffff", create timestamp with DateTime.SpecifyKind(..., DateTimeKind.Unspecified), assign it to logEventInfo.TimeStamp, call ((NLog.Internal.IRawValue)renderer).TryGetRawValue(logEventInfo, out var value), Assert.True(success), Assert.IsType<DateTimeOffset>(value) and Assert.Equal(expected, (DateTimeOffset)value) where expected is new DateTimeOffset(timestamp, TimeZoneInfo.Local.GetUtcOffset(timestamp)); this adds the missing Unspecified-kind coverage consistent with the Local-kind test.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@src/NLog/LayoutRenderers/DateTimeOffsetLayoutRenderer.cs`:
- Around line 102-129: GetValue currently treats _universalTime unset
differently from explicit false for UTC timestamps; change it so only the true
case forces UTC and all other cases (unset or false) use ToDateTimeOffset to
preserve DateTime.Kind semantics. Concretely, update GetValue to: if
(_universalTime == true) convert/return UTC DateTimeOffset (preserving zero
offset) otherwise return ToDateTimeOffset(timestamp); remove the separate
else-branch that converts UTC to local, and ensure ToDateTimeOffset is the
single code path for non-UTC-forcing behavior (references: GetValue,
_universalTime, ToDateTimeOffset, UniversalTime).
---
Nitpick comments:
In `@src/NLog/LayoutRenderers/DateTimeOffsetLayoutRenderer.cs`:
- Line 69: The Format property on DateTimeOffsetLayoutRenderer is declared as
string but can be null via config/reflection; update the contract and guards:
either change the property declaration to string? Format to allow null, or leave
as string and add a null-forcing assertion; additionally ensure GetValue/Append
(methods in DateTimeOffsetLayoutRenderer) guard against null by using the
documented default format "yyyy/MM/dd HH:mm:ss.fff" when Format is null before
calling DateTimeOffset.ToString(format, provider). Make the change so the code
explicitly handles/configures nulls rather than relying on
DateTimeOffset.ToString((string)null) fallback.
In `@src/NLog/LayoutRenderers/ShortDateLayoutRenderer.cs`:
- Line 92: Update the inline comment on the timestamp conversion in
ShortDateLayoutRenderer.cs to be clear (e.g., "Only convert when Kind == Utc;
Local and Unspecified are already local") instead of the malformed "Utc?Local",
and add a focused regression unit test that constructs a LogEventInfo with
TimeStamp having DateTimeKind.Unspecified and UniversalTime=false then renders
with ShortDateLayoutRenderer (or calls the method that contains timestamp =
timestamp.ToLocalTime()) and asserts the timestamp is not converted (remains
treated as local). Ensure the test references the ShortDateLayoutRenderer
rendering method so it fails if Unspecified gets converted.
In `@tests/NLog.UnitTests/LayoutRenderers/DateTests.cs`:
- Around line 202-297: Add a unit test that covers DateTimeKind.Unspecified
being treated as local: create a new test method (e.g.
DateTimeOffsetLayoutRenderer_TryGetRawValue_ReturnsDateTimeOffset_UnspecifiedKind)
that uses SimpleLayout l = "${datetimeoffset}", sets logEventInfo.TimeStamp to a
DateTime with DateTimeKind.Unspecified, calls l.TryGetRawValue(logEventInfo, out
var value), asserts success and that value is a DateTimeOffset, and compares it
to an expected DateTimeOffset constructed with the same timestamp and
TimeZoneInfo.Local.GetUtcOffset(timestamp) (mirroring the Local-kind test
pattern).
In `@tests/NLog.UnitTests/LayoutRenderers/LongDateTests.cs`:
- Around line 215-311: Add a symmetric test for DateTimeKind.Unspecified by
creating a new fact (e.g.,
DateTimeOffsetLayoutRenderer_TryGetRawValue_LongDateFormat_UnspecifiedKind) that
mirrors DateTimeOffsetLayoutRenderer_TryGetRawValue_LongDateFormat_LocalKind:
instantiate DateTimeOffsetLayoutRenderer, set Format = "yyyy-MM-dd
HH:mm:ss.ffff", create timestamp with DateTime.SpecifyKind(...,
DateTimeKind.Unspecified), assign it to logEventInfo.TimeStamp, call
((NLog.Internal.IRawValue)renderer).TryGetRawValue(logEventInfo, out var value),
Assert.True(success), Assert.IsType<DateTimeOffset>(value) and
Assert.Equal(expected, (DateTimeOffset)value) where expected is new
DateTimeOffset(timestamp, TimeZoneInfo.Local.GetUtcOffset(timestamp)); this adds
the missing Unspecified-kind coverage consistent with the Local-kind test.
🪄 Autofix (Beta)
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: Repository UI
Review profile: CHILL
Plan: Pro
Run ID: 516be682-e5db-4a37-a660-4b6ba04b9596
📒 Files selected for processing (5)
src/NLog/Config/AssemblyExtensionTypes.cssrc/NLog/LayoutRenderers/DateTimeOffsetLayoutRenderer.cssrc/NLog/LayoutRenderers/ShortDateLayoutRenderer.cstests/NLog.UnitTests/LayoutRenderers/DateTests.cstests/NLog.UnitTests/LayoutRenderers/LongDateTests.cs
| private DateTimeOffset GetValue(LogEventInfo logEvent) | ||
| { | ||
| var timestamp = logEvent.TimeStamp; | ||
| if (_universalTime.HasValue) | ||
| { | ||
| if (_universalTime.Value) | ||
| { | ||
| var utc = timestamp.Kind != DateTimeKind.Utc ? timestamp.ToUniversalTime() : timestamp; | ||
| return new DateTimeOffset(utc, TimeSpan.Zero); | ||
| } | ||
| else | ||
| { | ||
| var local = timestamp.Kind == DateTimeKind.Utc ? timestamp.ToLocalTime() : timestamp; | ||
| return new DateTimeOffset(DateTime.SpecifyKind(local, DateTimeKind.Local), TimeZoneInfo.Local.GetUtcOffset(local)); | ||
| } | ||
| } | ||
| return ToDateTimeOffset(timestamp); | ||
| } | ||
|
|
||
| private static DateTimeOffset ToDateTimeOffset(DateTime timestamp) | ||
| { | ||
| if (timestamp.Kind == DateTimeKind.Utc) | ||
| return new DateTimeOffset(timestamp, TimeSpan.Zero); | ||
| if (timestamp.Kind == DateTimeKind.Local) | ||
| return new DateTimeOffset(timestamp, TimeZoneInfo.Local.GetUtcOffset(timestamp)); | ||
| // Unspecified: treat as local | ||
| return new DateTimeOffset(DateTime.SpecifyKind(timestamp, DateTimeKind.Local), TimeZoneInfo.Local.GetUtcOffset(timestamp)); | ||
| } |
There was a problem hiding this comment.
Subtle semantic difference between UniversalTime unset vs. explicit false for UTC timestamps.
GetValue uses _universalTime.HasValue to switch behavior, which means:
- Unset (default) +
Kind == Utc→DateTimeOffset(timestamp, TimeSpan.Zero)(preserves UTC). - Explicit
UniversalTime="false"+Kind == Utc→ converts viaToLocalTime()and returns with the local offset (different wall-clock value).
Users setting UniversalTime="false" in XML (which looks like a no-op since false is the documented default) will silently get a different DateTimeOffset than users who omit the attribute, whenever TimeStamp.Kind == Utc (e.g., when an AccurateUtcTimeSource is configured). This also contrasts with LongDateLayoutRenderer.GetValue, which only branches on UniversalTime being true.
Consider collapsing the two paths so that the unset case and the explicit-false case produce the same result (both preserving offset info from Kind), e.g.:
♻️ Proposed simplification
private DateTimeOffset GetValue(LogEventInfo logEvent)
{
var timestamp = logEvent.TimeStamp;
- if (_universalTime.HasValue)
- {
- if (_universalTime.Value)
- {
- var utc = timestamp.Kind != DateTimeKind.Utc ? timestamp.ToUniversalTime() : timestamp;
- return new DateTimeOffset(utc, TimeSpan.Zero);
- }
- else
- {
- var local = timestamp.Kind == DateTimeKind.Utc ? timestamp.ToLocalTime() : timestamp;
- return new DateTimeOffset(DateTime.SpecifyKind(local, DateTimeKind.Local), TimeZoneInfo.Local.GetUtcOffset(local));
- }
- }
- return ToDateTimeOffset(timestamp);
+ if (UniversalTime)
+ {
+ var utc = timestamp.Kind == DateTimeKind.Utc ? timestamp : timestamp.ToUniversalTime();
+ return new DateTimeOffset(utc, TimeSpan.Zero);
+ }
+ return ToDateTimeOffset(timestamp);
}This removes the code duplication with ToDateTimeOffset and makes UniversalTime=false equivalent to the default, matching the documented "Default: false" contract.
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.
In `@src/NLog/LayoutRenderers/DateTimeOffsetLayoutRenderer.cs` around lines 102 -
129, GetValue currently treats _universalTime unset differently from explicit
false for UTC timestamps; change it so only the true case forces UTC and all
other cases (unset or false) use ToDateTimeOffset to preserve DateTime.Kind
semantics. Concretely, update GetValue to: if (_universalTime == true)
convert/return UTC DateTimeOffset (preserving zero offset) otherwise return
ToDateTimeOffset(timestamp); remove the separate else-branch that converts UTC
to local, and ensure ToDateTimeOffset is the single code path for
non-UTC-forcing behavior (references: GetValue, _universalTime,
ToDateTimeOffset, UniversalTime).
There was a problem hiding this comment.
Pull request overview
Adds first-class ${datetimeoffset} support to NLog layout rendering so DatabaseTarget can pass a typed DateTimeOffset to ADO.NET parameters (e.g., SQL DATETIMEOFFSET) without string round-tripping, and fixes a ShortDateLayoutRenderer timezone conversion edge case.
Changes:
- Introduces
DateTimeOffsetLayoutRenderer(${datetimeoffset}) that always exposes a typedDateTimeOffsetviaTryGetRawValue, while keeping default string output aligned with${date}. - Registers the new renderer in
AssemblyExtensionTypesso it’s available without manual registration. - Fixes
ShortDateLayoutRendererlocal conversion soToLocalTime()is only applied when the timestamp isDateTimeKind.Utc. - Updates date-related unit tests to cover the new renderer and document current raw-value behavior of existing renderers.
Reviewed changes
Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.
Show a summary per file
| File | Description |
|---|---|
src/NLog/LayoutRenderers/DateTimeOffsetLayoutRenderer.cs |
New ${datetimeoffset} renderer with typed DateTimeOffset raw-value support and formatting options. |
src/NLog/Config/AssemblyExtensionTypes.cs |
Registers datetimeoffset renderer for config/SimpleLayout usage. |
src/NLog/LayoutRenderers/ShortDateLayoutRenderer.cs |
Fixes incorrect ToLocalTime() conversion for DateTimeKind.Unspecified. |
tests/NLog.UnitTests/LayoutRenderers/DateTests.cs |
Adds tests for ${datetimeoffset} raw value and string equivalence with ${date}. |
tests/NLog.UnitTests/LayoutRenderers/LongDateTests.cs |
Adds tests for ${datetimeoffset} with longdate-style formatting and raw-value behavior documentation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| if (_universalTime.Value) | ||
| timestamp = timestamp.ToUniversalTime(); | ||
| else | ||
| timestamp = timestamp.ToLocalTime(); | ||
| else if (timestamp.Kind == DateTimeKind.Utc) | ||
| timestamp = timestamp.ToLocalTime(); // Only convert Utc?Local; Local/Unspecified are already local |
There was a problem hiding this comment.
The ShortDate LocalTime path was adjusted to avoid calling ToLocalTime() for DateTimeKind.Unspecified, but there isn’t a unit test that would fail on non-UTC machines (the existing ShortDateTests mostly assert string output, which can still pass in UTC timezones). Consider adding a test that sets logEvent.TimeStamp.Kind=Unspecified and asserts the raw value (IRawValue) Kind stays Unspecified (or otherwise proves no conversion happens) when UniversalTime=false, so the regression is caught regardless of the machine timezone.
| private DateTimeOffset GetValue(LogEventInfo logEvent) | ||
| { | ||
| var timestamp = logEvent.TimeStamp; | ||
| if (_universalTime.HasValue) | ||
| { | ||
| if (_universalTime.Value) | ||
| { | ||
| var utc = timestamp.Kind != DateTimeKind.Utc ? timestamp.ToUniversalTime() : timestamp; | ||
| return new DateTimeOffset(utc, TimeSpan.Zero); | ||
| } | ||
| else | ||
| { | ||
| var local = timestamp.Kind == DateTimeKind.Utc ? timestamp.ToLocalTime() : timestamp; | ||
| return new DateTimeOffset(DateTime.SpecifyKind(local, DateTimeKind.Local), TimeZoneInfo.Local.GetUtcOffset(local)); | ||
| } | ||
| } | ||
| return ToDateTimeOffset(timestamp); | ||
| } | ||
|
|
||
| private static DateTimeOffset ToDateTimeOffset(DateTime timestamp) | ||
| { | ||
| if (timestamp.Kind == DateTimeKind.Utc) | ||
| return new DateTimeOffset(timestamp, TimeSpan.Zero); | ||
| if (timestamp.Kind == DateTimeKind.Local) | ||
| return new DateTimeOffset(timestamp, TimeZoneInfo.Local.GetUtcOffset(timestamp)); | ||
| // Unspecified: treat as local | ||
| return new DateTimeOffset(DateTime.SpecifyKind(timestamp, DateTimeKind.Local), TimeZoneInfo.Local.GetUtcOffset(timestamp)); | ||
| } |
There was a problem hiding this comment.
DateTimeOffsetLayoutRenderer has non-trivial conversion rules for DateTimeKind.Unspecified and for the UniversalTime=true/false overrides, but the added tests only cover Local and Utc kinds with the default behavior. Add unit tests that cover Unspecified (default and UniversalTime=false), and UniversalTime=true (verifying offset becomes zero and UtcDateTime matches) to ensure the typed DateTimeOffset semantics don’t regress.
|
|
Curious why the conversion introduced with NLog v6.1.2 as part of #6116 is not working for you? if (typeCode == TypeCode.DateTime && typeof(DateTimeOffset) == propertyType)
return new DateTimeOffset(convertibleValue.ToDateTime(formatProvider));See also NLog/NLog.Targets.Database#66 where |
|
I had tried to update my code before and it wasn't working but now it works fine with just "${date:format=O}" so I don't know what I did before but please disregard this PR. I'm fine with how it is currently. I'm sorry for the trouble. |
|
It should also work if you do this |
|
It does, thanks. |


Fixes #6161
Introduces a dedicated
DateTimeOffsetLayoutRenderer(${datetimeoffset}) so thatDatabaseTargetwithparameterType="System.DateTimeOffset"can flow timestamps directly to a SQLDATETIMEOFFSETcolumn without a string round-trip or workaround viaLogEventInfo.Properties.Changes
DateTimeOffsetLayoutRenderer(${datetimeoffset}) —TryGetRawValuealways returns a typedDateTimeOffset;Format,CultureandUniversalTimeproperties match the existing date renderers; default formatyyyy/MM/dd HH:mm:ss.fffkeeps string output identical to${date}out of the box.AssemblyExtensionTypes.cs— registered"datetimeoffset"so the renderer is available in XML config andSimpleLayoutwithout manual registration.ShortDateLayoutRenderer— fixedGetValue: whenUniversalTime=false,ToLocalTime()is now only called whentimestamp.Kind == Utc. Calling it unconditionally onDateTimeKind.Unspecifiedcaused an off-by-one date on UTC-offset machines.DateTestsandLongDateTeststo cover the new renderer.