DateTime.Now and Culture/Timezone specific

Billa picture Billa · Dec 25, 2013 · Viewed 43.4k times · Source

Our application was designed to handle user from different Geographic location.

We are unable to detect what is the current end user local time and time zone operate on it. They select different culture like sv-se, en-us, ta-In even they access from Europe/London timezone..

We hosted it in a hosting server in US, application users are from Norway/Denmark/Sweden/UK/USA/India

The problem is we used DateTime.Now to store the record created/updated date, etc.

Since the Server runs in USA all user data are saved as US time :(

After researching in SO, we decided to stored all history dates in DB as DateTime.UtcNow

PROBLEM:

enter image description here

There is a record created on 29 Dec 2013, 3:15 P.M Swedish time.

 public ActionResult Save(BookingViewModel model)
    {
        Booking booking = new Booking();
        booking.BookingDateTime = model.BookingDateTime; //10 Jan 2014 2:00 P.M
        booking.Name = model.Name;
        booking.CurrentUserId = (User)Session["currentUser"].UserId;
        //USA Server runs in Pacific Time Zone, UTC-08:00
        booking.CreatedDateTime = DateTime.UtcNow; //29 Dec 2013, 6:15 A.M
        BookingRepository.Save(booking);
        return View("Index");
    }

We want to show the same history time to the user who logged in in India/Sweden/USA.

As of now we are using current culture user logged in and choose the timezone from a config file and using for conversion with TimeZoneInfo class

<appSettings>
    <add key="sv-se" value="W. Europe Standard Time" />
    <add key="ta-IN" value="India Standard Time" />
</appSettings>

    private DateTime ConvertUTCBasedOnCulture(DateTime utcTime)
    {
        //utcTime is 29 Dec 2013, 6:15 A.M
        string TimezoneId =                  
                System.Configuration.ConfigurationManager.AppSettings
                [System.Threading.Thread.CurrentThread.CurrentCulture.Name];
        // if the user changes culture from sv-se to ta-IN, different date is shown
        TimeZoneInfo tZone = TimeZoneInfo.FindSystemTimeZoneById(TimezoneId);

        return TimeZoneInfo.ConvertTimeFromUtc(utcTime, tZone);
    }
    public ActionResult ViewHistory()
    {
        List<Booking> bookings = new List<Booking>();
        bookings=BookingRepository.GetBookingHistory();
        List<BookingViewModel> viewModel = new List<BookingViewModel>();
        foreach (Booking b in bookings)
        {
            BookingViewModel model = new BookingViewModel();
            model.CreatedTime = ConvertUTCBasedOnCulture(b.CreatedDateTime);
            viewModel.Add(model);
        }
        return View(viewModel);
    }

View Code

   @Model.CreatedTime.ToString("dd-MMM-yyyy - HH':'mm")

NOTE: The user can change the culture/language before they login. Its a localization based application, running in US server.

I have seen NODATIME, but I could not understand how it can help with multi culture web application hosted in different location.

Question

How can I show a same record creation date 29 Dec 2013, 3:15 P.M for the users logged in INDIA/USA/Anywhere`?

As of now my logic in ConvertUTCBasedOnCulture is based user logged in culture. This should be irrespective of culture, since user can login using any culture from India/USA

DATABASE COLUMN

CreatedTime: SMALLDATETIME

UPDATE: ATTEMPTED SOLUTION:

DATABASE COLUMN TYPE: DATETIMEOFFSET

UI

Finally I am sending the current user's local time using the below Momento.js code in each request

$.ajaxSetup({
    beforeSend: function (jqXHR, settings) {
        try {
      //moment.format gives current user date like 2014-01-04T18:27:59+01:00
            jqXHR.setRequestHeader('BrowserLocalTime', moment().format());
        }
        catch (e) {
        }
    }
});

APPLICATION

public static DateTimeOffset GetCurrentUserLocalTime()
{
    try
    {
      return 
      DateTimeOffset.Parse(HttpContext.Current.Request.Headers["BrowserLocalTime"]);
    }
    catch
    {
        return DateTimeOffset.Now;
    }
}

then called in

 model.AddedDateTime = WebAppHelper.GetCurrentUserLocalTime();

In View

@Model.AddedDateTime.Value.LocalDateTime.ToString("dd-MMM-yyyy - HH':'mm")

In view it shows the local time to user, however I want to see like dd-MMM-yyyy CET/PST (2 hours ago).

This 2 hours ago should calculate from end user's local time. Exactly same as stack overflow question created/edited time with Timezone display and local user calculation.

Example: answered Jan 25 '13 at 17:49 CST (6 hours/days/month ago) So the other viewing from USA/INDIA user can really understand this record was created exactly 6 hours from INDIA/USA current time

Almost I think I achieved everything, except the display format & calculation. How can i do this?

Answer

Jon Skeet picture Jon Skeet · Dec 30, 2013

It sounds like you need to store a DateTimeOffset instead of a DateTime. You could just store the local DateTime to the user creating the value, but that means you can't perform any ordering operations etc. You can't just use DateTime.UtcNow, as that won't store anything to indicate the local date/time of the user when the record was created.

Alternatively, you could store an instant in time along with the user's time zone - that's harder to achieve, but would give you more information as then you'd be able to say things like "What is the user's local time one hour later?"

The hosting of the server should be irrelevant - you should never use the server's time zone. However, you will need to know the appropriate UTC offset (or time zone) for the user. This cannot be done based on just the culture - you'll want to use Javascript on the user's machine to determine the UTC offset at the time you're interested in (not necessarily "now").

Once you've worked out how to store the value, retrieving it is simple - if you've already stored the UTC instant and an offset, you just apply that offset and you'll get back to the original user's local time. You haven't said how you're converting values to text, but it should just drop out simply - just format the value, and you should get the original local time.

If you decide to use Noda Time, you'd just use OffsetDateTime instead of DateTimeOffset.