DateTime vs DateTimeOffset - der richtige Umgang mit Zeiten in .NET
Dieser Artikel behandelt den korrekten Umgang sowie Best Practises und Empfehlungen von Datums- und Zeitinformationen, Zeitzonen und Betriebssystem-Eigenheiten in Anwendungen, in Text-Dateien, Datenbanken und APIs. Alle Empfehlungen stammen aus dem Open Source-Bereich bzw. stellen eine Zusammenfassung aus der Microsoft Dokumentation dar, zB. .NET: Die Wahl zwischen "DateTime", "DateTimeOffset", "TimeSpan" und "TimeZoneInfo"
Zusammenfassung:
- In einer Applikation sollten Zeitangaben immer Zeitzonen-neutral (also UTC) behandelt werden.
- Die Anpassung an die Zeitzone erfolgt bei der Visualisierung der Zeitinformation (zB. in der UI).
- Die explizite Art und Weise sollte einer implizierten immer vorgezogen werden: daher sollte in .NET - wenn immer möglich - DateTimeOffset verwendet werden
- Das Speichern von Zeiten in Textdateien oder auch die Übergabe von Zeiten als String erfolg anhand der Regeln von ISO8601 (String Format "o").
- In MSSQL sollten Zeiten als datetimeoffset gespeichert werden; in PostgreSQL unter "time with timezone". Für MySQL müssen Zeiten manuell in UTC konvertiert werden.
DateTime
DateTime existiert seit Beginn von .NET.
Das große Defizit von DateTime, das auch bei .NET 1.0 früh erkannt wurde, ist, dass anhand der DateTime-Information nicht klar ist, welche Zeitzone die Zeitangabe darstellt. Daher wird DateTime auch als implizite Darstellung der Zeitinformation bezeichnet, dessen "Hoffnung" es ist, dass die Zeitinformation immer in Relation zu UTC-0 steht. Eine Garantie dafür kann DateTime nicht stellen, weshalb oft Fehler im Zusammenhang mit Zeitzonen und DateTime passieren. DateTime unterstützt an dieser Stelle nämlich nur zwei Möglichkeiten: die lokale Zeit der Anwendung oder UTC. Diese Information wird in der Kind-Eigenschaft definiert.
Dazu der Hinweis aus der deutschen .NET Dokumentation:
Mehr unter DateTime.
DateTimeOffset
Da eben die Probleme von DateTime früh erkannt wurde, gab es in Form von DateTimeOffset auch sehr früh eine viel bessere Alternative, die seit .NET 1.1 auch als die empfohlene Variante gilt. Vermutlich weil DateTime im IntelliSense früher auftaucht als DateTimeOffset, wird trotz der massiven Defizite und dem großen Fehlerpotential von DateTime oft dies verwendet.
Im Gegensatz zu DateTime enthält DateTimeOffset vollständig alle Informationen, die für eine global eindeutige Repräsentierung einer Zeitinformation notwendig ist.
Dazu der Hinweis aus der deutschen .NET Dokumentation:
Mehr unter DateTimeOffset.
.NET
Aufgrund dieser Empfehlungen gilt:
In der Anwendungslogik sollte mit DateTimeOffset.UtcNow gearbeitet werden.
DateTimeOffset currentTime = DateTimeOffset.UtcNow;
In der UI (zB WinForms/WPF/ASP.NET MVC) sollte die Zeit dann in der Visualisierung lokalisiert werden:
currentTime.ToLocalTime();
Im Falle von WPF und MVVM empfiehlt sich entweder eine Shadow-Property
public DateTime CurrentLocalTime
{
get { return CurrentTime.ToLocalTime(); }
}
oder noch besser ein entsprechende ValueConverter für die XAML-Implementierung.
Mehr unter DateTimeOffset.ToLocalTime Methode.
[B]Zeiten in APIs sowie in Text-Dateien (inkl. XML / Json und Co)[/B]
Es gibt zwei Möglichkeiten, wie Zeiten in APIs übergeben oder in Dateien gespeichert werden sollten:
- Zeiten beziehen sich implizit immer auf UTC+0 => der Entwickler muss dafür sorgen
- Die Zeitzonen beachten ISO 8601 und beinhalten die Zeitzone => erfolgt automatisch durch DateTimeOffset und Format("o").
Beide Varianten sind weit verbreitet; letztere ist jedoch die empfohlene Variante, da explizit.
Zeiten in MSSQL
In MSSQL gibt es zwar mehrere Typen für das Speichern von Zeitinformationen; es gibt jedoch nur einen einzigen Typ, der eine explizite Art und Weise - also inkl. der Zeitzoneninformation - unterstützt: datetimeoffset
. datetimeoffset ist das Äquivalent zum .NET Typ DateTimeOffset und sollte wenn immer möglich verwendet werden.
Alle anderen MSSQL Typen wie datetime und datetime2 unterstützen keine Zeitzoneninformation und müssen daher durch den Entwickler als UTC+0 implizit gespeichert werden.
Mehr unter MSSQL: Datums- und Uhrzeitdatentypen
Zeiten in PostgreSQL
PostgreSQL unterstützt mehrere Typen für das Speichern von Zeitinformationen; das Äquivalent für DateTimeOffset ist hier "time with time zone" und ist die empfohlene Variante. Auch hier sind alle anderen Datentypen als implizite Darstellung zu verstehen, die eine implizierte Art und Weise darstellt.
Mehr unter PostreSQL: Date and Time Handling
Zeiten in MySQL / MariaDB
In MySQL bzw. dem Fork MariaDB gibt es leider keinen Typ (weder TIMESTAMP noch DATETIME), der das Speichern über die explizierte Art und Weise inkl. Zeitzone unterstützt. Es bleibt hier leider nur die Möglichkeit Zeitangaben implizit als UTC+0 zu speichern.
Aufgrund dieser Defizite bei MySQL / MariaDB ist es sogar weit verbreitet, dass die Zeitangaben entweder als Text mit ISO 8601 Format ("o") oder die Zeitzone in zwei verschiedenen Spalten gespeichert werden (DATETIME+TEXT).
Mehr unter MySQL: The DATE, DATETIME, and TIMESTAMP Types
Zeiten mit dem EntityFramework
Auch im Entity Framework gilt DateTimeOffset als die empfohlene Variante, die automatisch auf die entsprechenden Datenbank-Typen gemappt werden.
Zeitzonen-Bezeichner (aka TimeZoneId)
Das Handling von Zeitzonen-Bezeichnern ist nicht ganz so einfach wie Zeitangaben selbst. Der Grund: die Zeitzonen-Quelle ist das Betriebssystem - und hier unterscheiden sich die Bezeichner von Windows- und *Unix-Betriebssysteme.
Folgende beiden Bezeichner repräsentieren die gleiche Zeitzone; haben eben aber pro Betriebssystem unterschiedliche Bezeichner aka TimeZoneIds.
// Windows
TimeZoneInfo windowsTimezone = TimeZoneInfo.FindSystemTimeZoneById("Central Europe Standard Time");
// Linux
TimeZoneInfo linuxTimezone = TimeZoneInfo.FindSystemTimeZoneById("Europe/Budapest");
Versucht man auf eine Zeitzone zugreifen, die das Betriebssystem nicht kennt, dann wird das mit einer TimeZoneNotFoundException
quittiert.
Leider gibt es von Haus aus in .NET keinerlei Möglichkeiten, dass Zeitzonen Betriebssystem-neutral verwendet werden können; daher haben sich entsprechend viele Open Source Projekte entwickelt. Als eines der beliebtesten und qualitativ besten Projekte hat sich NodaTime (von der "C# Legende Jon Skeet") entwickelt, mit dessen Hilfe Zeiteniformationen und vor allem Zeitzonen viel besser und einfacher programmiert werden können.
Windows vs Linux (und andere Unix-artige)
- Windows verwendet intern für den Umgang mit Zeiten immer die Zeitzone, die in den Region-Einstellungen hinterlegt sind.
- Linux verwendet intern immer die Zeitinformation bezogen auf UTC+0; egal in welcher Region man sich befindet => Unixzeit
Während also unter Windows eine Zeitangabe zu UTC+0 immer umgewandelt werden muss, ist dies bei Linux nicht der Fall.
DateOnly und TimeOnly
Mit .NET 6 gibt es die neuen Typen DateOnly und TimeOnly, nach denen die Community lange gefragt hat.
Beide Typen stellen keine absolute, eindeutige Zeitwerte dar, haben aufgrund ihrer Natur also wie DateTime keine Zeit-Eindeutigikeit; sollten dafür also nicht verwendet werden. Die Idee der beiden Typen ist die individuelle Verwendung, zum Beispiel:
- ein Zeitalarm alá "Jeden Tag 08:00 Uhr klingelt der Wecker": hier ist das Datum i.d.R. irrelevant
- Geboren am 17.05.1997; hier ist die Uhrzeit i.d.R. irrelevant
Beide Typen werden sich wahrscheinlich in vielen APIs schnell verbreiten; Entity Framework wird ab EF Core 6 beide sofort unterstützen.
Das Announcement: Date, Time, and Time Zone Enhancements in .NET 6
ISO 8601
Wie so vieles sind auch Zeitangaben standardisiert; dazu gehört seit 2006 auch der ISO 8601. Obwohl der Standard damit schon sehr viele Jahre existiet beachten bzw. kennen diesen leider immer noch viele Entwickler nicht. In diesem Standard werden aber so gut wie alle Möglichkeiten des neutralen Umgangs von Zeitinformationen abgedeckt; und das Technologie-neutral.
Ihr solltet daher diesen Standard unbedingt verinnerlichen, beachten sowie anwenden, wenn ihr Zeiten in Dateien speichert oder sie zum Beispiel über APIs übergebt.
Zu den wichtigsten Inhalten des Standards gehört das Datum yyyy-MM-DD
sowie aber auch die Zeitangabe HH:mm:ss
.
Besonders wichtig ist jedoch der Zeitzonen-neutrale Austausch von Zeitangaben yyyy'-'MM'-'dd'T'HH':'mm':'ss'.'fffffffK
(für DateTime) bzw. dd'T'HH':'mm':'ss'.'fffffffzzz
(für DateTimeOffset). Hierbei geht die Zeitzone nicht verloren und ist damit global eindeutig.
In .NET gibt es einen entsprechenden Format-Key, der diesen Standard vollständig beachtet:
DateTimeOffset currentTime = DateTimeOffset.UtcNow;
string isoTime = currentTime.Format("o");
Mehr unter Standardformatzeichenfolgen für Datum und Uhrzeit in .NET
Praktisches Beispiel
Ein praktisches Beispiel, das die Fehler und Risiken in DateTime aufzeigt, und wieso DateTimeOffset verwendet werden sollte*, habe ich in DateOnly und TimeOnly - die neuen Typen in .NET 6 gezeigt.
DateTime und die Problematik der Zeitzonen-Behandlung:
// Aktuelle UTC Zeit - Dateime Kind ist durch UtcNow korrekterweise Utc
DateTime dt = DateTime.UtcNow;
Console.WriteLine(dt.ToString("HH:mm")); // Hier kommt aktuell 0746 raus
// String als ISO 8601, wie man es zB tun sollte bei HTTP APIs (ASP macht das automatisch) und zB auch bei jeder Art Zeitangaben in Textdateien
string apiReturn = dt.ToString("o");
// zurück als DateTime -> DateTime Kind ist nun Local - Verfälschung der Zeitangabe
DateTime apiValue = DateTime.ParseExact(apiReturn, "o", null);
Console.WriteLine(apiValue.ToString("HH:mm")); // hier kommt 0946 raus
Obwohl gar nicht gewollt muss DateTime immer [u]explizit[/u] verwendet werden, weil die Zeit nicht neutral, standardisiert behandelt wird. Ansonsten - siehe Output - stimmt die Zeitrelation plötzlich nicht mehr.
Und diese Notwendigkeit, dass DateTime explizit programmiert werden muss ist, die Quelle vieler Fehler; vor allem wenn man das eben nicht weiß. Ich persönlich habe noch keine einzige große Anwendung gesehen, die mit DateTime arbeitet und KEINEN Zeitzonenfehler hat - weil man mit DateTime eben diesen Fehler so extrem einfach und unscheinbar begeht. Die Information der Eindeutigkeit geht in DateTime immer verloren. "Ich verwende immer UTC" ist also ein großer Trugschluss und hilft nicht!
Hingegen DateTimeOffset: es wird immer die absolute Zeit verwendet - implizit. Es ist keinerlei Korrektur notwendig.
DateTimeOffset dt = DateTimeOffset.UtcNow;
Console.WriteLine(dt.ToString("HH:mm")); // hier kommt 0749 raus
string apiReturn = dt.ToString("o");
DateTimeOffset apiValue = DateTimeOffset.ParseExact(apiReturn, "o", null);
Console.WriteLine(dt.ToString("HH:mm")); // hier kommt 0749 raus
Noch problematischer ist zB. die Verwendung von DateTime eben mit der Datenbank (siehe oben). Dort geht beim Schreiben die Zeitzone vollständig verloren und beim Lesen ist sie Unspecified
.
Vermeidet DateTime und verwendet immer DateTimeOffset!