Skip to content

DatabaseTarget writes default(DateTimeOffset) when ParameterType = typeof(DateTimeOffset) with ${date} layout #6115

@LorneCash

Description

@LorneCash

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:

  1. DateTime is not string → skip TryConvertFromString
  2. DateTime implements IConvertible GetTypeCode() returns DateTime (16)
  3. TypeCode is not DBNull (2) → skip return
  4. TypeCode is non-zero → branch to format check (IL brtrue.s)
  5. Format "o" is not null/empty, DateTime implements IFormattableToString("o", culture) → ISO 8601 string
  6. propertyValue is now a string
  7. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions