I'm trying to get ahold of this timezone issue we are having. We would like to store all DateTimes in UTC, and then convert the DateTime to the user's timezone.
We decided to use NodaTime for this, as it seems like the right approach. However, we are experiencing an issue with it.
This is how we convert the DateTime to UTC (note - I hardcoded the usersTimeZone for now):
public static DateTime ConvertLocaltoUTC(this DateTime dateTime)
{
LocalDateTime localDateTime = LocalDateTime.FromDateTime(dateTime);
IDateTimeZoneProvider timeZoneProvider = DateTimeZoneProviders.Tzdb;
var usersTimezoneId = "Europe/Copenhagen";
var usersTimezone = timeZoneProvider[usersTimezoneId];
var zonedDbDateTime = usersTimezone.AtLeniently(localDateTime);
var returnThis = zonedDbDateTime.ToDateTimeUtc();
return zonedDbDateTime.ToDateTimeUtc();
}
And here is how we convert it back:
public static DateTime ConvertUTCtoLocal(this DateTime dateTime)
{
Instant instant = Instant.FromDateTimeUtc(dateTime);
IDateTimeZoneProvider timeZoneProvider = DateTimeZoneProviders.Tzdb;
var usersTimezoneId = "Europe/Copenhagen"; //just an example
var usersTimezone = timeZoneProvider[usersTimezoneId];
var usersZonedDateTime = instant.InZone(usersTimezone);
return usersZonedDateTime.ToDateTimeUnspecified();
}
However, when we convert it back to local time, it throws an exception:
Argument Exception: Invalid DateTime.Kind for Instant.FromDateTimeUtc
at the first line of ConvertUTCtoLocal().
An example of the DateTime could be: "9/18/2017 5:28:46 PM" - yes this has been through the ConvertLocalToUTC method.
Am I providing an incorrect format? What am I doing wrong here?
The exception you show:
Argument Exception: Invalid DateTime.Kind for Instant.FromDateTimeUtc
Is thrown from this code:
Instant instant = Instant.FromDateTimeUtc(dateTime);
It means that dateTime.Kind needs to be DateTimeKind.Utc to be convertible to an Instant, and for whatever reason it is not.
If you look at the result of your ConvertLocaltoUTC method, you'll find that it does have .Kind == DateTimeKind.Utc.
So, the problem lies elsewhere in your code, wherever you created the dateTime you're passing in to ConvertUTCtoLocal.
You may find the solution could be any of the following:
You might need to call DateTime.SpecifyKind to set the kind to UTC, but be careful to only do this when your values are actually UTC and it's just not setting the kind. For example, use this when loading a UTC-based DateTime from a database.
You might need to call .ToUniversalTime(), but be careful to only do this if the machine's local time zone is relevant to your situation. For example, do this in desktop or mobile apps where a UI control is picking a date, but you meant it to mean UTC instead of local time.
You might need to change how you parse strings into DateTime values, such as by passing DateTimeStyles.RoundTripKind to a DateTime.Parse call (or any of its variants. For example, do this if you are reading data from text, csv, etc.
If you want to avoid having to decide, don't write functions that take DateTime as input or give DateTime as output. Instead, use DateTimeOffset, or use Noda-Time types like Instant, LocalDateTime, etc. as early as possible.
This is what worked for me:
Instant instant = Instant.FromDateTimeUtc(DateTime.SpecifyKind(datetime, DateTimeKind.Utc));
Related
I am passed a DateTime object and a string timezone variable separately. Is there a way to cast this DateTime as the timezone in the string without "converting"(changing the actual time)?
If your time zone string is very specifically one of the system-defined strings, you can definitely do this. The easiest way is to make the assumption that the DateTime passed in is already in UTC, and that the string passed in is one of the Id values listed in TimeZoneInfo.GetSystemTimeZones(). Your function would look something like this:
public DateTime ConvertUtcDateTime(DateTime utcDateTime, string timeZoneId)
{
var tz = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
var convertedDateTime = TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, tz);
return convertedDateTime;
}
This would create a "new" DateTime value, and your original utcDateTime value would be unchanged. If you can't assume that the DateTime value is UTC, you'll have to require both the source and destination time zones and your function would change slightly:
public DateTime ConvertDateTime(DateTime currentDateTime, string sourceTimeZoneId, string destinationTimeZoneId)
{
var tzSource = TimeZoneInfo.FindSystemTimeZoneById(sourceTimeZoneId);
var tzDestination = TimeZoneInfo.FindSystemTimeZoneById(destinationTimeZoneId);
var convertedDateTime = TimeZoneInfo.ConvertTime(currentDateTime, tzSource, tzDestination);
return convertedDateTime;
}
Keep in mind that in both of these functions, you need to test if the time zone returned by TimeZoneInfo.FindSystemTimeZoneById is not null. If it is, your input string was bad and you will need to handle that. An exception would likely work well there, but I don't know your needs.
Edit:
As an afterthought, if your strings aren't exactly the same as the Id values from TimeZoneInfo.GetSystemTimeZones(), but they do kind of match up one-to-one, you could make some kind of Dictionary<string, string> to map your time zone strings to the system's Id values and run from that. That's a good bit of hard-coding, though, but sometimes you need something like this if your inputs can't be changed.
DateTime itself is immutable in C# so you can't really change it like you are wanting to.
Your best bet would be to store the object as a UTC DateTime and then based on the timezone string you are passed, add or subtract time from that to "correct" the UTC time stamp.
Use UTC Time.
https://time.is/UTC
To implelent it, follow this code.
var currentDate = DateTime.SpecifyKind(DateTime.UtcNow, DateTimeKind.Utc);
I am storing datetime in a stringified JSON object in Redis cache like so:
"{\"email\":\"myemail#testdomain.com\", \"expiry\": \"2018-03-19T23:00:03.0658822+00:00\"}"
In C#, when I query this data from Redis and convert it to string, it loses its timezone value and gets automatically stripped off of its timezone information.
RedisValue cookie = GetRedisDatabase().StringGet("sessionhash");
JObject cookieValue = JObject.Parse(cookie.ToString());
var email = JObject.Parse(cookie.ToString())["email"];
var expiry = JObject.Parse(cookie.ToString())["expiry"].ToString();
The "expiry" string above only contains "2018/03/19 23:00:03". It seems like C# is automatically detecting the string to be of datetime format, and is stripping off timezone information from it.
How can I ensure the "expiry" string is "2018-03-19T23:00:03.0658822+00:00"?
DateTime does not know about timezones. Instead it has a DateTimeKind property which tells you if the time is machine local, UTC, or unknown. Methods ToLocalTime will convert a known UTC or unknown time to local time, and do nothing of already local.
You'll need to use something else that keeps the timezone information, i believe DateTimeOffset can track a time with a variable offset, but not the timezone.
NodaTime is a library which understands timezones.
internal class Program
{
private static void Main()
{
string expiry = "2018-03-19T23:00:03.0658822+00:00";
DateTime parsedExpiry = DateTime.Parse(expiry);
Console.WriteLine(parsedExpiry.ToString());
Console.ReadKey();
}
}
This code converts 19/3/2018 23:00 into 20/3/2018 7:00.
The reason it does this is because, as per above answers, DateTime doesn't hold on to any TimeZone information. The only information you have is DateTime.Kind, which in the case of my code, outputs Local. I can use parsedExpirey.ToUniversalTime() to get UTC.
You could do some extra parsing on the string representation and use the TimeZoneInfo class to maintain the timezone, but you'll likely need an extra column / storage space to store that info. You can use the Convert option, but then you'll be storing DateTimes in all different timezones, you'd be better off using ToUniversalTime and storing it all in UTC (best practice), then converting it to Local time for presentation to the user (or leave it UTC, depending on the application).
Your final ToString asked for the time without TZ info. Do this
RedisValue cookie = GetRedisDatabase().StringGet("sessionhash");
JObject cookieValue = JObject.Parse(cookie.ToString());
var email = JObject.Parse(cookie.ToString())["email"];
var expiry = JObject.Parse(cookie.ToString())["expiry"].ToString("O");
I have a few general rules regarding handling DateTimes:
Always store, retrieve and transmit the value in UTC. Windows is pretty good at translating any UTC value to whatever the current user picked as his favourite timezone. You do not want to deal with Timezones if you can avoid it at all.
Never store, retrieve and transmit the value as string.
In case 3 can not work, at least pick a fixed culture and string encoding at all endpoints. You do not want to add those to your worries.
In rare cases (Callendar Apps) it might be beneficial to store the "originally saved timezone".
Unfortunately you cannot determine the time zone from an ISO date/time string. You can only determine the offset. The time zone names and codes are not unique-- for example, "Arabia Standard Time" has an offset of UTC+03, but has the code "AST," which collides with "Atlantic Standard Time" (offset UTC-04). So while you can map in one direction, you can't reliably map in the other.
That being said, getting the offset isn't so bad if you use a DateTimeOffset instead of DateTime. If the field isn't a DateTimeOffset in your object model, you can create a temporary anonymous type as a template and get it that way. Example:
public static void Main()
{
var input = "{\"email\":\"myemail#testdomain.com\", \"expiry\": \"2018-03-19T23:00:03.0658822+01:00\"}";
var template = new { email = "", expiry = DateTimeOffset.Now };
var result = JsonConvert.DeserializeAnonymousType(input, template);
Console.WriteLine("DateTime (GMT): {0:R}\r\nOffset from GMT: {1}", result.expiry, result.expiry.Offset);
}
Output:
DateTime (GMT): Mon, 19 Mar 2018 22:00:03 GMT
Offset from GMT: 01:00:00
Code on DotNetFiddle
I am getting this error while in Debug though the ToString() is executed:
A UTC DateTime is being converted to text in a format that is only
correct for local times. This can happen when calling
DateTime.ToString using the 'z' format specifier, which will include a
local time zone offset in the output. In that case, either use the 'Z'
format specifier, which designates a UTC time, or use the 'o' format
string, which is the recommended way to persist a DateTime in text.
This can also occur when passing a DateTime to be serialized by
XmlConvert or DataSet. If using XmlConvert.ToString, pass in
XmlDateTimeSerializationMode.RoundtripKind to serialize correctly. If
using DataSet, set the DateTimeMode on the DataColumn object to
DataSetDateTime.Utc.
public static string ToInterfaceString(this DateTime value)
{
return value != DateTime.MinValue ? value.ToString("yyyy-MM-ddTHH:mm:sszzz") : string.Empty;
}
In the app that I've just starting to work on it is used this format in many places. So what should I do in fact? Replace zzz with Z?
Update 1:
the DateTime that is passed to my extension is initiated to:
DateTimeCreated = DateTime.UtcNow;
Weird thing is that if I pass to this extension some other DateTime objects I don't receive any error/warning.
It's a green warning only.
So - as you seem to know what you are doing - you can just comprehend the message (which is correct) and mark the checkbox to Ignore this warning in the future.
As the DateTimeInvalidLocalFormat error explains, the conversion of the date value to string with your defined date format is not usable with DateTime which have the property Kind set to 'Utc'. Even though this error is just a warning and your code should still work, the resulting date string you will get after such operation will be incorrect. This means that if you convert your date to string and will try to parse it again to DateTime, the resulting DateTime value will be different from your original DateTime value. The difference will be as large as your time zone offset from the UTC time is. This is a serious error and should not be ignored especially if your local time does not match the UTC time.
Example:
In this example I assume that the local time zone is 2 hours ahead of UTC time.
DateTime yourLocalTime = DateTime.Now; // => 2020-05-15 08:00:00 => yourLocalTime.Kind = Local
DateTime yourTimeInUTC = DateTime.UtcNow; // => 2020-05-15 06:00:00 => yourTimeInUTC.Kind = Utc, note hours, e.g. 6 vs 8
string dateFormat = "yyyy-MM-ddTHH:mm:sszzz";
string testDateLocal = yourLocalTime.ToString( dateFormat ); // 2020-05-15T08:00:00+02:00 - This is correct date and time
string testDateUtc = yourTimeInUTC.ToString( dateFormat ); // 2020-05-15T06:00:00+02:00 - This date and time is 2 hours behind your actual date and time
To print the date and time string correctly using your date format you have to first convert your UTC date and time to the local date and time:
string testDateUtc2 = yourTimeInUTC.ToLocalTime().ToString( dateFormat ); // 2020-05-15T08:00:00+02:00 - This is correct date and time
To fix your code you should add conversion to the local time in your method:
public static string ToInterfaceString(this DateTime value)
{
return value != DateTime.MinValue ? value.ToLocalTime().ToString("yyyy-MM-ddTHH:mm:sszzz") : string.Empty;
}
You could also print UTC date and time instead of local date and time by replacing date format specifiers 'zzz' to 'Z'. However, in this case you will have a similar issue with conversion in case the DateTime passed to your method is not in UTC Kind, but e.g. in Local Kind and therefore, you have to always convert it to UTC before generating the string:
public static string ToInterfaceString(this DateTime value)
{
return value != DateTime.MinValue ? value.ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ") : string.Empty;
}
Printing of date and time without doing conversion can also be done by using date format specifier 'o', but then the generated date string format will differ based on supplied DateTime Kind and this date format also includes the milliseconds.
public static string ToInterfaceString(this DateTime value)
{
return value != DateTime.MinValue ? value.ToString("o") : string.Empty;
}
This exception is thrown and caught internally inside of ToString() method so the application will continue to run further. It's the only debugger who shows this.
It might initially look like a warning which is visible when debugging only but in fact, this code will generate valid results only in case when your computer time zone is UTC (seems there will be no internal exception in this case too). If your local time zone offset is, for example, UTC+02 than the result of:
new DateTime(2020,7,30,11,22,33, DateTimeKind.Utc).ToString(#"yyyy-MM-dd\THH:mm:sszzz");
on your computer will be "2020-07-30T11:22:33+02:00" which doesn't make sense because the result obviously should have UTC offset as 00:00.
Also, of course, such internal exceptions is a bad thing for control flow and might affect your app performance if this occurs too often.
To avoid invalid .ToString() results and also internal exceptions you can just create DateTimeOffset from given DateTime and invoke .ToString() for it. So the code:
new DateTimeOffset(new DateTime(2020,7,30,11,22,33, DateTimeKind.Utc)).ToString(#"yyyy-MM-dd\THH:mm:sszzz");
will give expected result "2020-07-30T11:22:33+00:00" for any local time zone.
Another suggestion is to use the "K" custom format specifier which gives the correct results for both DateTime and DateTimerOffset.
I have some code in my UI layer, which is supposed to take a DateTime, which is in UTC, and convert it to a local date time:
In my Data layer, I simply do this:
private DateTime ConvertToLocal(DateTime dt)
{
if (_currentTimeZoneUser == string.Empty)
{
var u = new UserData(_userId).GetUser(_userId);
_currentTimeZoneUser = u.TimeZoneId;
}
var reply = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(dt, _currentTimeZoneUser);
return reply;
}
What that does is check if _currentTimeZoneUser is set. If not, get the zimezone from the user table, and then does a conversion.
This code is working, and I get a valid result.
I then copied the code to my UI layer (As I need to do a conversion there as well, for a data grid), but 'reply' always equals 'dt'.
I googled, and noticed that I should be doing it a slightly different way. So I change my UI method to this:
public static DateTime GetLocalDateTime(DateTime serverTime)
{
var timeZoneId = HttpContext.Current.Session["TimeZoneId"].ToString();
TimeZoneInfo cstZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
var reply = TimeZoneInfo.ConvertTimeFromUtc(serverTime, cstZone);
return reply;
}
and it works!
I can't see why it works in my data layer, but in the UI, I need to change the code.
Am I doing something wrong with my time conversion code in one of the methods?
If I'm understanding you correctly, your question boils down to the difference between ConvertTimeBySystemTimeZoneId and ConvertTimeFromUtc.
First, you need to understand that any time zone conversion operations involving DateTime may have behavioral differences depending on the value of the .Kind of DateTime you are giving it. When you look at the documentation for each of these methods (here and here), you will find a chart that describes the behavior for each of the three different kinds (Utc,Local, and Unspecified).
This is a sore point in .Net, which is why libraries like Noda Time exist. You can read more in these two articles:
What's wrong with DateTime Anyway?
The case against DateTime.Now
The actual reason for the specific problem is that you probably passed in a DateTime who's .Kind is Unspecified. In the ConvertTimeBySystemTimeZoneId method, that will be treated as if it were Local, while in the ConvertTimeFromUtc method it will be treated as if it were Utc.
There are two solutions.
The first is what you already found - use the ConvertTimeFromUtc method. You should do this in the server code also.
The second solution is to set the .Kind to Utc when you load the value from your database. Somewhere you probably have code like this:
foo.MyDateTime = (DateTime) dataReader["MyDateTime"]
Which would change to this:
foo.MyDateTime = DateTime.SpecifyKind(
(DateTime) dataReader["MyDateTime"],
DateTimeKind.Utc);
I'm assuming you are doing a direct ADO.Net call with a DataReader response. Adjust accordingly for whatever you are actually doing.
We had an issue where one developer creates the below code and it works on his DEV environment. But when it's checked into QA, the code breaks with the below error message:
myRecord.UTCStartTime = TimeZoneInfo.ConvertTimeToUtc(myRecord.StartTime, myTimeZone);
The conversion could not be completed because the supplied DateTime
did not have the Kind property set correctly. For example, when the
Kind property is DateTimeKind.Local, the source time zone must be
TimeZoneInfo.Local.
On my DEV environment, the above code generates the same error as the QA server. I applied the below change to fix the problem:
DateTime utcStart = DateTime.SpecifyKind(myRecord.StartTime, DateTimeKind.Unspecified);
myRecord.UTCStartTime = TimeZoneInfo.ConvertTimeToUtc(utcStart, myTimeZone);
Why does the first code example work on DEV1's environment but break on my DEV environment and on our QA server?
It depends on how the myRecord.StartTime was originated.
If you got it from DateTime.Now, then it will have a Local kind.
If you got it from DateTime.UtcNow then it will have an Utc kind.
If you got it from new DateTime(2013,5,1) then it will have an Unspecified kind.
It also depends on where you got myTimeZone from. For example:
TimeZoneInfo.Local
TimeZoneInfo.Utc
TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time")
The TimeZoneInfo.ConvertTimeToUtc function will only operate if it can match the zone to the kind you give it. If both are local, or both are UTC, then it will work. If you are giving it a specific zone, then the kind should be unspecified. This behavior is documented on MSDN.
You can easily reproduce the exception consistently:
var tz = TimeZoneInfo.FindSystemTimeZoneById("Fiji Standard Time");
var utc = TimeZoneInfo.ConvertTimeToUtc(DateTime.Now, tz);
Assuming you don't live in Fiji, this will error every time. You basically said, "convert my local time, in some other zone, to utc" - which doesn't make sense.
It probably works in your dev environment because the value you're testing for myTimeZone happens to be the local zone for the developer.
Regarding your change - sure you can force the kind to be unspecified, and that changes the meaning of what you are doing such that it makes sense. But are you sure this is what you want? What is the .Kind of the date before hand? If it's not already Unspecified, then it is carrying some intent. You should probably go back to the source of this data and make sure it is what you expect.
If all of this sounds crazy, mad, frustrating, and bizarre, it's because the DateTime object stinks. Here's some additional reading:
What's wrong with DateTime anyway?
The case against DateTime.Now
You might consider using NodaTime instead. Its API will prevent you from making these types of common mistakes.
in this example i have converted the local timezone to unspecified type so it is working fine for me by using "DateTime.SpecifyKind()" method
DateTime.SpecifyKind(utc,DateTimeKind.Unspecified);
this method Creates a new DateTime object that has the same number of ticks as the specified DateTime, but is designated as unspecified type of DateTimeKind.
public static DateTime ConvertLocalDate(DateTime utc)
{
string id = ConfigurationManager.AppSettings["Timezone"].ToString();
TimeZoneInfo cstZone = TimeZoneInfo.FindSystemTimeZoneById(id);
utc = DateTime.SpecifyKind(utc,DateTimeKind.Unspecified);
DateTime cstTime = TimeZoneInfo.ConvertTimeFromUtc(utc, cstZone);
return cstTime;
}
I found a really simple solution here
https://kiranpatils.wordpress.com/2011/01/09/the-conversion-could-not-be-completed-because-the-supplied-datetime-did-not-have-the-kind-property-set-correctly-for-example-when-the-kind-property-is-datetimekind-local-the-source-time-zone-must/
Which appears only to be happening when you use DateTime.Now. I update my code like following and it is working again :)
DateTime currentTime = new DateTime(DateTime.Now.Ticks, DateTimeKind.Unspecified);
Basically what was said in previous answers, but to trim it down:
DateTime.Now or DateTime.Today sets the Kind to Local, so change this to Unspecified
In the method that handles the dates:
// Incoming date has DateTimeKind.Local
var localDateTime = DateTime.Now;
// Set to unspecified
var unspecifiedDateTime = DateTime.SpecifyKind(localDateTime, DateTimeKind.Unspecified);
I was only receiving this when deployed, then realised a library was passing DateTime.Now rather than new DateTime(...) to a method.
The accepted answer does a great job explaining the cause. But I'm still adding my 2 cents for completeness:
When serializing DateTime objects, some serializers will mess up the Kind of your DateTime properties. We've had this problem when caching POCO objects in Redis using ProtoBuf and MessagePack serializers.
So, if you know that's the case, then it's perfectly fine to force the kind of your datetime variable to "Unspecified" like this:
myDateTime = DateTime.SpecifyKind(myDateTime, DateTimeKind.Unspecified);
in C#
public static DateTime IndianDateTime(DateTime currentTime)
{
DateTime cstTime = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(currentTime, TimeZoneInfo.Local.Id, "India Standard Time");
return cstTime;
}
In VB
Public Shared Function IndianDateTime(ByVal currentTime As DateTime) As DateTime
Dim cstTime As DateTime = TimeZoneInfo.ConvertTimeBySystemTimeZoneId(currentTime, TimeZoneInfo.Local.Id, "India Standard Time")
Return cstTime
End Function