How to elegantly deal with timezones

69,169

Solution 1

Not that this is a recommendation, its more sharing of a paradigm, but the most agressive way I've seen of handling timezone information in a web app (which is not exclusive to ASP.NET MVC) was the following:

  • All date times on the server are UTC. That means using, like you said, DateTime.UtcNow.

  • Try to trust the client passing dates to the server as little as possible. For example, if you need "now", don't create a date on the client and then pass it to the server. Either create a date in your GET and pass it to the ViewModel or on POST do DateTime.UtcNow.

So far, pretty standard fare, but this is where things get 'interesting'.

  • If you have to accept a date from the client, then use javascript to make sure the data that you are posting to the server is in UTC. The client knows what timezone it is in, so it can with reasonable accuracy convert times into UTC.

  • When rendering views, they were using the HTML5 <time> element, they would never render datetimes directly in the ViewModel. It was implemented as as HtmlHelper extension, something like Html.Time(Model.when). It would render <time datetime='[utctime]' data-date-format='[datetimeformat]'></time>.

    Then they would use javascript to translate UTC time into the clients local time. The script would find all the <time> elements and use the date-format data property to format the date and populate the contents of the element.

This way they never had to keep track of, store, or manage a clients timezone. The server didn't care what timezone the client was in, nor had to do any timezone translations. It simply spit out UTC and let the client convert that into something that was reasonable. Which is easy from the browser, because it knows what timezone it is in. If the client changed his/her timezone, the web application would automatically update itself. The only thing that they stored were the datetime format string for the locale of the user.

I'm not saying it was the best approach, but it was a different one that I had not seen before. Maybe you'll glean some interesting ideas from it.

Solution 2

After several feedbacks, here is my final solution which I think is clean and simple and covers daylight saving issues.

1 - We handle the conversion at model level. So, in the Model class, we write:

    public class Quote
    {
        ...
        public DateTime DateCreated
        {
            get { return CRM.Global.ToLocalTime(_DateCreated); }
            set { _DateCreated = value.ToUniversalTime(); }
        }
        private DateTime _DateCreated { get; set; }
        ...
    }

2 - In a global helper we make our custom function "ToLocalTime":

    public static DateTime ToLocalTime(DateTime utcDate)
    {
        var localTimeZoneId = "China Standard Time";
        var localTimeZone = TimeZoneInfo.FindSystemTimeZoneById(localTimeZoneId);
        var localTime = TimeZoneInfo.ConvertTimeFromUtc(utcDate, localTimeZone);
        return localTime;
    }

3 - We can improve this further, by saving the timezone id in each User profile so we can retrieve from the user class instead of using constant "China Standard Time":

public class Contact
{
    ...
    public string TimeZone { get; set; }
    ...
}

4 - Here we can get the list of timezone to show to user to select from a dropdownbox:

public class ListHelper
{
    public IEnumerable<SelectListItem> GetTimeZoneList()
    {
        var list = from tz in TimeZoneInfo.GetSystemTimeZones()
                   select new SelectListItem { Value = tz.Id, Text = tz.DisplayName };

        return list;
    }
}

So, now at 9:25 AM in China, Website hosted in USA, date saved in UTC at database, here is the final result:

5/9/2013 6:25:58 PM (Server - in USA) 
5/10/2013 1:25:58 AM (Database - Converted UTC)
5/10/2013 9:25:58 AM (Local - in China)

EDIT

Thanks to Matt Johnson for pointing out the weak parts of original solution, and sorry for deleting original post, but got issues getting right code display format... turned out the editor has problems mixing "bullets" with "pre code", so I removed the bulles and it was ok.

Solution 3

In the events section on sf4answers, users enter an address for an event, as well as a start date and an optional end date. These times are translated into a datetimeoffset in SQL server that accounts for the offset from UTC.

This is the same problem you are facing (although you are taking a different approach to it, in that you are using DateTime.UtcNow); you have a location and you need to translate a time from one timezone to another.

There are two main things I did which worked for me. First, use DateTimeOffset structure, always. It accounts for offset from UTC and if you can get that information from your client, it makes your life a little easier.

Second, when performing the translations, assuming you know the location/time zone that the client is in, you can use the public info time zone database to translate a time from UTC to another time zone (or triangulate, if you will, between two time zones). The great thing about the tz database (sometimes referred to as the Olson database) is that it accounts for the changes in time zones throughout history; getting an offset is a function of the date that you want to get the offset on (just look at the Energy Policy Act of 2005 which changed the dates when daylight savings time goes into effect in the US).

With the database in hand, you can use the ZoneInfo (tz database / Olson database) .NET API. Note that there isn't a binary distribution, you'll have to download the latest version and compile it yourself.

At the time of this writing, it currently parses all of the files in the latest data distribution (I actually ran it against the ftp://elsie.nci.nih.gov/pub/tzdata2011k.tar.gz file on September 25, 2011; in March 2017, you'd get it via https://iana.org/time-zones or from ftp://fpt.iana.org/tz/releases/tzdata2017a.tar.gz).

So on sf4answers, after getting the address, it is geocoded into a latitude/longitude combination and then sent to a third-party web service to get a timezone which corresponds to an entry in the tz database. From there, the start and end times are converted into DateTimeOffset instances with the proper UTC offset and then stored in the database.

As for dealing with it on SO and websites, it depends on the audience and what you are trying to display. If you notice, most social websites (and SO, and the events section on sf4answers) display events in relative time, or, if an absolute value is used, it's usually UTC.

However, if your audience expects local times, then using DateTimeOffset along with an extension method that takes the time zone to convert to would be just fine; the SQL data type datetimeoffset would translate to the .NET DateTimeOffset which you can then get the universal time for using the GetUniversalTime method. From there, you simply use the methods on the ZoneInfo class to convert from UTC to local time (you'll have to do a little work to get it into a DateTimeOffset, but it's simple enough to do).

Where to do the transformation? That's a cost you are going to have to pay somewhere, and there's no "best" way. I'd opt for the view though, with the timezone offset as part of the view model presented to the view. That way, if the requirements for the view change, you don't have to change your view model to accommodate the change. Your JsonResult would simply contain a model with the IEnumerable<T> and the offset.

On the input side, using a model binder? I'd say absolutely no way. You can't guarantee that all the dates (now or in the future) will have to be transformed in this way, it should be an explicit function of your controller to perform this action. Again, if the requirements change, you don't have to tweak one or many ModelBinder instances to adjust your business logic; and it is business logic, which means it should be in the controller.

Solution 4

This is just my opinion, I think that MVC application should separate well data presentation problem from data model management. A database can store data in local server time but it's a duty of the presentation layer to render datetime using local user timezone. This seems to me the same problem as I18N and number format for different countries. In your case, your application should detect the Culture and timezone of the user and change the View showing different text, number and datime presentation, but the stored data can have the same format.

Solution 5

For output, create an display/editor template like this

@inherits System.Web.Mvc.WebViewPage<System.DateTime>
@Html.Label(Model.ToLocalTime().ToLongTimeString()))

You can bind them based on attributes on your model if you want only certain models to use those templates.

See here and here for more details on creating custom editor templates.

Alternatively, since you want it to work for both input and output, I would suggest extending a control or even creating your own. That way you can intercept both the input and outputs and convert the text/value as needed.

This link will hopefully push you in the right direction if you want to go down that path.

Either way, if you want an elegant solution, its going to be a bit of work. On the bright side, once you have done it once you can keep it in your code library for future use!

Share:
69,169

Related videos on Youtube

TheCloudlessSky
Author by

TheCloudlessSky

http://adrianphinney.com

Updated on November 10, 2020

Comments

  • TheCloudlessSky
    TheCloudlessSky over 3 years

    I have a website that is hosted in a different timezone than the users using the application. In addition to this, users can have a specific timezone. I was wondering how other SO users and applications approach this? The most obvious part is that inside the DB, date/times are stored in UTC. When on the server, all date/times should be dealt with in UTC. However, I see three problems that I'm trying to overcome:

    1. Getting the current time in UTC (solved easily with DateTime.UtcNow).

    2. Pulling date/times from the database and displaying these to the user. There are potentially lots of calls to print dates on different views. I was thinking of some layer in between the view and the controllers that could solve this issue. Or having a custom extension method on DateTime (see below). The major down side is that at every location of using a datetime in a view, the extension method must be called!

      This would also add difficulty to using something like the JsonResult. You could no longer easily call Json(myEnumerable), it would have to be Json(myEnumerable.Select(transformAllDates)). Maybe AutoMapper could help in this situation?

    3. Getting input from the user (Local to UTC). For example, POSTing a form with a date would require converting the date to UTC before. The first thing that comes to mind is creating a custom ModelBinder.

    Here's the extensions that I thought of using in the views:

    public static class DateTimeExtensions
    {
        public static DateTime UtcToLocal(this DateTime source, 
            TimeZoneInfo localTimeZone)
        {
            return TimeZoneInfo.ConvertTimeFromUtc(source, localTimeZone);
        }
    
        public static DateTime LocalToUtc(this DateTime source, 
            TimeZoneInfo localTimeZone)
        {
            source = DateTime.SpecifyKind(source, DateTimeKind.Unspecified);
            return TimeZoneInfo.ConvertTimeToUtc(source, localTimeZone);
        }
    }
    

    I would think that dealing with timezones would be such a common thing considering a lot of applications are now cloud-based where the server's local time could be much different than the expected time zone.

    Has this been elegantly solved before? Is there anything that I'm missing? Ideas and thoughts are much appreciated.

    EDIT: To clear some confusion I thought add some more details. The issue right now isn't how to store UTC times in the db, it's more about the process of going from UTC->Local and Local->UTC. As @Max Zerbini points out, it's obviously smart to put the UTC->Local code in the view, but is using the DateTimeExtensions really the answer? When getting input from the user, does it make sense to accept dates as the user's local time (since that's what JS would be using) and then use a ModelBinder to transform to UTC? The user's timezone is stored in the DB and is easily retrieved.

    • dodgy_coder
      dodgy_coder over 12 years
      You might first have a read through this excellent post ... Daylight saving time and Timezone best practices
    • TheCloudlessSky
      TheCloudlessSky over 12 years
      @dodgy_coder - That has always been a great resource link for timezones. However, it doesn't really solve any of my problems (specifically pertaining to MVC). Thanks though.
    • Arnis Lapsa
      Arnis Lapsa over 12 years
      code.google.com/p/noda-time might be useful
    • Sean
      Sean almost 12 years
      Curious which solution you have opted into. Facing similar decision myself. Good question.
    • TheCloudlessSky
      TheCloudlessSky almost 12 years
      @Sean - The solution thus far hasn't been elegant (which is why I've yet to accept an answer). It's been a lot of manual overhead of printing dates/times and manually converting them back with a ModelBinder.
    • Matt Johnson-Pint
      Matt Johnson-Pint almost 11 years
      There is an underlying assumption in your question that the user is always interested in the precise UTC moment, translated to their own time zone. That is not always the case. For example, I may be looking at times in someone else's time zone, or times that are the same local time but intentionally different UTC moments (such as TV shows).
    • Matthew
      Matthew over 9 years
      @TheCloudlessSky - Regarding your comment "it doesn't really solve any of my problems (specifically pertaining to MVC).", your issue of handling times and timezone conversions is not related to MVC. The challenge you are expressing exists independent of that particular framework.
    • TheCloudlessSky
      TheCloudlessSky over 9 years
      @Matt No, this question is specifically asking about handing timezones for MVC. Check the tags.
    • Matthew
      Matthew over 9 years
      @thecloudlesssky-I think you might be confusing intent w/ result. First I understand that you were interested in this question as it related to asp.net mvc however the actual words in your question barely reference mvc at all with exception of a modelbinder. Second the answer to your question (as asked) is not mvc specific. So when I say your question isn't related to mvc I'm not saying what is in your mind I am saying that the question as written and the answer to it are not mvc specific. Third the answer which you yourself marked as accepted explicitly (cont...)
    • Matthew
      Matthew over 9 years
      @thecloudlesssky-(cont) says, "...(which is not exclusive to asp.net mvc)..." Fourth Im not the only responder who felt this way, in fact you commented on one of the answers saying it didnt relate to asp.net mvc. In summary the challenge you are facing would be the same for web forms, windows forms etc-but if you want an mvc specific answer you should make that clear in the title or body of the question not just via a tag, as that tag is likely to be ignored when none of the other parts of the post mention or relate to mvc.
    • TheCloudlessSky
      TheCloudlessSky over 9 years
      @Matt If you have something to add, add it as an answer (that is specific to MVC if you want). Just because the answer applies to other frameworks, doesn't mean it doesn't answer the question. I mention both JsonResult and ModelBinding in the original question. You're nitpicking for no reason.
  • TheCloudlessSky
    TheCloudlessSky over 12 years
    Thanks for your detailed response. I hope I don't come across as rude, but this didn't really answer any of my concerns with ASP.NET MVC: What about user input (would using a custom model binder suffice)? And most importantly, what about displaying these dates (in the user's local timezone)? I feel like the extension method is going to add a lot of weight to my views that seems unnecessary.
  • casperOne
    casperOne over 12 years
    @TheCloudlessSky: See the last two paragraphs of my edited response. Personally, I think the details of where to do the transformations are minor; the major issue is the actual conversion and storage of the datetime data (btw, I can't emphasize enough the use of datetimeoffset in SQL Server and DateTimeOffset in .NET here, they really simplify things tremendously) which I believe .NET doesn't handle adequately at all for the reasons outlined above. If you have a date in NYC that was entered in 2003, and then you want it translated to a date in LA in 2011, .NET fails hard in this case.
  • TheCloudlessSky
    TheCloudlessSky over 12 years
    Thanks for your response. Yeah this is what I've basically described, I'm just wondering how this could be elegantly achieved with MVC. The app stores the user's timezone as part of their account creation (they pick their timezome). The problem again comes with how to render the dates in each view without (hopefully) having to litter them with the methods that I created for DateTimeExtensions.
  • TheCloudlessSky
    TheCloudlessSky over 12 years
    The problem with not using a model binder (or any layer before the action) is that every controller becomes very hard to test when working with dates (they'd all gain a dependency on the date-conversion). This would allow unit test dates to be written in UTC. My application has users that are associated with a profile (think doctors/secretaries associated with their practice). The profile holds the timezone information. Therefore, it's pretty easily to get the current user's profile's timezone and transform during binding. Do you have other arguments against this? Thanks for your input!
  • TheCloudlessSky
    TheCloudlessSky over 12 years
    Thanks for your response. When getting date input from the user (for example scheduling an appointment), isn't this input assumed to be in their current timezone? What do you think about the ModelBinder doing this transformation before entering the action (see my comments to @casperOne. Also, the <time> idea is pretty cool. The only disadvantage is that it means that I need to query the whole DOM for these elements, which isn't exactly nice just to transform dates (what about JS disabled?). Thanks again!
  • Massimo Zerbini
    Massimo Zerbini over 12 years
    There are many ways to do this, one is to use helper methods as you suggested, another perhaps more complex but elegant is the use of a filter that processes the requests and responses and convert datetime. Another way is to develop a custom Display attribute to annotate the fields of type DateTime of your view-model.
  • TheCloudlessSky
    TheCloudlessSky over 12 years
    Those are the kinds of things I was looking for. If you look at my OP, I also mentioned using AutoMapper to do some processing before going to the view/json result but after the action has executed.
  • J. Holmes
    J. Holmes over 12 years
    Usually, yes the assumption is that it would be in a local timezone. But they made a point to make sure that any time they collected a time from a user, it was transformed into UTC using javascript before it was sent to the server. Their solution was very JS heavy, and didn't degrade very nicely when JS was off. I haven't thought about it enough to see if there is a clever around that. But like I said maybe it'll give you some ideas.
  • casperOne
    casperOne over 12 years
    The case against that is that your tests would not accurately reflect the test cases. Your input is not going to be in UTC, so your test cases shouldn't be crafted to use it. It should use real-world dates with locations and all (although if you use the DateTimeOffset you will mitigate this a great deal, IMO).
  • TheCloudlessSky
    TheCloudlessSky over 12 years
    Yes - but this means that every test that deals with dates has to account for this. Every action that deals with datetimes will always convert to UTC. To me this is a prime candidate for the model binder and then test the model binder.
  • Kevin Stricker
    Kevin Stricker over 12 years
    I think custom display/editor templates and binder is the most elegant solution, because it will "just work" once implemented. No developer will need to know anything special to get the dates working right just use DisplayFor and EditorFor and it will work every time. +1
  • Alex
    Alex about 12 years
    wouldn't this render the application server's time and not the browser's time?
  • TheCloudlessSky
    TheCloudlessSky almost 11 years
    I've since worked on another project that needed local dates and this was the method that I used. I'm using moment.js to do the date/time formatting... it's sweet. Thanks!
  • Uri Abramson
    Uri Abramson over 10 years
    Isn't there a simple way to define in the app.config / web.cofig of the application an Application-scope timezone and make it turn all DateTime.Now values into DateTime.UtcNow? (I want to avoid situations where programmers in the company would still use DateTime.Now by mistake)
  • shenku
    shenku about 10 years
    One dissadvantage to this approach that I can see is when you are using the time for other things, creating pdf's, emails and such, there is not time element for those so you will still have to convert those manually. Otherwise, very neat solution
  • Vikash Kumar
    Vikash Kumar almost 9 years
    It seems to be workable if some one does not have n-tier architecture or Web libraries are shared. How we can share timezone id to data layer.
  • Manik Arora
    Manik Arora about 8 years
    Just to confirm, shall we keep the timezone of server machine to UTC?
  • Soner Gönül
    Soner Gönül almost 6 years
    Some links seems dead casper.
  • Jesse Chisholm
    Jesse Chisholm about 5 years
    re: JS-less graceful degrading If each date entry field has a hidden companion field flagging it as local, the JS could change that flag to UTC and the server would at least know it had funky data. If JS isn't available, then the hidden flag remains local by fiat.