Repository navigation
DateTime UTC conversion to local time does not use historical DST rules #120434
Description
Description
Hi, we noticed an unexpected behavior when working with UTC DateTimes.
Some context: a web app sends UTC dates in ISO8601 format via JSON to a .net backend. On the server side they are deserialized to DateTime. We then use DateTime.ToLocalTime in order to render a PDF containing the user-entered DateTime.
Crucially, both client and server are located in Italy, "Europe/Rome" time zone. Here the current DST rules today say that DST ends "the last Sunday of October". This rule is active since 1994; before that it ended "the last Sunday of September". When the client wants to send the date "30/09/1993 00:00:00", it correctly sends it as the ISO8601 value "1993-09-29T23:00:00Z". However, when we print the date in the PDF report we call DateTime.ToLocalTime as we want the same value the user entered to be displayed in the document. Here .net uses the current DST adjustment rule instead of the historical one, and this makes the displayed date into "30/09/1993 01:00:00".
I have verified that .net does not actually have its own table but loads it from the underlying OS. Indeed, the error is only present when running under Windows while running under Linux renders the date correctly. A nice rabbit hole led me to a registry key on Windows where DST rules are stored, and it actually seems that Windows 11 does not include historical DST rules.
It feels really strange that we are the only ones noticing this behavior, but I wasn't able to find any other source dealing with this issue on Google. I understand it's not fixable by the .net team alone, but I'm looking for some guidance on how to resolve this issue. I also tried to use App Local ICU (https://learn.microsoft.com/en-us/dotnet/core/extensions/globalization-icu#app-local-icu) hoping it would load the same rules on Windows as it does on Linux, but it didn't have any effect.
Do you have any suggestion?
Reproduction Steps
Under Windows:
string strUtcA = "1993-09-19T22:00:00Z";
string strUtcB = "1993-09-29T23:00:00Z";
DateTime dtA = DateTime.Parse(strUtcA).ToUniversalTime();
DateTime dtB = DateTime.Parse(strUtcB).ToUniversalTime();
Console.WriteLine(strUtcA + " > UTC " + dtA.ToString("o") + " > Local " + dtA.ToLocalTime());
Console.WriteLine(strUtcB + " > UTC " + dtB.ToString("o") + " > Local " + dtB.ToLocalTime());Output (wrong):
1993-09-19T22:00:00Z > UTC 1993-09-19T22:00:00.0000000Z > Local 20/09/1993 00:00:00
1993-09-29T23:00:00Z > UTC 1993-09-29T23:00:00.0000000Z > Local 30/09/1993 01:00:00
Under Linux
string strUtcA = "1993-09-19T22:00:00Z";
string strUtcB = "1993-09-29T23:00:00Z";
DateTime dtA = DateTime.Parse(strUtcA).ToUniversalTime();
DateTime dtB = DateTime.Parse(strUtcB).ToUniversalTime();
Console.WriteLine(strUtcA + " > UTC " + dtA.ToString("o") + " > Local " + dtA.ToLocalTime());
Console.WriteLine(strUtcB + " > UTC " + dtB.ToString("o") + " > Local " + dtB.ToLocalTime());Output (correct):
1993-09-19T22:00:00Z > UTC 1993-09-19T22:00:00.0000000Z > Local 20/09/1993 00:00:00
1993-09-29T23:00:00Z > UTC 1993-09-29T23:00:00.0000000Z > Local 30/09/1993 00:00:00
Expected behavior
The output should be the same between Windows and Linux. The Linux one is correct - in 1993 a different DST rule was in place.
Actual behavior
When running on Windows the current DST rule is applied, leading to spurious +/- 1 hour differences.
Regression?
Not a regression AFAIK.
Known Workarounds
NodaTime on Windows behaves correctly as it doesn't use OS tables for DST changes. However switching DateTime for NodaTime is not a straightforward replacement.
Configuration
Tested with .net 6 and 9, on Linux (Ubuntu 22) and Windows.
Other information
No response
Activity
dotnet-policy-service commentedon Oct 6, 2025
Tagging subscribers to this area: @dotnet/area-system-datetime
See info in area-owners.md if you want to be subscribed.
MihuBot commentedon Oct 6, 2025
I'm a bot. Here are possible related and/or duplicate issues (I may be wrong):