How to show UTC time relative to the user's local time on a .NET website

Posted written by Paul Seal on October 02, 2017 .NET Framework

This was harder than it should be

This post gives you the code I found to help you get a user's country and culture code and display a UTC date and time relative to the user who is viewing in their browser.

How does it work?

There is some code to resolve the culture and country from the user's browser. Then there is a method to get the time zone based on the country code (this falls down when you have countries like USA with multiple time zones). It uses a NuGet package called NodaTime to help with the time zones. Once you have the time zone there is a method to create a new local date using the utc datetime and the time zone. It also uses session to store the resolved country and caching for the time zone.

How do you use it?

I created it as an extension method for DateTime, so you can do the following:

DateTime.UtcNow.ConvertUtcToLocalDateTime().ToString()

Before you get started.

You will need to install NodaTime from NuGet, so open Package Manager Console and add it using this command.

Install-Package NodaTime -Version 2.2.0

If you want to install the latest version then take off "-Version 2.2.0"

Here's the code:

using NodaTime;
using NodaTime.TimeZones;
using System;
using System.Globalization;
using System.Linq;
using System.Web;

namespace CodeShare.Library.Globalization
{

    /// <summary>
    /// A set of methods to help display Utc DateTimes based on the language and country code in the user's browser.
    /// </summary>
    public static class CultureHelper
    {

        /// <summary>
        /// Gets and sets the country code in the session to save keep resolving it from the browser every time.
        /// </summary>
        /// <returns>A two letter country code</returns>
        private static string UserCountryCode()
        {
            const string sessionKeyName = "UserCountryCode";
            string countryCode = "";

            if (HttpContext.Current.Session[sessionKeyName] == null)
            {
                countryCode = ResolveCountry().ToString();
                HttpContext.Current.Session[sessionKeyName] = countryCode;
            }
            else
            {
                countryCode = (string)HttpContext.Current.Session[sessionKeyName];
            }
            return countryCode;
        }

        /// <summary>
        /// Gets the Culture from the browser. Found this here:
        /// https://madskristensen.net/post/get-language-and-country-from-a-browser-in-aspnet
        /// </summary>
        /// <returns>A CultureInfo object based on the user language from the browser</returns>
        public static CultureInfo ResolveCulture()
        {
            string DEFAULT_CULTURE = System.Web.Configuration.WebConfigurationManager.AppSettings["DefaultCountryLCID"].ToLower();
            string[] languages = HttpContext.Current.Request.UserLanguages;

            if (languages == null || languages.Length == 0)
                return CultureInfo.CreateSpecificCulture(DEFAULT_CULTURE);

            try
            {
                string language = languages[0].ToLowerInvariant().Trim();
                return CultureInfo.CreateSpecificCulture(language);
            }
            catch (ArgumentException)
            {
                return CultureInfo.CreateSpecificCulture(DEFAULT_CULTURE);
            }
        }

        /// <summary>
        /// Gets the Culture from the browser. Found this here:
        /// https://madskristensen.net/post/get-language-and-country-from-a-browser-in-aspnet
        /// </summary>
        /// <returns>A RegionInfo object based the users CultureInfo</returns>
        public static RegionInfo ResolveCountry()
        {
            int DEFAULT_LCID = int.Parse(System.Web.Configuration.WebConfigurationManager.AppSettings["DefaultCountryLCID"]);
            CultureInfo culture = ResolveCulture();
            return new RegionInfo(culture != null ? culture.LCID : DEFAULT_LCID);
        }

        /// <summary>
        /// An extension method for DateTime. It converts a Utc DateTime to a local date time using the user's time zone
        /// </summary>
        /// <param name="utcDateTime">The datetime object which the extension method is called from</param>
        /// <param name="cachingTimeInMins">Caching time in minutes, used so you don't need to keep getting the timezone every time.</param>
        /// <returns>A DateTime object local to the user</returns>
        public static DateTime ConvertUtcToLocalDateTime(this DateTime utcDateTime)
        {
            int cachingTimeInMins = int.Parse(System.Web.Configuration.WebConfigurationManager.AppSettings["TimeZoneCachingTimeInMins"]);
            //I got this line from StackOverflow. It helped a lot at the end. https://stackoverflow.com/a/41662352/4782728
            var newUtcDateTime = DateTime.SpecifyKind(utcDateTime, DateTimeKind.Utc);
            DateTimeZone TimeZone = GetDateTimeZoneFromCache(UserCountryCode(), cachingTimeInMins);
            DateTime objdate = Instant.FromDateTimeUtc(newUtcDateTime)
                      .InZone(TimeZone)
                      .ToDateTimeUnspecified();
            return objdate;
        }

        /// <summary>
        /// Gets the time zone using the country code
        /// I got this code from here https://stackoverflow.com/a/24907552/4782728. I'm so grateful as it really helped.
        /// </summary>
        /// <param name="countryCode">Two digit country code.</param>
        /// <returns>A DateTimeZone from NodaTime based on the country code.</returns>
        public static DateTimeZone GetDateTimeZoneFromCountryCode(string countryCode)
        {
            var CountryInfo = (from location in TzdbDateTimeZoneSource.Default.ZoneLocations
                               where location.CountryCode.Equals(countryCode,
                                          StringComparison.OrdinalIgnoreCase)
                               select new { location.ZoneId, location.CountryName })
                             .FirstOrDefault();
            DateTimeZone TimeZone = DateTimeZoneProviders.Tzdb[CountryInfo.ZoneId];
            return TimeZone;
        }

        /// <summary>
        /// Calls the method for getting the TimeZone from the country code, but sets the country code before calling it. 
        /// Helps to do this when using caching and the delegate method would have had parameters.
        /// </summary>
        /// <returns>A DateTimeZone from NodaTime based on the country code.</returns>
        public static DateTimeZone GetDateTimeZone()
        {
            return GetDateTimeZoneFromCountryCode(UserCountryCode());
        }

        /// <summary>
        /// Gets the DateTimeZone from the cache, or from the GetDateTimeZone if it's not in the cache yet.
        /// See this post for a simple .NET caching example. http://www.codeshare.co.uk/blog/simple-reusable-net-caching-example-code-in-c/
        /// </summary>
        /// <param name="countryCode">Two digit country code.</param>
        /// <param name="cachingTimeInMins">Caching time in minutes, used so you don't need to keep getting the timezone every time.</param>
        /// <returns></returns>
        public static DateTimeZone GetDateTimeZoneFromCache(string countryCode, int cachingTimeInMins)
        {
            return Caching.GetObjectFromCache("dateTimeZone-" + countryCode, cachingTimeInMins, GetDateTimeZone);
        }
    }
}