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:
which drops down a list retrieved with TimeZoneInfo.GetSystemTimeZones:
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.
Other Posts you might also like