TimeZoneInfo IANA ID support since .NET 6: which time zone source should a cross-platform app standardize on?
0 reputation · 30 Jan 2023, 22:29 UTC
0 reputation · 30 Jan 2023, 22:29 UTC
Since .NET 6, TimeZoneInfo.FindSystemTimeZoneById accepts IANA IDs like America/Los_Angeles on all platforms, including Windows. On Windows, however, the runtime translates the IANA ID through a built-in mapping table to a Windows time zone key, and that table is fixed at the runtime's build time. On Linux and macOS the same call resolves against the operating system's tzdata, which typically carries full historical transition rules.
We are standardizing a service that runs in both Windows and Linux containers on .NET 8, and we need to store and convert timestamps going back several decades. The uncertainty is whether a single IANA-ID-based code path behaves identically on both hosts, given that Windows data may lack pre-1970 rules and the mapping table may lag current IANA tzdata releases. The alternative is taking a dependency on a converter package that ships its own tzdata copy, but that adds a second source of truth to keep updated.
Is the built-in IANA mapping on Windows documented as complete for current zones, or are there known unmapped IDs we should enumerate? For historical conversions, does the Windows-resolved zone produce different offsets than the Linux tzdata-resolved zone for the same IANA ID? Is there a supported way to detect at runtime which tzdata version the mapping reflects?
32775 reputation · 30 Jan 2023, 23:32 UTC
Standardize on IANA IDs in your storage and API contracts, but do not assume TimeZoneInfo gives identical results on Windows and Linux for pre-1970 timestamps. It does not. If your service converts timestamps going back several decades and both hosts must agree, the built-in Windows path is the weak link, and shipping your own tzdata (NodaTime with its bundled TZDB) is the only way to get bit-identical behavior across platforms.
Since .NET 6, TimeZoneInfo.FindSystemTimeZoneById("America/Los_Angeles") works on Windows by translating the IANA ID through a CLDR-derived mapping table compiled into the runtime. That table is fixed per runtime build — Windows Update and .NET servicing do not refresh it. On Linux and macOS the same call resolves against the OS tzdata, which carries full historical transition rules. Windows-resolved zones typically expose only the current adjustment rule set, so GetUtcOffset for dates before roughly 1970 can disagree with the Linux result for the same IANA ID. There is no supported public API to ask the runtime which CLDR/tzdata version its mapping reflects; you infer it from the .NET release notes for your exact runtime version.
The mapping covers currently active IANA zones, but it is documented as best effort, not exhaustive. Deprecated IDs, some aliases, and parts of the Etc/ namespace may not resolve. The practical move is to enumerate every zone ID your app accepts and probe each one at startup or in a test:
foreach (var id in knownZoneIds)
{
try { TimeZoneInfo.FindSystemTimeZoneById(id); }
catch (TimeZoneNotFoundException) { /* log unmapped ID */ }
}Run this on Windows with your production runtime version, since gaps depend on the build.
A cheap cross-platform check is a known wartime date. On Linux with current tzdata, America/Los_Angeles on 1942-02-09 was on year-round daylight time (UTC-7); a Windows-resolved zone may report UTC-8 because it lacks that historical rule:
var tz = TimeZoneInfo.FindSystemTimeZoneById("America/Los_Angeles");
var offset = tz.GetUtcOffset(new DateTime(1942, 2, 9, 12, 0, 0, DateTimeKind.Unspecified));
// Linux tzdata: -07:00; Windows: often -08:00Build a small test matrix (1900, 1942, 1969, 2000, 2024 across your actual zone list) and run it in CI on both Windows and Linux agents. The failures tell you exactly where the built-in path is unsafe.
Given decades-deep history and a hard consistency requirement, take the NodaTime dependency with its bundled TZDB (TzdbDateTimeZoneSource). It ships a full IANA tzdata copy, behaves identically on Windows and Linux, and exposes the tzdata version via TzdbDateTimeZoneSource.Default.VersionId — which also answers your version-detection question, something TimeZoneInfo cannot do. The cost is a second source of truth, but it is an explicit, versioned one you update via NuGet, rather than an invisible one baked into whichever runtime build each container happens to run.
If you stay on built-in TimeZoneInfo: keep IANA IDs as your canonical format, pin the tzdata package version in your Linux container images (dpkg -l tzdata to verify), document that pre-1970 conversions on Windows are best effort, and run the divergence test above on every runtime upgrade. One caveat to verify in your environment: Alpine-based images use musl tzdata packaging, which can lag Debian's, so confirm the version rather than assuming the base image is current.
Use comments to ask for clarification. Post a solution as an answer.
No question comments on this page.