Seriously, I just need to know what time it is! But that’s a harder question to answer these days. At least half the apps I work on require some form of "Tell me what’s happened today" reporting. And today depends on where the user is. And today where the user is doesn’t match today where the app is running in Azure, which uses UTC.

[more]

Let’s talk about a concrete example. I have an app that logs incoming calls. The technician receiving the call creates a new event into the app, enters details, and moves on.

At some point later, someone will come along and run a report to see what happened yesterday, or what happened last week, or what happened last month. And that’s when things get interesting.

That event that happened at 11:30 PM, Central Time, on Sunday night? Well, that didn’t happen on Sunday night as far as the call log app is concerned. That app is running in Azure, which uses UTC, so to the app the call came in at 5:30 AM Monday. That throws the reports off.

My Solution

The first part of my solution is to use the DateTimeOffset class everywhere. I use it in my C# code, and I use it in my SQL tables.

If you aren’t familiar, a DateTimeOffset is like a normal DateTime, but it also stores an offset for the timezone, so you now know for sure when something occurred, because you know the date and time and the offset.

Let’s take a look at the difference:

> DateTime.Now.ToString()
1/26/2017 2:43:43 PM
> DateTimeOffset.Now.ToString()
1/26/2017 2:43:49 PM -06:00
>

See that "-06:00" on the end? That’s the offset to my current time zone (Central Standard).

I’m a bit lucky in that, for at least this app, I know the end-users are always in Central Time. I can leverage that fact and the fact that all my data is stored timezone-aware to correctly find the start and stop times for reporting periods.

All I need to do is this:

  1. Determine what day and time it is now (in UTC)
  2. Figure out what day and time that is in Central Time
  3. Back things up to the start of that day in Central Time (because our reporting periods need to start at mindnight CST)
  4. Convert back to UTC for easy comparison to the stored data (probably not really necessary since UTC stores the offset, but it makes me feel better to have everything in the same time zone)

This is a helper class I use, annotated so you can understand what the heck I was thinking when I created this.

    public static class CentralTime
    {
        public const string CentralTimeZoneId = "Central Standard Time";

        public static DateTimeOffset Today => GetStartOfDayFor(DateTimeOffset.UtcNow);

        private static DateTimeOffset GetStartOfDayFor(DateTimeOffset datetimeUtc)
        {
            //Let's find the TZI for Central time...
            var centralTimeZone = TimeZoneInfo.FindSystemTimeZoneById(CentralTimeZoneId);
            //Since Central Time uses Daylight Savings Time for part of the year, we need
            //to see what offset to use for our target datetime.
            var centralTimeOffset = centralTimeZone.GetUtcOffset(datetimeUtc);

            //So, now let's see what time it is in Central.  Note that this might change
            //the date!!  If it was 4am on 1/26/2017 in UTC, our result here will be
            //11pm on the 25th!  
            var timeInCentral = datetimeUtc.ToOffset(centralTimeOffset);

            //Now we have the current day and time, in central time, and we want the start of
            //the day, so let's just clear the time component...
            var startOfDateInCentral = timeInCentral.Subtract(timeInCentral.TimeOfDay);

            //And now we can move this back to UTC.  
            startOfDateInCentral.ToUniversalTime();

            return startOfDateInCentral;
        }
    }

Here’s an example for displaying the last 7 days of activity using my helper:

var sevenDaysAgo = CentralTime.Today.AddDays(-7);
var tomorrow = CentralTime.Today.AddDays(1);

//_context is an Entity Framework context
var calls = _context.Calls.Where(x => x.ReceivedUtc >= sevenDaysAgo && x.ReceivedUtc < tomorrow);

Or if I just want the current day’s worth of data for a dashboard-type view:

var today = CentralTime.Today;
var tomorrow = today.AddDays(1);

//We probably don't really have to worry about calls being logged for 
//"in the future", but again, it makes me feel better that I've handled
//that potential scenario. 
var callsForCurrentDay = _context.Calls.Where(x => x.ReceivedUtc >= today && x.ReceivedUtc < tomorrow);

Limitations

The big limitation with this approach is that it assumes everyone wants to see data in Central Time. But what if my users were spread all over the place? Well, one option could be to capture the user’s timezone during login, store it somewhere that I can get to it (perhaps in a claim or in session state), then use that timezone instead of hardcoding things to central time.

Anyway, what do you think? How do you deal with the concept of "today" across time zones in your .NET application?