Rick Strahl's Weblog  

Wind, waves, code and everything in between...
.NET • C# • Markdown • WPF • All Things Web
Contact   •   Articles   •   Products   •   Support   •   Advertise
Sponsored by:
West Wind WebSurge - Rest Client and Http Load Testing for Windows

Back to Basics: UTC and TimeZones in .NET Web Apps


:P
On this page:

Most applications we build tend to have date and time data associated with it. If the application is not used just in a single location you are likely have to deal with times zones in your application. While .NET has a good set of date manipulation function, the various time zone conversion routines are a bit of a pain to use. I find myself having to look up what functions need to be called and review my thinking about how to best manage dates frequently. I see others struggling with this often as well, so I decided to take some time to write it down here in a post so I can find this info in one place.

Thinking about Time in .NET Web Apps

Web apps typically require that dates are stored in time zone agnostic fashion. This means storing DateTime values as UTC, so that you have a consistent baseline date value. This means storing dates in UTC format either with generated values using DateTime.UtcNow or, if you are capturing user input dates converting the time zone specific dates to UTC dates using .ToUniversalTime().

However in Web apps, this is not quite so easy because you can’t just use the server’s local time and use .ToUniversalTime() on a captured date input because that time reflects the server’s time, not necessarily the user’s time. So in addition to converting to UTC you also need to be able to convert user local dates to and from specific time zones which you can do with TimeZoneInfo.ConvertTime() and THEN convert that value to UTC.

On the other end when retrieving dates you convert all dates from UTC dates to local dates according to the user’s time zone for which you need to using the TimeZoneInfo class. You need two static methods from it: TimeZoneInfo.FindSystemTimeZoneById() and TimeZoneInfo.ConvertTimeFromUtc(). Also useful are TimeZoneInfo.ConvertTime() which converts between two timezones and TimeZoneInfo.ConvertTimeToUtc(). Finally if you’re building UI to allow users to select Time zones you’ll probably want to use TimeZoneInfo.GetSystemTimeZones().

Let’s see how we can put all of these together.

Simple Conversion Routines

Before looking at more specific examples of how to set this up in a more application specific way in a Web application, let’s look at a couple of easy ways to convert UTC dates to a specific locale via some DateTime extension methods which demonstrate the basic features that .NET provides for time zone conversions.

/// <summary>
/// Returns TimeZone adjusted time for a given from a Utc or local time.
/// Date is first converted to UTC then adjusted.
/// </summary>
/// <param name="time"></param>
/// <param name="timeZoneId"></param>
/// <returns></returns>
public static DateTime ToTimeZoneTime(this DateTime time, string timeZoneId = "Pacific Standard Time")
{
    TimeZoneInfo tzi = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
    return time.ToTimeZoneTime(tzi);
}

/// <summary>
/// Returns TimeZone adjusted time for a given from a Utc or local time.
/// Date is first converted to UTC then adjusted.
/// </summary>
/// <param name="time"></param>
/// <param name="timeZoneId"></param>
/// <returns></returns>
public static DateTime ToTimeZoneTime(this DateTime time, TimeZoneInfo tzi)
{
    return TimeZoneInfo.ConvertTimeFromUtc(time, tzi);
}

To use these extension methods is super easy. The first one works by getting the value from a string:

DateTime time = DateTime.UtcNow.ToTimeZoneTime("Pacific Standard Time");

while the second works of an existing TimeZoneInfo instance:

var tz = TimeZoneInfo.FindSystemTimeZoneById("Pacific Standard Time");
DateTime time2 = DateTime.UtcNow.ToTimeZoneTime(tz);

The latter is important as you want to cache a TimeZoneInfo instance in order to reduce the overhead of looking up the TimeZoneInfo repeatedly when running these functions in a loop – like displaying a long list of items with dates in them. I’ll talk more at performance later, but suffice it say that compared to direct date access doing time zone conversions is (relatively) slow…

Next let’s look at how we get TimeZones into our applications.

Admin User Interface

In order to properly convert user time zone values you need to figure out which time zone the user is coming from, so you you need to capture a preference for this and save it with a user. I typically capture his as part of my User Admin Interface and let the user pick from a list of the available time zones. This looks something like this on my Profile form:

DateField

which drops down a list retrieved with TimeZoneInfo.GetSystemTimeZones:

DroppedDownTimeZones

The code to do this is pretty straight forward. I use ASP.NET MVC 5 and have the code in my ViewModel to create an array of SelectList objects:

public class ProfileViewModel : AppBaseViewModel { public User User { get; set; } public string Password { get; set; } public string PasswordConfirm { get; set; }

public string JsTime { get; set; } public SelectListItem[] TimezoneList { get; set; }
public ProfileViewModel() { var tzs = TimeZoneInfo.GetSystemTimeZones(); TimezoneList = tzs.Select(tz => new SelectListItem() { Text = tz.DisplayName, Value = tz.Id }).ToArray(); } }

The actual TimeZone value is then bound to my User object which has a TimeZone property.

<div class="form-group">
    <div class="input-group ">
        <div class="input-group-addon" title="Timezone to display dates.">
            <i class="fa fa-fw fa-flag"></i>
        </div>
        @Html.DropDownListFor(mod => Model.User.TimeZone, Model.TimezoneList,
            new {@class = "form-control", title = "Timezone to display dates."})
    </div>
</div>

The model binds to it and the value is stored in the database in this case in MongoDb. The property stores the TimeZoneInfo.Id which is a value like Hawaiian Standard Time or Eastern Standard Time that can be used as keys for looking up timezones. This key is what’s used to find the right time zone to perform any conversions.

Capturing a Web User’s Default Time Zone

When a user comes in to the site for the first time, we can in most cases also detect the user’s time zone from the browser, so the list above can be pre-selected to the proper time zone. The JavaScript Date() object includes the time zone information in it when you write out the time to string:

var dt = new Date().toString();
//Wed Feb 04 2015 18:37:55 GMT-1000 (Hawaiian Standard Time) 

The time zone value uses official time zone string which is the same as the standard names that the .NET time zones use, so all we have to do is peel out the zone expressions in parenthesis and map that to our list box input.

To do this  I have to pass the time zone from the client to the server in some way when accessing my Profile view for the first time. This requires a little JavaScript to do. There are a few ways to do this. The easiest is probably to add some JavaScript to append the time to the link URL:

@if (!Model.IsSubscribed)
{
    <a href="@Url.Action("Profile", "Account")" id="SubscriptionButton">
        SUBSCRIBE NOW
    </a>
    <script>
        var el = document.getElementById("SubscriptionButton");
        //Wed Feb 04 2015 18:37:55 GMT-1000 (Hawaiian Standard Time)                    
        var href = el.href + "?JsTime=" + new Date().toString();
        el.href = href;
    </script>
}

This code injects a JsTime query string value into the link URL and… creates a pretty ugly URL. If you care about that, you can use RegEx in the JavaScript to clean up the date string and extract just the time zone string for a little cleaner URL. However, I prefer to do the parsing on the server so I just pass the whole messy date string up to the server in the URL. If you want to keep the URL clean you can also wrap the initial access into a form and POST the data using a hidden form variable. Either way you have to get the data to the server in some way.

Inside of the controller I can then check and convert this value to a proper time zone string with the following C# ASP.NET MVC code (ExtractString is a helper method in Westwind.Utilities):

// try to pick up user's timezone
var jsTime = Request.QueryString["JsTime"];
if (!string.IsNullOrEmpty(jsTime))
    //Wed Feb 04 2015 18:37:55 GMT-1000 (Hawaiian Standard Time) 
    model.TimeZone = StringUtils.ExtractString(jsTime, "(", ")");
else
    model.TimeZone = App.DefaultTimeZone;

It’s a good idea to check for a missing value and default it to the default time zone even though the conversion routines automatically detect invalid timezones and fix them up.

Sweet - with this you save your users some mousing around a long list of time zones.

TimeZone Conversions on the User Class

My application level User class includes some logic to convert dates for a given user. Since the time zone is typically associated with a user and the user is usually in context throughout the application’s domain and front end logic, it makes sense to attach the time zone and a few time zone routines that act on it the user for easy access. Here’s a partial code of the relative properties and method of my user entity:

public class User
{
    public User()
    {
        Id = AppUtils.GenerateId();
        TimeZone = "Hawaiian Standard Time";
    }

    public string Id { get; set; }
        
    [Required]
    public string UserName { get; set; }
        
    [MinLength(6)]
    public string Password { get; set; }

    /// <summary>
    /// The users TimeZone using .NET TimeZoneNames
    /// </summary>
    public string TimeZone
    {
        get { return _timeZone; }
        set
        {
            TimeZoneInstance = null;
            _timeZone = value;
        } 
    }
    private string _timeZone;

    [BsonIgnore]
    public TimeZoneInfo TimeZoneInstance
    {
        get
        {
            if (_timeZoneInstance == null)
            {
                try
                {
                    _timeZoneInstance = TimeZoneInfo.FindSystemTimeZoneById(TimeZone);
                }
                catch
                {
                    TimeZone = "Hawaiian Standard Time";
                    _timeZoneInstance = TimeZoneInfo.FindSystemTimeZoneById(TimeZone);
                }
            }
            return _timeZoneInstance;
        }
        private set { _timeZoneInstance = value; }
    }

    private TimeZoneInfo _timeZoneInstance;

   ... additional model data omitted

    /// <summary>
    /// Returns a UTC time in the user's specified timezone.
    /// </summary>
    /// <param name="utcTime">The utc time to convert</param>
    /// <param name="timeZoneName">Name of the timezone (Eastern Standard Time)</param>
    /// <returns>New local time</returns>
    public DateTime GetUserTime(DateTime? utcTime = null)
    {
        TimeZoneInfo tzi = null;

        if (utcTime == null)
            utcTime = DateTime.UtcNow;        
           
        return TimeZoneInfo.ConvertTimeFromUtc(utcTime.Value, TimeZoneInstance);
    }

    /// <summary>
    /// Converts local server time to the user's timezone and
    /// returns the UTC date.
    /// 
    /// Use this to convert user captured date inputs and convert
    /// them to UTC.  
    /// 
    /// User input (their local time) comes in as local server time 
    /// -> convert to user's timezone from server time
    /// -> convert to UTC
    /// </summary>
    /// <param name="localServerTime"></param>
    /// <returns></returns>
    public DateTime GetUtcUserTime(DateTime? localServerTime)
    {
        if (localServerTime == null)
            localServerTime = DateTime.Now;

        return TimeZoneInfo.ConvertTime(localServerTime.Value, TimeZoneInstance).ToUniversalTime();
    }
                   
}

This code basically provides you the ability to make conversions based on the user’s time zone. There are two steps in the process for time zone conversion:

  • Get an instance of the TimeZone for the user
  • Converting the actual Date values from UTC to the specific TimeZone

Note that the time zone is cached so that the time zone lookup occurs only once in looping situations. Whenever the TimeZone is changed the cached value is also released.

Converting Utc Dates to User Local Time for Display

.GetUserTime() returns an adjusted local time for a given UTC time which you can use for user displayed output of dates. Anytime I display a Date value I apply this function to the date to get the dates to display in the user’s time zone. Pretty straight forward. The easiest scenarios are display cases where you can simply wrap the code in your views with calls to the converter methods – in my case the User.GetUserTime() method.

Here’s what this looks like in my Razor views for example for a simple date value:

<a href="@Url.Action("Prognosis", new {id = prakritiQuiz.Id})">
    @Model.User.GetUserTime(prakritiQuiz.Started.Value).ToString("MMM. d, yyyy")
</a>

or in looping situations:

@foreach (var quiz in historyQuizzes)
{
    <tr>
        <td>
            <a href="@Url.Action("Prognosis", new {id = quiz.Id})">
                @Model.User.GetUserTime(quiz.Started.Value).ToString("MMM. d")
            </a>
        </td>
        <td>@quiz.Result.Dosha</td>
        <td>@quiz.ToDoshaResultsString()</td>
    </tr>
}

Note that my Model includes a user record as part of my Base ViewModel that all models are based on so that User info is always available in all views.

If you don’t want to use a specific User model or have extra baggage on your view model you can also use the .ToTimeZoneTime() extension methods I showed earlier. If you do, I recommend you first retrieve the TimeZoneInfo and then store it as a variable in the view for caching. Then use the overload with the TimeZone to improve performance.

The same logic is applied inside of business code that needs to generate any output related data. For example, in one of my business object I generate summary graph data that is fed via JSON to a client side graph control and I populate the graph data in a loop:

foreach (var res in resultValues)
{                    
    var gp = new QuizGraphDataPoint();
    gp.StartedDate = user.GetUserTime(quiz.Started.Value);
    gp.Date = gp.StartedDate.ToString("MMM d");                    
    gp.Dosha = res.Key;
    gp.Value = ((decimal) res.Value/(decimal) totalDoshaCount)*100;

    data.ResultSets[res.Key].Add(gp);

    doshaString += res.Key;
}

Just keep in mind though that most business code probably doesn’t need to deal with display formatting as this conversion is generally a UI operation. So you’ll typically find calls to user.GetUserTime() isolated to View markup code, or possibly in controller code that preformats display data.

Date Conversions for Date Range Queries

Using timezone adjusted dates in queries tend to be tricky because you have to adjust the input dates for the the users time zone before you can use them in the query and then convert them to UTC. If you also have to group dates by date boundaries things get even more tricky as you have to adjust the UTC times to catch the date boundaries properly.

This can be a simple thing like do a query and return data for the last 30 days, or run a query where the user provides local dates for start and end dates. The issue here is that the date range has be based on the user’s local time not UTC, lest you miss or include to much data for a request.

The following example demonstrates doing a date range query for the last 30 days range using a LINQ query against MongoDb (same concept works with any LINQ provider):

var now = user.GetUserTime(DateTime.UtcNow);

// force date boundary to be matched to users time
var start = now.Date.AddDays(days*-1).ToUniversalTime();
var end = now.Date.AddDays(1).AddSeconds(-1).ToUniversalTime();

var results = Collection.AsQueryable()
    .Where(q => q.UserId == userId &&
                q.Started >= start &&
                q.Started <= end &&
                q.Result != null &&
                q.QuizType == QuizTypes.Daily)
    .OrderBy(q => q.Started)
    .Select(q => q)
    .ToList();

Note that I have to first get the current date (or whatever dates the user provided) into the specific user’s time. Then once I have the adjusted local time, I can perform the date math to establish the date range – in this case subtracting 30 days and adding 1 day to today. This gives me the date range in the user’s timezone. Then convert that back to UTC so it can be passed into the LINQ query.

Note that you need to be careful in your queries to pre-calculate these values as stored in the start and end  variables as I’ve done here – provider LINQ queries tend to not understand custom methods so always pre-assign the values and don’t use the expressions in the query.

User Captured Time

If you capture user input from users from a different time zone you have to convert the captured time to the user’s time zone. Most built in data binding date conversions give you dates in server local time, which is likely not the time user expected to give you. So a user in Oregon will input date/time values as Oregon time, not Hawaiian time as my server expects.

In order to account for that the user’s input has to be converted to the proper time zone.

To demonstrate, take the following MVC controller method as an example, which captures a user time input from the Pacific Timezone when running on a machine in the Hawaiian TimeZone that has a –2 hour offset:

public string DateMath(DateTime start)
{    
    var offset  = user.TimeZoneInstance.GetUtcOffset(start).TotalHours;
    var offsetLocal = TimeZoneInfo.Local.GetUtcOffset(start).TotalHours;
    var startTime = start.AddHours(offsetLocal - offset);

    // this is the time you write to the db
    var timeToSave = startTime.ToUniversalTime();

    return TimeZoneInfo.Local.ToString() + "<hr/>" + 
           "Captured time: " + start + " -> UTC:  " + start.ToUniversalTime() + "  <hr/>   " +
           "Adjusted time: " + startTime + " -> UTC: " + timeToSave;
}
If I now call this page with:

http://localhost/MyDailyDosha/home/DateMath?start=2/1/2015

I get the following output:

(UTC-10:00) Hawaii
(UTC-08:00) Pacific Time (US & Canada)
Captured time: 2/1/2015 12:00:00 AM -> UTC: 2/1/2015 10:00:00 AM
Adjusted HI time: 1/31/2015 10:00:00 PM -> UTC: 2/1/2015 8:00:00 AM

The time is entered by the user with the intent of using Pacific time (-8) as midnight 12am, but the time is captured as Hawaiian time. In Hawaiian local time the user’s date would be at 10pm to reflect the 2 hour time offset.

The code above calculates the difference between the local time zone and the users timezone and applies the difference. The result is the last value which appropriately captures the 8 hour time zone offset for UTC: 12am PST entered –> 8am UTC written to DB.

This seems terribly convoluted, but it’s necessary as you need to take the user’s input and convert it into a known timezone. ASP.NET MVC converts dates based on the local server time, so the time comes in as Hawaiian, even though the user is in Oregon and this code essentially adjusts that difference.

A more generic version of this logic is also attached to my User class:

/// <summary>
/// Converts a local machine time to the user's timezone time
/// by applying the difference between the two timezones.
/// </summary>
/// <param name="localTime">local machine time</param>
/// <param name="tzi">Timezone to adjust for</param>
/// <returns></returns>
public DateTime AdjustTimeZoneOffset(DateTime localTime, TimeZoneInfo tzi = null)
{
    if (tzi == null)
        tzi = TimeZoneInstance;

    var offset = tzi.GetUtcOffset(localTime).TotalHours;
    var offset2 = TimeZoneInfo.Local.GetUtcOffset(localTime).TotalHours;
    return localTime.AddHours(offset2 - offset);
}

DayLight Savings Time

Since we’re talking about time offsets keep in mind that time zones also have to deal with day light savings time. You might be tempted to store TimeZone offsets from UTC for your users, but the problem of DayLight Savings time makes that a really bad idea.

Daylight savings time changes the time for certain parts of the year (typically by an hour), in some places of the world. Daylight savings time is not applied universally. For example Hawaii doesn’t have it, while Oregon does. Typically the closer you are to the equator the less likely the time zone will have daylight savings time since days don’t vary much closer to the equator.

The good news is that daylight savings time is automatically handled by the various time zone conversions I’ve discussed so far. So if I change my date to a date that is in the summer when daylight savings is active – 6/1/2015 -the result looks like this:

(UTC-10:00) Hawaii
(UTC-08:00) Pacific Time (US & Canada)
Captured time: 6/1/2015 12:00:00 AM -> UTC: 6/1/2015 10:00:00 AM
Adjusted time: 5/31/2015 9:00:00 PM -> UTC: 6/1/2015 7:00:00 AM

Note that the difference between the Hawaii and Pacific time zones now is 3 hours and the conversion automatically figured this out. There were no changes required in code because the time zone conversion and offset routines know and understand the day light savings rules.

Date Grouping

Another tricky task you may have to deal with when doing UTC date conversions is date grouping. The problem is that when dates are stored in UTC you can’t easily group on date boundaries (ie. give me all orders from March 1st – 31st). If you do the grouping on the UTC date the date groups will be off by the UTC offset and you would end up with some orders falling into the wrong groups.

To get around this you need to convert dates to user adjusted local dates. Sounds easy enough, except that you can’t do these types of date conversions we’ve talked about in some LINQ data code because the various DB LINQ Providers don’t understand arbitrary .NET functions – they have a finite set of support functions that are translated into queries.

So if you take the previous result which contains all entries and all dates and run it to list you could then filter it down to only retrieve the last date for a given date:

// find all entries by last started date
var gresults = results
    .OrderByDescending(s => s.Started)
    .GroupBy(q=> user.GetUserTime(q.Started.Value.Date))
    .Select(group => group.First())
    .ToList();

You might think that this grouping would work in the first query but it won’t. MongoDb doesn’t support GroupBy queries at all (using LINQ anyway), and Entity Framework would choke on the User.GetUserTime() method call which it wouldn’t know how to convert to SQL.

So if you need to do internal query operations that require UTC date conversions that you can’t do with the date intrinsic functions you unfortunately have to resort to LINQ to Objects in memory to make the aggregations work.

What about DateTimeOffset?

A number of people commented asking about DateTimeOffset which is DateTime like class that is actually recommended by Microsoft to use as a DateTime replacement. DateTimeOffset stores the timezone offset of a date value with the date so you can always tell the timezone the date came from originally. One of the things about DateTimeOffset is that you have to use it throughout the system to be effective – both in the application layer and the data layer. SQL Server has a DateTimeOffset field type that matches the .NET type.

Personally I’ve never used DateTimeOffset because it only addresses a single scenario that I think doesn’t work very well for Web applications. DateTimeOffset is not all that useful in server applications where the timezone is fluent. If I enter a date in Hawaii, then later go to Oregon and want to see the date in the Oregon time zone format I still have to do the conversions mentioned in this post. Since DateTimeOffset only supports a single time zone,  you also have to get the date into the right time zone for saving first, since the server will capture dates using server local time. So you end up doing the time zone conversion up front, but then it’s stuck in that timezone. If a user moves across time zones, you’re back to doing conversions based on the UTC date part of the DateTimeOffset.

So in a fluent Web date environment where users are not statically tied to a timezone DateTimeOffset buys very little IMHO. To be honest I've not used DateTimeOffset much at all, because of the above issues, but maybe I'm missing something obvious.

Performance

One thing to consider is that time zone calculations are relatively slow. Compared to just grabbing a raw date value  like DateTime.Now and just using it or even adding some offset value to a date, time zone conversions add a fair bit of overhead. Times are still fast, but if you’re doing time conversions in tight loops for thousands of dates you will definitely see some overhead. Hopefully you’re practicing good view design and don’t ever display many thousands of records in the first place, which is your primary protection against the extra overhead.

John Skeet’s NodaTime

Realizing that the concepts of time in the .NET BCL are scattered about in various classes that aren’t quite optimal for doing time conversions, Jon Skeet created a separate Time system library called NodaTime. NodaTime makes the concept of a timezone a first class citizen where you pick a date based on the timezone and the date time format it presents. You can then easily convert between these formats. It’s a great implementation and definitely worth checking out, but it is a little more complex than the BCL DateTime/TimeZoneInfo implementations because it forces you to think about what kind of date you are dealing with before you assign it. But it requires some commitment – for it to work effectively, you essentially have to replace standard DateTime values with NodaTime types throughout your application.

In the end though the issues I discussed above still apply. You still have to decide when times have to be adjusted and how, so while NodaTime makes the conversions and management easier and more intuitive, it doesn’t free you from the application related issues that come up when you’re dealing with TimeZones in your application.

Using UTC in Applications

Using UTC dates for data is a pretty common and necessary practice but it definitely adds some complexity when managing dates as you always have to remember to properly adjust dates. For display purposes this is pretty straight forward, but for query operations there’s a bit of mental overhead to ensure your date math adds up properly.

No easy solutions, but I hope this post and some of the helpers make life a little easier for you – I know they do for me.

So what about you? Have any tips and tricks and best practices that have worked well for you to make managing UTC dates easier in your application? Leave a comment if you do.

Posted in ASP.NET  C#  .NET  Localization  

The Voices of Reason


 

Chris Hynes
February 10, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

NodaTime takes a lot of the pain out of dealing with time zones: http://nodatime.org/.

Instead of doing querying for local dates in memory, with EF you can define custom SQL functions like this: http://blog.3d-logic.com/2014/08/11/the-beta-version-of-store-functions-for-entityframework-6-1-1-code-first-available/. NHibernate has a similar capability.

Michael Weinand
February 10, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

Any thoughts on http://nodatime.org/? We've used that in the past with success to make Timezones a bit easier to deal with.

Rick Strahl
February 10, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

NodaTime is excellent, but it does requires a full commitment in an application which is often not possible especially in existing applications. I've added a few notes to the post . Thanks for the reminder.

Tom Deleu
February 11, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

What about DateTimeOffset? Where does that fit in according to your opinion?

Rick Strahl
February 11, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

@Tom - DateTimeOffset is useful as it provides built-in support for the Offset from Utc, but it doesn't really help simplify the issues addressed in this post. I think DateTimeOffset would be very useful in applications that run in many different physical locations where you both need local dates for user display in a fixed location and Utc dates for storage to the database. In the end if you pull data out of the database you still have to somehow figure out what timezone is used though.

There's a great entry that compares about both DateTime and DateTimeOffset on SO:
http://stackoverflow.com/questions/4331189/datetime-vs-datetimeoffset

James Manning
February 11, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

"In the end if you pull data out of the database you still have to somehow figure out what timezone is used though."

Nope, that's why the datetimeoffset data type was introduced in SQL Server (linked to in that SO thread, even)

https://msdn.microsoft.com/en-us/library/bb630289.aspx

DateTimeOffset keeps the DateTime along with the UTC offset, which datetimeoffset does in SQL Server as well. It's simpler to use that end-to-end instead of having to remember to convert to and from UTC when dealing with the database.

If you're stuck with a legacy situation where only datetime/datetime2 is supported, then yes, storing as UTC is definitely the way to go. If you're using SQL 2008 or later, though, IMHO you should definitely be using datetimeoffset instead of dealing with timezone conversion to and from UTC.

Martin Enzelsberger
February 11, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

»[NodaTime] is a little more complex [...] because it forces you to think about what kind of date you are dealing with«

I'm pretty sure that's not a negative.
To me this sentence sounds like »catch blocks are great, but they are a little more complex because they force you to think about to do when an exception occurs«.

You *should* be thinking about it, so it's a good thing that NodaTime forces you to do so. To me it's more of an advantage than a disadvantage (if you're dealing with time zones).

Johan
February 11, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

As James says above DateTimeOffset handles the server side storage for you, client-side you can just stick with Date object to handle UTC to local time both ways, especially if you use an SPA and do all the binding client side, this makes timezone handling trivial. Convert to UTC for input from client as early as possible and present back to client as local time as late as possible, no need to complicate things server side

Rick Strahl
February 11, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

@James, @Johan - DateTimeOffset is not all that useful in server applications where the timezone is fluent. If I enter a date in Hawaii, then later go to Oregon and want to see the date in Oregon timezone format I still have to do conversions. Since DTO only supports a single timezone - I first have to convert to that timezone to save as well so I have up front work to do. You also have to get the date into the right TimeZone for saving first since the server will catch date using server local time. So you end up doing the time zone conversion up front. So in a fluent Web date environment where users are not statically tied to a timezone DateTimeOffset buys very little IMHO. 

To be honest I've not used DateTimeOffset much at all, because of the above issues, but maybe I'm missing something obvious :-)

@Johan - yes client side data display is easier if you push data down as UTC with JSON and then just display the data, which is nice since the browser knows what timezone to use. But that only addresses one of the scenarios - and which is basically the easiest one (ie. calling user.GetUserTime()). It doesn't address querying on the server, it doesn't address grouping or assignment for display values that are often pushed down from views in the server (I tend to pre-format dates as strings on the server in my view objects).

Jeremy
February 11, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

Funny timing - I have been globalizing a Vacation tracking system for the past week and am using nodatime. I first tried the built in .NET classes you describe above, but I found the list of available time zones (which I believe are read from the server registry) to be terribly lacking and insufficient for my project. Further research led me to the tz database of IANA/Olson time zones which were perfect for what I was trying to accomplish. A little more research and I happened upon NodaTime. Within an hour I had it working with an external tz database. This is awesome because I can automate the updates to the db. Once I got my app working with NodaTime, I spent the majority of my time analyzing the dates and datetime values in the system to determine how best to store the dates. This portion of the project seems to require the most thought and is still ongoing. I was surprised to see this blog post today in my inbox and was glad to see that you mentioned NodaTime. Good luck everyone with your projects and keep up the helpful blog posts.

Luke Puplett
February 13, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

I tend to write more web services than web sites, though they work the same way, so the same logic should apply and the same problems.

Web services tend to use date-time formats that capture and serialize the local time and the offset from UTC, and no other time-zone information.

This is stored as DATETIMEOFFSET in MSSQL - indeed, EF for a while, removed support for the old DateTime, forcing DTO!

This information has captured a moment in time that has all the information to be able to show what a person thought the time was, historically. Even if their local daylight savings time rules are changed, such as for the USA, I remember the impact on IT when working on a trade-floor in 2005:

http://en.wikipedia.org/wiki/Daylight_saving_time_in_the_United_States#2005_revision_to_dates_of_observance

The problem with not persisting the offset is that the programmer is at the mercy of the current algorithm in whatever library, when retroactively applying it to an historic timestamp. Will the algorithm know that daylight savings time was different back then?

As you can imagine, this is very important legally. I believe this is why ISO 8601/RFC 3339 has come to be the standard for date exchange and persistence.

....I think. Seriously, its one of those mind-boggling problems that invokes a fair bit of self doubt.

In any case, Microsoft are de-emphasising DateTime in favour of DateTimeOffset, probably for similar reasons. Used correctly, it is, I guess "non destructive".

Thanks for the blog post, its a useful tool for rationalizing and scenario planning.

All the best,

Luke

James Manning
February 18, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

@Rick - WRT "Since DTO only supports a single timezone - I first have to convert to that timezone to save" and "have to get the date into the right TimeZone for saving first" - those are not correct (and very unfortunate that those were the reasons you've avoided using it).

The whole point is that since the data type stores the offset with the datetime, then the same column can store any timezone. You don't have to do *any* timezone conversions at any point if you don't want. You get a DateTimeOffset from someone in Hawaii that's got a UTC offset of -10 and you can store it as-is and still happily allow Oregon people to store in -8/-7, EST people to store -5/-4, etc. If SQL Server forced developers/users to convert to a particular timezone for saving the datatype would have no benefit over just saving as datetime2 and telling people to store as UTC (certainly the best practice if you're stuck using datetime/datetime2).

The *only* time you need to do any conversion is strictly when displaying, and only if you want to display it in a different timezone than what it already is (for instance, displaying it in the user's timezone regardless of what the originating timezone was). You can do comparisons, queries, etc all without having to do any conversions.

In your enter-in-Hawaii/display-in-Oregon scenario, with datetime or datetime2, you would typically convert twice, once on the write path to convert to UTC for storage (since your storage has no support to encode the UTC offset, so you would either encode it as a separate column, or more likely, store the version with no offset), then again on the read path to convert the UTC to Oregon.

If you use datetimeoffset instead, you don't need to convert on your write path at all. It comes in as offset -10 from your Hawaii person, -8 half the time from your Oregon people, -7 the rest of the time, etc. and you just store it like that. On your read path, you can display it with the original offset if you want (something that's not really an option if you forced conversion to UTC on the write path), or convert it to local time (just like you would in the datetime/datetime2 scenario)

I think Bart Duncan said it best, IMHO :)

http://blogs.msdn.com/b/bartd/archive/2009/03/31/the-death-of-datetime.aspx

***
When should you use datetimeoffset instead of datetime? The answer is: you should almost always use datetimeoffset. I’ll make the claim that there is only a single case where datetime is clearly the best data type for the job, and that’s when you actually require an ambiguous time
***

Rick Strahl
February 19, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

@James - I think I understand what DateTimeOffset does - it stores the DT *and* a timezone offset of when the data is captured for that particular tz. Makes sense. But for Web apps you NEVER capture time that way. You want to capture the date in UTC and the offset is irrelevant because it represents the server's time not the client's time.

Even if you DID store the offset from the original users timezone (which means you'd have to do the conversion up front because the server's not running that users timezone) you still get *only that timezone*. Not the timezone that a user of the date might want to see at a later time.

I guess I don't see how DTO helps if the time value's offset is fixed to a specific timezone when the application always adjusts to the user's timezone preference which mostly will not be the original DTO offset. You STILL have to do these conversions for EVERY user and if I do that then what does DTO buy me? Nothing except more storage required.

I also don't agree with this:

> The *only* time you need to do any conversion is strictly when displaying

because if you do certain datetime operations like Date queries that group by day or month or less granual time increment, that requires that you adjust those queries for that timezone. Otherwise you're going to include the wrong time range. So there are a number of places where this matters - almost every date query with user input in particular since those are typically done based on day ranges.

Andrei Ignat
February 20, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

Wrote also about JsonResult in MVC and the corresponding problems in reading in HTML .
See
http://msprogrammer.serviciipeweb.ro/2013/03/10/mvc-jsonresult-datetime-and-timezone/

MarcelDevG
February 24, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

Hi Rick,

I'm with you on the datetime offset/datetime issue. On the server I only want to deal with UTC dates.
But I wonder why you don't use the javascript method getTimezoneOffset() of a Date on the client to get the timezone of the user ?

Marcel

Matt Roberts
April 09, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

Stellar job, this was a really nice summary of some techniques I've used and then forgotten about and had to re-google, thanks!

Matt Johnson
April 19, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

Great post. Thanks for bringing awareness to my favorite subject! Though if you don't mind, I'm going to tear it apart. :) Allow me to clarify a few points:

TimeZoneInfo.ConvertTimeFromUtc will throw an exception if passed a DateTime with Local kind. If it's unspecified, it will assume UTC. So the ToTimeZoneTime extension method will not behave as it says in the comments, accepting a Utc or local time. The input time parameter must be in terms of UTC. The way you used it, coming from DateTime.UtcNow, is just fine.

Regarding caching - It's good to hold on to a TimeZoneInfo object for as long as you need it within the local scope. However, I don't recommend building your own cache, as the FindSystemTimeZoneById method already uses a lookup cache internally.

Next, with regard to displaying the time zone names in a drop-down - While there's nothing inherently wrong with your approach, it's important to note that the DisplayName property will be localized by the OS language of the server (which might not be English). The usual localization methods in .NET don't apply to the TimeZoneInfo object. In a multilingual app, one may need to use the TimeZoneInfo.Id to look up a display name from a resource file, rather than use the DisplayName property. Alternatively, one could use my TimeZoneNames library. (http://mj1856.github.io/TimeZoneNames/)

With regard to capturing the user's default time zone. Sorry, but the approach you describe is flawed. The problem is that every implementation of JavaScript is free to present time zones in whatever manner they wish. The spec is not refined enough to rely on it having any particular set of values. In particular, the example you show of "Hawaiian Standard Time" mapping back to the TimeZoneInfo ID is just a coincidence. Worldwide, typically they will NOT match up. Also, the JavaScript names will indeed switch out for DST, while the TimeZoneInfo.Id will not. So "Eastern Standard Time" will always be the ID for the US Eastern time zone, even when JavaScript shows "Eastern Daylight Time". Additionally, some browsers will just show an abbreviation (HST, EST, EDT, etc.), or may localize the time zone string to the user's language.

Time zone detection is actually quite difficult, which is why it makes sense to ask your user in a drop-down list. There are libraries like jsTimeZoneDetect which try to guess your time zone, but it is not 100% accurate. There is the JavaScript getTimeZoneOffset method, but that only returns the offset associated with the date you called it on - which is not enough to determine the actual time zone. There's also a newer way, supported by Chrome and Opera, but not IE, FF, or Safari: Intl.DateTimeFormat().resolvedOptions().timeZone - Hopefully that will catch on in other browsers, but today - there's no universal way to detect the time zone.

Moving along - The GetUserTime method looks fine, but in your GetUtcUserTime method, there's a bug. You call ToUniversalTime on the result of the time zone conversion. So that's DateTime.ToUniversalTime, which will still use the time zone of the server - not the destination time zone. You would need to use TimeZoneInfo.ConvertTimeToUtc to avoid that problem, rather than ToUniversalTime. However, I'm a bit confused why you would need this method to begin with, or what it's actually doing. If the time is in the user's time zone, then you would just have one call to TimeZoneInfo.ConvertTimeToUtc. If the time is in the server's time zone, then you would just have one call to DateTime.ToUniversalTime. The mix here doesn't make a lot of sense.

Moving further down your post, in the section on Date range queries - again, calling ToUniversalTime on a DateTime will use the server's local time zone, not the user's time zone. So the query boundaries are not correctly aligned to the user. Again, you'd need to use TimeZoneInfo.ConvertTimeToUtc.

In the User Captured Time section, the DateMath function could just be a single call to TimeZoneInfo.ConvertTimeToUtc, passing in the user's time zone. The manual manipulation of offsets is not only unnecessarily, but it's slightly flawed in that you expect the "offset" and "offsetLocal" values to be reflecting the offset for the same "start" time, but that's defined by the user's time zone - so you're not really referring to the same instant. This will show up if either time zone is near a DST transition. It's possible that your local time zone could be on one side of the transition, and the user's time zone could be on the other side.

The same easy-to-make mistake is in you AdjustTimeZoneOffset class. It will work if the "localTime" parameter has Kind=Local, or Kind=Utc, but not necessarily will it be correct in all cases when Kind=Unspecified. Think of it this way - the TimeZoneInfo.GetUtcOffset(DateTime) *prefers* to work with Unspecified kinds, as those mean that the time is relative to to the time zone of that particular TimeZoneInfo object. When it gets a Local or Utc kind, it converts to that time zone first. (This is inverted from most of the other methods that assume Unspecified means Local or Utc).

The theme throughout all of this feedback is that you should be working directly with the user's time zone and UTC - avoiding your own servers local time zone whenever possible. Really, the time zone of the server should be considered irrelevant. Avoid any use of "local" time (DateTime.Now, DateTime.ToUniversalTime, DateTime.ToLocalTime, DateTimeKind.Local, etc.) and everything will be much simpler.

Nitpick: "DayLight Savings Time" => "Daylight Saving Time". Daylight is one word, and the second word should have no "s" at the end. Think "I'm saving daylight during this time" - which is arguable, but nonetheless, that's the term's origin.

With regard to DateTimeOffset - I'll disagree with you completely. DateTimeOffset is essential in a web application, and it addresses much more than just a single scenario. You're also not tied to a single time zone, as the offset can be adjusted to any time zone you might be in. The offset doesn't track the time zone, just the offset for the time zone that happens to be in effect. Remember, there's much more to a time zone than just an offset, as many time zones switch between more than one offset due to daylight saving time.

The essential part you're missing about DateTimeOffset is that it's the only way to effectively disambiguate between ambiguous local values. For example, if you have 2014-11-02 01:00 as a DateTime, and you're in the US Pacific time zone, you have a problem because you don't know if that's PDT (-07:00), or PST (-08:00). The DateTimeOffset keeps the offset intact, so you affirmatively know which of the two possibilities you're using. If you store a UTC time, or if the time zone doesn't use DST (like Hawaii), then you're OK. But in cases where the time zone uses DST, not storing the offset could mean that you are losing data - you'd potentially be using a value as an hour before or after the intended moment.

You're right about the performance of DateTime.Now, and TimeZoneInfo. In fact Noda Time is one of the ways you can improve upon performance, as it is thoroughly optimized for perf.

Speaking of Noda Time, I'll disagree with you that you have to replace everything throughout your system. Sure, if you do, you'll have a lot less opportunity to make mistakes, but you certainly can just use Noda Time where it makes sense. I've personally worked on systems that needed to do time zone conversions using IANA time zones (ex. "America/Los_Angeles"), but tracked everything else in DateTime and DateTimeOffset types. It's actually quite common to see Noda Time used extensively in application logic, but left completely out of the DTOs and persistence layers. In some technologies, like Entity Framework, you couldn't use Noda Time directly if you wanted to - because there's no where to hook it up. Others, such as Json.Net, Dapper, and RavenDB have extension libraries for Noda Time so you can use it there if you want to, but there's still no hard requirement that you do.

In general, I think your post is great in that you are covering most of the areas of concern, where attention needs to be paid to converting between time zones. But like many things, the devil is in the details. Even you, who are more familiar with these functions than most, still were able to make mistakes like calling ToUniversalTime on a non-local DateTime, or calling GetUtcOffset with a value from a different time zone. Noda Time won't let you get into trouble, because the API prevents you from calling methods that have confusing or ambiguous behavior.

Hope this feedback was helpful.
-Matt

Matt Johnson
April 19, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

I've blogged with detail in response to one particularly problematic part of this blog post.
http://codeofmatt.com/2015/04/20/beware-the-edge-cases-of-time/

Cheers!

Ron Birk
June 09, 2015

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

Using "var dt = new Date().toString();" doesn't seem to work on Chinese browsers (for example) and also not on IE9. As the time zone will either be localized or not shown at all depending on language and browser version.

Luke Puplett
April 20, 2017

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

Reading these comments again, unless Rick is not explaining clearly enough (it's arduous to explain this stuff) then I think Rick is mistaken and that a system using DTO entirely will work for his needs.

@Rick, the DTO is effectively an ISO1806 structure. The offset it stores is from UTC, not from the local time. I believe you do have all the information needed to convert or display or review in many years time, captured in that single format.

(Local time + difference to UTC at the time)

That's all you'll ever need. Come up with a good scenario where that wouldn't work, I can't.

I think storing as UTC is where everyone gets it wrong, it's destructive, it moves the time observed by the user and then throws away the offset.


Edgar Ricárdez
September 09, 2017

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

Hi @Rick, thanks for this post.

With the client value: Central Daylight Time (Mexico), I get TimeZoneExceptionNotFound.

Regards


Dave
July 19, 2019

# re: Back to Basics: UTC and TimeZones in .NET Web Apps

DateTimeOffset never appealed to me either but I think the main advantage is the arithmetic operations that DateTimeOffSet brings, can easily add/subtract DateTimeOffSet from different timezones. Whichever way you go (DateTime vs DateTimeOffSet) to have the perfect scenario you need to get the user TimeZone and convert. With DateTimeOffSet even if you don't implement TimeZone conversions, if you implement request localisation you get a pretty good package out of the box (UTC stored in db based on Accept-Language) and can add TimeZones for db save and display at a later stage


West Wind  © Rick Strahl, West Wind Technologies, 2005 - 2024