DEV Community

Ben Crinion
Ben Crinion

Posted on

Date handling bites again

Hi

One of my team mates found an interesting issue in our code-base so I thought I'd do a write-up.

TLDR: Don't compare a DateTimeOffset to DateTime.MinValue, compare it with DateTimeOffset.MinValue.

Technically you could compare DateTimeOffset with DateTime.MinValue if you can guarantee that the system timezone will always be UTC or UTC minus some offset.

Into The Detail

Every request my teammate mate to our API was failing with the same exception, but despite running the master branch and the exact same request, I couldn't reproduce the error.

The exception was being thrown by the constructor of an AbstractValidator from the FluentValidation package.

public class TestValidator : AbstractValidator<ThingWithDate>
{
    public TestValidator()
    {
        RuleFor(x => x.Date)
            .GreaterThan(default(DateTime));
    }
}

This validator was injected into nearly every controller in our API via a BaseController (👍).

It turns out that to reproduce the issue I needed to run the service using the same system timezone as my colleague in the Balkans (UTC+3).

You can see from the stack trace that:

  • We're initialising the TestValidator
  • That is hitting the DateTimeOffset implicit cast operator to convert the DateTime to a DateTimeOffset.
  • That calls the DateTimeOffset constructor which accepts one DateTime parameter.
  • The constructor calls ValidateDate(DateTime, Timespan). The TimeSpan is the difference between UTC and the local system timezone.

Internally a DateTimeOffset is a DateTime and an offset in minutes. ValidateDate, unsurprisingly, ensures that the DateTime is valid when the offset is applied. When you apply a negative offset to DateTime.MinValue you get a date that can't be represented by the DateTime type and you get an ArgumentOutOfRangeException instead.

Here is the code for the validate method lifted from here. Comments are mine...

private static DateTime ValidateDate(DateTime dateTime, TimeSpan offset)
{
    // +2 is 72000000000 ticks
    // utcTicks is converting the "local" dateTime to UTC by subtracting the offset
    Int64 utcTicks = dateTime.Ticks - offset.Ticks;

    // This section validates that utcTicks is a valid time that can be represented

    // utcTicks = -72000000000
    // DateTime.MinTicks = 0
    // -72000000000 < 0 == true => Kablamo
    if (utcTicks < DateTime.MinTicks || utcTicks > DateTime.MaxTicks)
    {
        throw new ArgumentOutOfRangeException("offset", Environment.GetResourceString("Argument_UTCOutOfRange"));
    }
    return new DateTime(utcTicks, DateTimeKind.Unspecified);
}

Top comments (0)