C# : DateTimeOffset et fuseaux horaires
L'instant sans ambiguïté (date + heure + offset UTC), la comparaison par instant, et les conversions de fuseau avec TimeZoneInfo.
C# : DateTimeOffset et fuseaux horaires
DateTimeOffset désigne un instant précis et non ambigu : une date, une heure, et son décalage par rapport à UTC. Là où un DateTime en Unspecified laisse planer le doute, un DateTimeOffset dit exactement quel point du temps il représente. C'est le type à privilégier pour un horodatage (log, création d'un enregistrement, message).
# Construire un DateTimeOffset
DateTimeOffset maintenant = DateTimeOffset.Now; // instant courant + offset local
DateTimeOffset utc = DateTimeOffset.UtcNow; // instant courant, offset +00:00
// Explicite : 1er septembre 2026, 14:30, décalage +02:00 (heure d'été de Paris)
DateTimeOffset precis = new DateTimeOffset(2026, 9, 1, 14, 30, 0, TimeSpan.FromHours(2));DateTimeOffset maintenant = DateTimeOffset.Now; // instant courant + offset localDateTimeOffset utc = DateTimeOffset.UtcNow; // instant courant, offset +00:00 // Explicite : 1er septembre 2026, 14:30, décalage +02:00 (heure d'été de Paris)DateTimeOffset precis = new DateTimeOffset(2026, 9, 1, 14, 30, 0, TimeSpan.FromHours(2));L'offset est un TimeSpan : +02:00, -05:00, +00:00 pour UTC.
# Lire ses différentes vues
Un même instant se lit sous plusieurs angles :
DateTimeOffset d = new DateTimeOffset(2026, 9, 1, 14, 30, 0, TimeSpan.FromHours(2));
DateTime murale = d.DateTime; // 14:30 (l'heure telle qu'affichée à cet offset)
DateTime enUtc = d.UtcDateTime; // 12:30 (le même instant en UTC)
TimeSpan offset = d.Offset; // +02:00
// Recalculer le même instant sous un autre décalage
DateTimeOffset aNewYork = d.ToOffset(TimeSpan.FromHours(-4)); // 08:30 -04:00DateTimeOffset d = new DateTimeOffset(2026, 9, 1, 14, 30, 0, TimeSpan.FromHours(2)); DateTime murale = d.DateTime; // 14:30 (l'heure telle qu'affichée à cet offset)DateTime enUtc = d.UtcDateTime; // 12:30 (le même instant en UTC)TimeSpan offset = d.Offset; // +02:00 // Recalculer le même instant sous un autre décalageDateTimeOffset aNewYork = d.ToOffset(TimeSpan.FromHours(-4)); // 08:30 -04:00ToOffset ne change pas l'instant : il le ré-exprime dans un autre décalage (les chiffres de l'heure changent, le point du temps non).
# La comparaison compare l'instant
Deux DateTimeOffset avec des décalages différents mais le même instant sont égaux :
DateTimeOffset paris = new DateTimeOffset(2026, 9, 1, 14, 30, 0, TimeSpan.FromHours(2));
DateTimeOffset londres = new DateTimeOffset(2026, 9, 1, 13, 30, 0, TimeSpan.FromHours(1));
paris == londres; // true : c'est le même moment vu de deux villes
paris.EqualsExact(londres); // false : mais l'offset diffèreDateTimeOffset paris = new DateTimeOffset(2026, 9, 1, 14, 30, 0, TimeSpan.FromHours(2));DateTimeOffset londres = new DateTimeOffset(2026, 9, 1, 13, 30, 0, TimeSpan.FromHours(1)); paris == londres; // true : c'est le même moment vu de deux villesparis.EqualsExact(londres); // false : mais l'offset diffèreC'est précisément ce qui manque à DateTime : deux DateTime de chiffres identiques mais de fuseaux différents seraient jugés égaux à tort.
# Convertir entre fuseaux avec TimeZoneInfo
TimeZoneInfo connaît les fuseaux du système et gère l'heure d'été (DST). Depuis .NET 6, les identifiants IANA ("Europe/Paris") marchent sur toutes les plateformes.
TimeZoneInfo paris = TimeZoneInfo.FindSystemTimeZoneById("Europe/Paris");
TimeZoneInfo tokyo = TimeZoneInfo.FindSystemTimeZoneById("Asia/Tokyo");
DateTimeOffset instant = DateTimeOffset.UtcNow;
DateTimeOffset aParis = TimeZoneInfo.ConvertTime(instant, paris);
DateTimeOffset aTokyo = TimeZoneInfo.ConvertTime(instant, tokyo);TimeZoneInfo paris = TimeZoneInfo.FindSystemTimeZoneById("Europe/Paris");TimeZoneInfo tokyo = TimeZoneInfo.FindSystemTimeZoneById("Asia/Tokyo"); DateTimeOffset instant = DateTimeOffset.UtcNow; DateTimeOffset aParis = TimeZoneInfo.ConvertTime(instant, paris);DateTimeOffset aTokyo = TimeZoneInfo.ConvertTime(instant, tokyo);Le fuseau sait aussi si l'heure d'été s'applique, car l'offset change dans l'année :
TimeSpan enEte = paris.GetUtcOffset(new DateTime(2026, 7, 1)); // +02:00
TimeSpan enHiver = paris.GetUtcOffset(new DateTime(2026, 1, 1)); // +01:00TimeSpan enEte = paris.GetUtcOffset(new DateTime(2026, 7, 1)); // +02:00TimeSpan enHiver = paris.GetUtcOffset(new DateTime(2026, 1, 1)); // +01:00 Ne calcule jamais un changement de fuseau en ajoutant un offset fixe à la main : tu te tromperais aux dates de passage à l'heure d'été. Laisse TimeZoneInfo faire la conversion.
# DateTime ou DateTimeOffset ?
DateTimeOffset: dès qu'il s'agit d'un instant réel à stocker, comparer ou transmettre (horodatage, événement, log). Non ambigu.DateTimeen UTC : acceptable si tu maîtrises que c'est de l'UTC de bout en bout.DateTimelocal : seulement pour un affichage immédiat, jamais pour persister.
La règle inchangée : stocke un instant en UTC (ou en DateTimeOffset), convertis vers le fuseau de l'utilisateur au dernier moment, pour l'affichage.
# La suite
- Dates et temps - le hub : quel type pour quel besoin
- DateTime en profondeur - le type dont DateTimeOffset lève l'ambiguïté
- TimeSpan - l'offset est un TimeSpan