NLog version
• NLog: 5.5.1
• NLog.Database: 5.5.1
• Runtime: .NET Framework 4.8.1
Description:
When a DatabaseParameterInfo is configured with ParameterType = typeof(DateTimeOffset) and a ${date:format=o} layout, the value written to SQL Server is always 0001-01-01 00:00:00.0000000 +00:00 (default(DateTimeOffset)).
The ${date} layout renderer returns a raw DateTime via TryGetRawValue. The internal conversion from DateTime → DateTimeOffset inside PropertyTypeConverter.ChangeObjectType fails silently, and the exception is swallowed by TryParseValueFromObject.
Expected behavior:
NLog should successfully convert a DateTime raw value to DateTimeOffset when ParameterType = typeof(DateTimeOffset), since new DateTimeOffset(DateTime) is a valid, lossless conversion.
Actual behavior:
PropertyTypeConverter.ChangeObjectType follows a code path that ultimately calls Convert.ChangeType(string, typeof(DateTimeOffset)), which throws InvalidCastException. The exception is caught by TryParseValueFromObject, which sets parsedValue = null, resulting in default(DateTimeOffset).
Root cause (IL-verified):
Traced via ildasm through the full rendering chain:
DatabaseParameterInfo.RenderValue()
→ ValueTypeLayoutInfo.RenderValue()
→ LayoutTypeValue.RenderObjectValue()
→ Layout.TryGetRawValue() → returns raw DateTime ✓
→ TryParseValueFromObject(rawValue: DateTime, ...)
→ PropertyTypeConverter.Convert(DateTime, typeof(DateTimeOffset), ...)
→ ChangeObjectType(DateTime, typeof(DateTimeOffset), "o", ...)
Inside ChangeObjectType, the DateTime value takes this path:
DateTime is not string → skip TryConvertFromString
DateTime implements IConvertible → GetTypeCode() returns DateTime (16)
TypeCode is not DBNull (2) → skip return
TypeCode is non-zero → branch to format check (IL brtrue.s)
- Format "o" is not null/empty,
DateTime implements IFormattable → ToString("o", culture) → ISO 8601 string
- propertyValue is now a string
- Falls through to
Convert.ChangeType(string, typeof(DateTimeOffset), formatProvider) → throws InvalidCastException
DateTimeOffset does not implement IConvertible, so Convert.ChangeType cannot target it from any source type.
The exception is caught in TryParseValueFromObject:
// Pseudocode from IL
try
{
parsedValue = ParseValueFromObject(rawValue, format, culture);
return true;
}
catch (Exception)
{
parsedValue = null;
InternalLogger.Warn("Failed converting object '{0}' of type {1} into type {2}", ...);
return false;
}
The Warn is never visible in practice because InternalLogger is typically at Error(bool) level or off.
Reproduction:
var dbTarget = new DatabaseTarget("sql")
{
ConnectionString = connectionString,
DBProvider = "System.Data.SqlClient",
CommandText = "INSERT INTO Logs ([Timestamp]) VALUES(@Timestamp)"
};
dbTarget.Parameters.Add(new DatabaseParameterInfo("@Timestamp", "${date:format=o}")
{
DbType = "DateTimeOffset",
ParameterType = typeof(DateTimeOffset) // triggers the failing conversion
});
The SQL column [Timestamp] datetimeoffset(7) NOT NULL always receives 0001-01-01 00:00:00.0000000 +00:00.
Workaround:
Set the timestamp as a typed DateTimeOffset event property and use ${event-properties} instead of ${date}:
// In the layout configuration:
// "${event-properties:item=Timestamp:format=o}" instead of "${date:format=o}"
// Before logging:
var e = LogEventInfo.Create(logLevel, loggerName, message);
e.Properties["Timestamp"] = new DateTimeOffset(e.TimeStamp);
logger.Log(e);
This works because the event-properties layout renderer returns the typed DateTimeOffset as the raw value, and PropertyTypeConverter.Convert sees DateTimeOffset.IsAssignableFrom(DateTimeOffset) = true → returns as-is.
Suggested fix:
In PropertyTypeConverter.ChangeObjectType, before the IConvertible branch, add a special case for DateTime → DateTimeOffset:
// DateTime → DateTimeOffset is a safe, lossless conversion that Convert.ChangeType cannot handle
if (propertyValue is DateTime dateTime && propertyType == typeof(DateTimeOffset))
return new DateTimeOffset(dateTime);
Alternatively, add it in TryConvertToType as a well-known conversion, or handle it in the IConvertible branch before falling through to Convert.ChangeType for target types that don't implement IConvertible.
Additional context:
• IRawValue is internal, so users cannot create a custom layout renderer that provides a typed DateTimeOffset raw value to bypass this issue.
• The ${date} layout renderer always returns DateTime from TryGetRawValue, never DateTimeOffset.
• This also affects any scenario where ParameterType (or inferred type) is DateTimeOffset and the source layout provides a DateTime raw value.
• Convert.ChangeType fundamentally cannot target DateTimeOffset from any source because DateTimeOffset doesn't implement IConvertible. This means ChangeObjectType's final fallback line will always fail for DateTimeOffset targets regardless of the input type.
NLog version
• NLog: 5.5.1
• NLog.Database: 5.5.1
• Runtime: .NET Framework 4.8.1
Description:
When a
DatabaseParameterInfois configured with ParameterType =typeof(DateTimeOffset)and a ${date:format=o} layout, the value written to SQL Server is always 0001-01-01 00:00:00.0000000 +00:00 (default(DateTimeOffset)).The ${date} layout renderer returns a raw
DateTimeviaTryGetRawValue. The internal conversion fromDateTime→DateTimeOffsetinsidePropertyTypeConverter.ChangeObjectTypefails silently, and the exception is swallowed byTryParseValueFromObject.Expected behavior:
NLog should successfully convert a
DateTimeraw value toDateTimeOffsetwhen ParameterType =typeof(DateTimeOffset), since newDateTimeOffset(DateTime)is a valid, lossless conversion.Actual behavior:
PropertyTypeConverter.ChangeObjectTypefollows a code path that ultimately callsConvert.ChangeType(string, typeof(DateTimeOffset)), which throwsInvalidCastException. The exception is caught byTryParseValueFromObject, which sets parsedValue = null, resulting indefault(DateTimeOffset).Root cause (IL-verified):
Traced via ildasm through the full rendering chain:
DatabaseParameterInfo.RenderValue()
→ ValueTypeLayoutInfo.RenderValue()
→ LayoutTypeValue.RenderObjectValue()
→ Layout.TryGetRawValue() → returns raw DateTime ✓
→ TryParseValueFromObject(rawValue: DateTime, ...)
→ PropertyTypeConverter.Convert(DateTime, typeof(DateTimeOffset), ...)
→ ChangeObjectType(DateTime, typeof(DateTimeOffset), "o", ...)
Inside ChangeObjectType, the DateTime value takes this path:
DateTimeis not string → skipTryConvertFromStringDateTimeimplementsIConvertible→GetTypeCode()returnsDateTime(16)TypeCodeis not DBNull (2) → skip returnTypeCodeis non-zero → branch to format check (IL brtrue.s)DateTimeimplementsIFormattable→ToString("o", culture)→ ISO 8601 stringConvert.ChangeType(string, typeof(DateTimeOffset), formatProvider)→ throwsInvalidCastExceptionDateTimeOffsetdoes not implementIConvertible, soConvert.ChangeTypecannot target it from any source type.The exception is caught in TryParseValueFromObject:
// Pseudocode from IL
The Warn is never visible in practice because InternalLogger is typically at Error(bool) level or off.
Reproduction:
The SQL column
[Timestamp] datetimeoffset(7) NOT NULLalways receives0001-01-01 00:00:00.0000000 +00:00.Workaround:
Set the timestamp as a typed DateTimeOffset event property and use ${event-properties} instead of ${date}:
// In the layout configuration:
// "${event-properties:item=Timestamp:format=o}" instead of "${date:format=o}"
// Before logging:
This works because the event-properties layout renderer returns the typed
DateTimeOffsetas the raw value, andPropertyTypeConverter.ConvertseesDateTimeOffset.IsAssignableFrom(DateTimeOffset)= true → returns as-is.Suggested fix:
In PropertyTypeConverter.ChangeObjectType, before the IConvertible branch, add a special case for DateTime → DateTimeOffset:
Alternatively, add it in
TryConvertToTypeas a well-known conversion, or handle it in theIConvertiblebranch before falling through toConvert.ChangeTypefor target types that don't implementIConvertible.Additional context:
•
IRawValueis internal, so users cannot create a custom layout renderer that provides a typed DateTimeOffset raw value to bypass this issue.• The ${date} layout renderer always returns
DateTimefromTryGetRawValue, neverDateTimeOffset.• This also affects any scenario where
ParameterType(or inferred type) isDateTimeOffsetand the source layout provides aDateTimeraw value.•
Convert.ChangeTypefundamentally cannot targetDateTimeOffsetfrom any source becauseDateTimeOffsetdoesn't implementIConvertible. This means ChangeObjectType's final fallback line will always fail forDateTimeOffsettargets regardless of the input type.