Datum, Uhrzeit und Zeitzonen in Business-Apps — Von den Fallstricken von DateTime über das UTC-Speicherprinzip bis zum Testdesign
· Go Komura · C#, .NET, .NET Framework, Windows, Zeitzonen, Datums-/Zeitverarbeitung, Testen, Betrieb, Technische Beratung
„Wir haben den Server auf eine Cloud-VM umgezogen, und jeder Zeitstempel in unseren Berichten hat sich um neun Stunden verschoben.“ „Nur die Geräte, die wir an unser Auslandsbüro geliefert haben, zeigen im Tagesbericht das Datum des Vortags.“ „Es gibt Anzeichen dafür, dass der nächtliche Sammel-Batch an einem bestimmten Tag zweimal gelaufen ist.“ Anfragen zu Datums-/Zeit- und Zeitzonenproblemen erreichen uns fast immer in genau dieser Form: „in dem Moment, in dem sich die Umgebung geändert hat“. Keine einzige Codezeile wurde dabei angefasst.
Man ist geneigt anzunehmen: „Unsere App wird nur in Japan verwendet, also spielen Zeitzonen für uns keine Rolle“ – in der Praxis passiert ein großer Teil dieser Beratungsfälle jedoch genau bei solchen rein inländischen Apps. Cloud-VMs und Container werden häufig mit UTC als Systemeinstellung bereitgestellt, und ausländische Bibliotheken oder SaaS-Web-APIs liefern Zeitstempel in UTC oder mit explizitem Offset zurück. Die App selbst mag glauben, vollständig in der japanischen Standardzeit zu leben, doch sobald man eine Grenze überschreitet, befindet man sich bereits in einer UTC-Welt. Werden Werte weitergereicht, ohne jemals festzulegen, „worauf sich diese Zeit bezieht“, bleibt die Diskrepanz verborgen – bis zum Tag einer Serververlagerung oder Cloud-Migration, an dem sie schlagartig als Verschiebung um neun Stunden zutage tritt.
Dieser Artikel geht von einer .NET-Business-App aus und arbeitet die Kind-Eigenschaft von DateTime und ihre impliziten Konvertierungsfallen durch, wann stattdessen DateTimeOffset zum Einsatz kommen sollte, das Prinzip „in UTC oder mit explizitem Offset speichern und übertragen, nur zur Anzeige in lokale Zeit konvertieren“, TimeZoneInfo und Sommerzeit, die Grenze zur Datenbank sowie schließlich das Testdesign mit TimeProvider. Am Ende destillieren wir die Punkte, die wir bei jeder Designprüfung kontrollieren, zu einer Checkliste.
1. Die Kurzfassung
- Die Ursache nahezu jedes Datums-/Zeitvorfalls ist dieselbe: Ein Wert ohne Information darüber, „auf welche Referenz sich diese Zeit bezieht“, überschreitet eine Grenze (eine Datenbank, eine API, eine Datei). Das Symptom taucht nicht auf, wenn sich der Code ändert – es taucht auf, wenn sich die Umgebung ändert.
DateTimeträgt ein Attribut namens Kind (Utc / Local / Unspecified), das standardmäßig auf Unspecified steht.ToLocalTimebehandelt Unspecified als UTC, währendToUniversalTimees als lokale Zeit behandelt – diese asymmetrische implizite Interpretation ist die klassische Quelle der „Neun-Stunden-Verschiebung“.1- Für neuen Code sollte
DateTimeOffsetder Standard sein. Die offizielle Richtlinie besagt ausdrücklich, dass er „als Standard-Datums-/Zeittyp für die Anwendungsentwicklung gelten sollte“.2 - Das Prinzip lautet: „In UTC oder mit explizitem Offset speichern und übertragen; nur zur Anzeige in lokale Zeit konvertieren.“ Schreiben Sie einen Wert als Zeichenkette mit dem ISO-8601-konformen Roundtrip-Formatstring „o“, und lesen Sie ihn mit
DateTimeStyles.RoundtripKindwieder ein.3 - Die Zeitzonenkonvertierung erfolgt über
TimeZoneInfo. Seit .NET 6 lassen sich sowohl IANA-IDs (Asia/Tokyo) als auch Windows-IDs (Tokyo Standard Time) verwenden, und es stehen auch APIs zur Konvertierung zwischen beiden zur Verfügung.4 Die Auflösung von IANA-IDs unter Windows hängt jedoch von ICU ab und schlägt auf älteren Windows-Server-Builds oder im Invariant-Globalization-Modus fehl (Kapitel 4).5 - Auch wenn Japan keine Sommerzeit kennt, gerät man in dem Moment, in dem ein Gerät im Auslandsbüro, eine Anbindung an ausländische SaaS-Dienste oder ein UTC-konfigurierter Server ins Spiel kommt, direkt in die „nicht existierende Zeit“ und die „mehrdeutige Zeit“ der Sommerzeit.6
- Hören Sie auf,
DateTime.Nowhart zu codieren. Injizieren SieTimeProvider(Standard in .NET 8; auf älteren Zielplattformen Microsoft.Bcl.TimeProvider verwenden), damit Tests die Uhr frei verschieben können.7
2. Kind von DateTime — die Bedeutung der drei Werte und der Vorfall der impliziten Konvertierung
Zusätzlich zu seinem Datums-/Zeitwert (Ticks) trägt DateTime genau ein weiteres Attribut namens Kind. Es hat drei mögliche Werte, und der Standardwert ist Unspecified.1
| Kind | Bedeutung | Typische Quelle |
|---|---|---|
| Utc | Zeit bezogen auf UTC | DateTime.UtcNow, das Ergebnis von ToUniversalTime() |
| Local | Zeit bezogen auf die lokale Zeitzone der ausführenden Maschine | DateTime.Now, das Ergebnis von ToLocalTime() |
| Unspecified | Keine Referenz bekannt (Standard) | new DateTime(...), DateTime.Parse (in den meisten Fällen), aus einer Datenbank gelesene Werte |
Entscheidend ist, dass gewöhnlicher Code überall Unspecified-Werte erzeugt. Über einen Konstruktor erstellte Werte, aus einer Zeichenkette geparste Werte und aus einer Datenbank gelesene Werte sind standardmäßig alle „von unbekannter Referenz“. Das allein ist noch nicht das Problem. Das Problem ist, dass Konvertierungsmethoden für diese Unspecified-Werte stillschweigend eine Referenz annehmen.1
| Aufruf | Kind=Utc | Kind=Local | Kind=Unspecified |
|---|---|---|---|
ToUniversalTime() |
unverändert zurückgegeben | in UTC konvertiert | als lokale Zeit behandelt und in UTC konvertiert |
ToLocalTime() |
in lokale Zeit konvertiert | unverändert zurückgegeben | als UTC behandelt und in lokale Zeit konvertiert |
Der entscheidende Punkt ist, dass derselbe Unspecified-Wert je nach aufgerufener Methode entweder als „lokal“ oder als „UTC“ interpretiert werden kann. Im Code sieht das so aus:
// Ein aus der Datenbank gelesener Wert. Über viele Codepfade landet er bei Kind = Unspecified
var fromDb = new DateTime(2026, 7, 3, 9, 0, 0);
// Wenn die ausführende Maschine auf JST (UTC+9) eingestellt ist:
Console.WriteLine(fromDb.ToLocalTime()); // 18:00 -- als UTC behandelt, also +9 Stunden
Console.WriteLine(fromDb.ToUniversalTime()); // 00:00 -- als lokale Zeit behandelt, also -9 Stunden
Genau so entsteht der Vorfall. Wird ein in der Datenbank als japanische Ortszeit gespeicherter Wert, 09:00 (Unspecified), unmittelbar vor der Anzeige „zur Sicherheit“ durch ToLocalTime() geschickt, wird er als UTC behandelt und zu 18:00. Umgekehrt: Ist eine Konvertierung, die „vor dem Speichern auf UTC normalisieren“ soll, irgendwo im Ablauf doppelt vorhanden, werden neun Stunden zweimal abgezogen. Besonders lästig ist, dass das Verhalten von der Zeitzoneneinstellung der ausführenden Maschine abhängt. Auf einer Entwicklungsmaschine (JST) verschiebt es sich um neun Stunden, auf einem UTC-konfigurierten Server um null Stunden – was zum klassischen Bericht „Bei mir tritt das nicht auf“ führt. Die eingangs erwähnte „Neun-Stunden-Verschiebung nach einer Servermigration“ ist meist genau dieses Muster.
2.1 Der Unterschied zu DateTimeOffset und wann welcher Typ verwendet wird
DateTimeOffset trägt neben Datum und Uhrzeit immer einen UTC-Offset (etwa +09:00), sodass der Wert allein weltweit einen eindeutigen Zeitpunkt festlegt. Für Anwendungsfälle, die „einen Zeitpunkt festhalten“ – Protokolleinträge, Transaktionszeiten, Systemereignisaufzeichnungen –, besagt die offizielle Richtlinie ausdrücklich, dass DateTimeOffset als Standard-Datums-/Zeittyp gelten sollte.2 Da für die implizite Interpretation von Kind erst gar kein Raum bleibt, werden die meisten der in diesem Artikel behandelten Vorfälle strukturell unmöglich.
Ein Allheilmittel ist er dennoch nicht. Beachten Sie, dass DateTimeOffset einen Offset trägt, keine Zeitzone. +09:00 sagt nicht, ob Sie sich in Japan oder Korea befinden, und enthält keine Sommerzeitregeln.2 Wenn Sie reproduzieren müssen, „was die Wanduhr an diesem bestimmten Ort anzeigen würde“, brauchen Sie weiterhin die Kombination mit dem später behandelten TimeZoneInfo. Hier eine Tabelle, wann welcher Typ passt.
| Typ | Getragene Information | Gut geeignet für | Anmerkungen |
|---|---|---|---|
DateTimeOffset |
Datum/Uhrzeit + UTC-Offset | Aufzeichnung, wann etwas geschah, Protokolle, API-Grenzen | Standard für neuen Code2 |
DateTime (als Kind=Utc betrieben) |
Nur Datum/Uhrzeit | Interne Berechnungen, Kompatibilität mit bestehenden Beständen | Kind wird vollständig selbst verwaltet |
DateOnly / TimeOnly |
Nur Datum / nur Uhrzeit | Geschäftsdaten, Geschäftszeiten, Stichzeiten | Unter .NET Framework nicht verfügbar2 |
TimeSpan |
Eine Zeitspanne | Verstrichene Zeit, Differenz zweier Zeitpunkte | |
TimeZoneInfo |
Eine Zeitzonendefinition (einschließlich Anpassungsregeln) | Konvertierung, Ermittlung der Sommerzeit | Kapitel 4 |
Jeden bestehenden DateTime-Bestand auf DateTimeOffset umzuschreiben ist oft nicht realistisch, sodass der Kompromiss, den wir bei Modernisierungsprojekten häufig wählen, lautet: „Intern und bei der Speicherung einheitlich DateTime mit Kind=Utc verwenden und an den Grenzen (APIs, Serialisierung) DateTimeOffset oder eine Zeichenkette im „o“-Format.“ In beiden Fällen bildet das Prinzip des nächsten Kapitels die Grundlage.
3. Das Prinzip — in UTC oder mit Offset speichern und übertragen, nur zur Anzeige in lokale Zeit konvertieren
Das Designprinzip für die Datums-/Zeitverarbeitung passt in drei Zeilen.
- Den Zeitpunkt eines Ereignisses mit
DateTime.UtcNowoderDateTimeOffset.UtcNowerfassen und durchgehend in UTC (oder mit Offset) weiterreichen. - Beim Überschreiten einer Grenze (Datenbank, API, Datei, Registrierung) Format und Referenz explizit als Teil der Spezifikation festlegen.
- Genau einmal in lokale Zeit konvertieren — unmittelbar bevor sie auf einem Bildschirm angezeigt oder auf einem Bericht ausgedruckt wird.
Ein Design, das lokale Zeit speichert, macht die „Bedeutung“ eines Werts von einem externen Zustand abhängig: der Betriebssystemeinstellung des Servers. Solange es auf einem On-Premises-Server läuft, der für Japan konfiguriert ist, bleibt das Problem unsichtbar – aber eine Migration auf eine Cloud-VM, eine DR-Konfiguration in einer ausländischen Region oder ein Konfigurationsunterschied zwischen Entwicklung und Produktion ändert diese Bedeutung, und jeder einzelne dieser Fälle reicht dafür aus. Bei UTC-Speicherung ist die Bedeutung eines Werts in jeder Umgebung dieselbe. Die Konvertierung zur Anzeige erfolgt auf der konsumierenden Seite (basierend auf der Einstellung des Benutzers oder einer in der Benutzerstammdatentabelle gespeicherten Standort-Zeitzone), sodass beim Betrachten derselben Daten aus Tokio oder aus Berlin jeweils die für den betreffenden Ort korrekte Uhrzeit angezeigt wird.
3.1 Für Zeichenkettendarstellungen an Grenzen ISO 8601 / das „o“-Format verwenden
Wenn eine Grenze als Zeichenkette überschritten wird (JSON, CSV, Protokolle, Konfigurationsdateien), verwenden Sie den ISO-8601-konformen Roundtrip-Formatstring „o“. „o“ erhält Kind von DateTime und den Offset von DateTimeOffset in der Zeichenkette, und ein Parsen mit DateTimeStyles.RoundtripKind stellt den ursprünglichen Wert wieder her.3
using System.Globalization;
// Ausgabe: 2026-07-03T13:30:00.0000000+09:00
DateTimeOffset now = DateTimeOffset.Now;
string s = now.ToString("o", CultureInfo.InvariantCulture);
// Einlesen: wird mit erhaltenem Offset wiederhergestellt
var restored = DateTimeOffset.Parse(
s, CultureInfo.InvariantCulture, DateTimeStyles.RoundtripKind);
Verwenden Sie dazu stets CultureInfo.InvariantCulture. Bleibt die Kultur auf ihrem Standardwert, kann sich die Jahresschreibweise auf einem Gerät ändern, das unter einer nicht-gregorianischen Kultur läuft, etwa dem japanischen Kaiserkalender. Was Sie unbedingt vermeiden müssen, ist die Speicherung oder Übertragung mit einem Format, das überhaupt keine Referenzinformation trägt, wie "yyyy/MM/dd HH:mm". Wer diese Zeichenkette empfängt, muss zwangsläufig raten, „worauf sie sich bezieht“, und Vermutungen hören auf zu stimmen, sobald sich die Umgebung ändert. Beachten Sie außerdem, dass die Standarddarstellung von Datum/Uhrzeit in System.Text.Json selbst auf ISO 8601 basiert, sodass es an einer JSON-Grenze sicher ist, einfach beim Standard zu bleiben.
Der Gedanke, dass „Daten, die eine Grenze überschreiten, ihr Format explizit in der Spezifikation festlegen müssen“, ist nicht auf Datum/Uhrzeit beschränkt. Genau dasselbe Argument führen wir zu Zeichenkodierung und Zeilenenden in „Zeichenkodierung und Zeilenenden unter Windows“. Implizite Annahmen an einer Grenze brechen in derselben Form, unabhängig davon, um welche Art von Daten es sich handelt.
3.2 Zeitstempel von „Geschäftsdatum“ unterscheiden
Hier noch eine Unterscheidung, die nötig ist, um das Beispiel aus der Einleitung zu erklären, bei dem „nur beim Auslandsbüro das Datum des Tagesberichts auf den Vortag zurückspringt“. Ein Zeitstempel (ein weltweit eindeutiger Zeitpunkt) und ein Geschäftsdatum (eine Bezeichnung wie „der Tagesbericht für den 3. Juli“) sind zwei verschiedene Dinge. Halten Sie ein Geschäftsdatum als „ein DateTime um Mitternacht“ und lassen Sie es durch eine UTC-Konvertierung laufen, wird Mitternacht am 3. Juli bei UTC+9 zu 15:00 Uhr am 2. Juli in UTC – und in dem Moment, in dem Sie nur den Datumsanteil herausschneiden, verschiebt es sich um einen Tag zurück. Das ist der klassische Mechanismus hinter diesem Fehler.
Halten Sie ein Geschäftsdatum als DateOnly (oder unter .NET Framework als im Format yyyy-MM-dd formatierte Zeichenkette oder als reinen Jahr-Monat-Tag-Wert), und legen Sie als Teil der Spezifikation fest, anhand welcher Zeitzone das Datum geschnitten wird. Solange die Spezifikation etwas festlegt wie „das Datum des Tagesberichts basiert auf der lokalen Zeit des Standorts“ oder „die Stichzeit basiert auf der JST der Zentrale“, besteht die Implementierung lediglich darin, UtcNow in die betreffende Zeitzone zu konvertieren und dann das Datum zu schneiden. Steht es nicht in der Spezifikation, ist standardmäßig die Maschineneinstellung des Implementierers zur Spezifikation geworden.
4. Zeitzonenkonvertierung — TimeZoneInfo und seine ID-Systeme
Die Zeitzonenkonvertierung erfolgt mit ConvertTimeFromUtc / ConvertTimeToUtc / ConvertTime von TimeZoneInfo. Zu beachten ist, dass diese APIs die Konsistenz zwischen Kind von DateTime und der Quellzeitzone, aus der konvertiert wird, prüfen. Übergibt man beispielsweise einen Kind=Utc-Wert mit der Angabe „Quelle ist Tokio“, wirft dies eine ArgumentException.8 Anders gesagt: Code, der bei der Kind-Verwaltung nachlässig ist, kann nicht einmal die Zeitzonenkonvertierungs-APIs sauber aufrufen. Das knüpft direkt an Kapitel 2 an.
4.1 Windows-Zeitzonen-IDs und IANA-IDs
Es gibt zwei ID-Systeme zur Angabe einer Zeitzone.
| Windows-ID | IANA-ID | |
|---|---|---|
| Beispiel (Japan) | Tokyo Standard Time |
Asia/Tokyo |
| Beispiel (Deutschland) | W. Europe Standard Time |
Europe/Berlin |
| Verwaltet von | Windows (Registrierung) | Die IANA-tz-Datenbank |
| Vorrangig genutzt von | Windows-APIs, .NET (Framework) | Linux, ausländische SaaS, Web-APIs, andere Sprachen |
In der Ära von .NET Framework konnten nur Windows-IDs verwendet werden, was zu dem wiederkehrenden Konvertierungstabellenproblem führte: „Die Web-API sendet Asia/Tokyo, aber ich kann das nicht an FindSystemTimeZoneById übergeben.“ Seit .NET 6 akzeptiert TimeZoneInfo.FindSystemTimeZoneById beide ID-Systeme, und wenn die übergebene ID lokal nicht registriert ist, wird sie automatisch konvertiert und aufgelöst. Auch TryConvertIanaIdToWindowsId / TryConvertWindowsIdToIanaId wurden hinzugefügt, für Fälle, in denen explizit konvertiert werden soll.4
// .NET 6+: eine IANA-ID funktioniert unverändert (die Windows-ID "Tokyo Standard Time" liefert dasselbe Ergebnis)
var tokyo = TimeZoneInfo.FindSystemTimeZoneById("Asia/Tokyo");
var berlin = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin");
DateTime utc = DateTime.UtcNow;
Console.WriteLine(TimeZoneInfo.ConvertTimeFromUtc(utc, tokyo)); // Tokioter Uhrzeit
Console.WriteLine(TimeZoneInfo.ConvertTimeFromUtc(utc, berlin)); // Berliner Uhrzeit
// Konvertierung zwischen den ID-Systemen (.NET 6+)
if (TimeZoneInfo.TryConvertWindowsIdToIanaId("Tokyo Standard Time", out var ianaId))
Console.WriteLine(ianaId); // Asia/Tokyo
Als praktische Faustregel empfehlen wir, die in Standortstammdaten oder Konfigurationsdateien gehaltenen Zeitzonen-IDs auf IANA-IDs zu vereinheitlichen. IANA-IDs sind das, was bei ausländischen SaaS-Diensten, Linux-Containern und anderen Sprachen universell verstanden wird, und ab .NET 6 lassen sie sich in der Regel unverändert übernehmen. Läuft ein Teil Ihres Systems noch auf .NET Framework, konvertieren Sie nur an dieser spezifischen Grenze in eine Windows-ID. Beachten Sie außerdem, dass FindSystemTimeZoneById eine TimeZoneNotFoundException wirft, wenn die ID nicht gefunden werden kann – fangen Sie das frühzeitig ab, entweder in der Oberfläche, in der Standort-IDs registriert werden, oder bei der Validierung beim Start.
Es gibt eine wichtige Voraussetzung, die zu beachten ist. Die Auflösung von IANA-IDs unter Windows hängt von der ICU-Bibliothek ab. Apps, die im NLS-Modus oder im Invariant-Globalization-Modus (InvariantGlobalization=true) laufen, können IANA-IDs nicht auflösen, und auch TryConvertIanaIdToWindowsId schlägt fehl.5 Dieselbe Einschränkung gilt auch, wenn .NET 6 auf einem älteren Betriebssystem läuft, dessen ICU nicht mit dem System gebündelt ist (Windows Server 2019, Windows 10 Builds 1809 und früher usw.), es sei denn, Sie bündeln eine app-lokale ICU (dies wurde ab .NET 7 geändert, sodass ICU auch auf diesen Betriebssystemversionen verwendet wird9). Anders gesagt: „Asia/Tokyo löst sich auf der Entwicklungsmaschine (Windows 11) problemlos auf, wirft aber gerade auf dem Windows Server 2019 des Kunden eine TimeZoneNotFoundException“ – das kommt real vor. Wenn Sie IANA-IDs für Ihre Stammdaten übernehmen, verbinden Sie diese Entscheidung mit drei Dingen: (1) Bestätigen Sie, dass die IANA-Auflösung auf dem Betriebssystem und der .NET-Version, auf der Sie laufen werden, tatsächlich funktioniert, (2) aktivieren Sie InvariantGlobalization in einem Container nicht leichtfertig nur um das Image zu verkleinern, und (3) bauen Sie als Absicherung in Ihre Startvalidierung einen Fallback ein, der über TryConvertIanaIdToWindowsId auf eine Windows-ID zurückfällt und es erneut versucht.
5. Sommerzeit (DST) — auch in einer rein japanischen App trifft man darauf
Da Japan derzeit keine Sommerzeit kennt, liegt die Annahme nahe: „DST hat mit uns nichts zu tun.“ Sie treffen jedoch definitiv darauf, wenn Folgendes zutrifft:
- Die App läuft auf Geräten, die in Auslandsbüros oder von ins Ausland reisenden Mitarbeitern genutzt werden (die lokale Zeitzone des Geräts kennt Sommerzeit)
- Sie binden ausländische SaaS-Dienste oder Web-APIs an und empfangen Zeitstempel oder Zeitpläne auf Basis der Ortszeit
- Sammel- oder Batch-Jobs laufen auf Servern oder VMs in einer ausländischen Region
- Sie haben eine Stichzeit basierend auf der Ortszeit eines Auslandsbüros (etwa „jeder Standort schneidet um Mitternacht Ortszeit ab“)
In Zeitzonen mit Sommerzeit entstehen am Umstellungstag zwei Arten anomaler Zeiten. Nicht existierende Zeiten (im Frühjahr das Band, das übersprungen wird, wenn die Uhr vorgestellt wird – in Deutschland 02:00–03:00 Uhr Ende März) und mehrdeutige Zeiten (im Herbst das Band, das doppelt vorkommt, weil die Uhr zurückgestellt wird). In .NET lassen sich diese mit TimeZoneInfo.IsInvalidTime / IsAmbiguousTime erkennen.6 Und Konvertierungs-APIs wie ConvertTimeToUtc werfen bei Übergabe einer nicht existierenden Zeit eine ArgumentException und interpretieren eine mehrdeutige Zeit als Normalzeit.8
var berlin = TimeZoneInfo.FindSystemTimeZoneById("Europe/Berlin");
// 2026-03-29 ist der Beginn der deutschen Sommerzeit. Die Ortszeit zwischen 02:00 und 03:00 existiert nicht
var t = new DateTime(2026, 3, 29, 2, 30, 0); // Kind = Unspecified
Console.WriteLine(berlin.IsInvalidTime(t)); // True
// TimeZoneInfo.ConvertTimeToUtc(t, berlin) wirft eine ArgumentException
Gibt es einen Eingabepfad, der „eine Ortszeit-Zeichenkette entgegennimmt und vor dem Speichern in UTC konvertiert“, lauert diese Ausnahme als Fehler, der nur an einem Tag im Jahr auftritt. Ein realistischer Ansatz ist, IsInvalidTime bei der Eingabevalidierung zu prüfen und in der Spezifikation ausdrücklich festzulegen, dass eine mehrdeutige Zeit „als Normalzeit behandelt“ werden soll.
5.1 Geplante Jobs und DST — das Problem „läuft zweimal / läuft nicht“
Geplante Jobs sind ein weiterer klassischer Ort, an dem man darauf trifft. Ein Job, der täglich um 02:30 Uhr Ortszeit läuft, hat diese Zeit an dem Tag, an dem die Sommerzeit beginnt, schlicht nicht verfügbar, und an dem Tag, an dem sie endet, zweimal. Je nach Implementierung des Schedulers variiert das Verhalten – „wird übersprungen“, „feuert zweimal“, „feuert um eine Stunde versetzt“ –, was bedeutet, dass ein Aggregationsprozess, der unter der Annahme „läuft genau einmal am Tag“ geschrieben wurde, Daten doppelt zählt oder ganz auslässt. Die Gegenmaßnahme ist eine Kombination aus drei Dingen:
- Den Zeitplan auf UTC (oder eine Zeitzone ohne DST) stützen. Für Batch-Jobs, bei denen das Auslösen zu einer bestimmten Ortszeit keine tatsächliche Anforderung ist, verschwindet das Problem allein dadurch.
- Den Prozess idempotent machen. Fügt man eine „bereits gelaufen“-Markierung hinzu – überspringen, wenn die Aggregation für das Zieldatum bereits existiert –, hört zweimaliges Feuern auf, schädlich zu sein.
- Die Aggregation nach Geschäftsdatum schlüsseln (Abschnitt 3.2). Das Datum nicht aus der Auslösezeit zurückrechnen.
Das Design geplanter Ausführung mit der Aufgabenplanung (Verhinderung von Mehrfachstarts, Isolierung von Fehlern) behandeln wir ausführlich in „Aufgabenplanungs-Aufgaben, die nicht laufen oder mit 0x1 beendet werden“, und das Design, einen Timer innerhalb eines residenten Diensts zu halten, in „Wie man einen Windows-Dienst baut und betreibt“. Unabhängig davon, welchen Ansatz Sie verwenden, muss die Beziehung zwischen DST und dem Zeitplan in gleicher Weise in die Spezifikation geschrieben werden.
6. Die Grenze zur Datenbank — SQL Server / SQLite / ORMs
Von allen Grenzen ist die Datenbank die, an der die meisten Vorfälle geschehen. Die meisten Datums-/Zeittypen von Datenbanken bewahren nicht auf, „auf welche Referenz sich das bezieht“, sodass Kind- und Offset-Information in dem Moment verloren gehen, in dem ein Wert gespeichert wird.
6.1 Die Datums-/Zeittypen von SQL Server
| Typ | Bereich/Genauigkeit | Referenzinformation | Empfehlung für neue Nutzung |
|---|---|---|---|
datetime |
Ab 1753, rund 1/300-Sekunden-Genauigkeit | Keine | Vermeiden (offiziell dokumentiert als für neue Entwicklung nicht empfohlen)10 |
datetime2 |
Ab dem Jahr 0001, bis zu 100-Nanosekunden-Genauigkeit | Keine | Die Standardwahl für eine in UTC gespeicherte Spalte10 |
datetimeoffset |
Entspricht datetime2 + Offset |
Erhält den Offset | Für Spalten, bei denen die Reproduktion der lokalen Zeit erforderlich ist10 |
datetime ist ein veralteter Typ mit grober Rundung und engem Bereich, und die offizielle Dokumentation stellt ausdrücklich fest, dass „neue Arbeiten datetime vermeiden und stattdessen datetime2 / datetimeoffset verwenden sollten“.10 Es besteht keine Notwendigkeit, datetime-Spalten eines bestehenden Schemas zwanghaft zu migrieren, aber es gibt auch keinen Grund, ihn für eine neue Tabelle zu wählen.
Ob Sie UTC in datetime2 speichern oder datetimeoffset verwenden, hängt davon ab, „ob der Offset zum Eingabezeitpunkt später reproduzierbar sein muss“. Wenn Prüfungs- oder Compliance-Anforderungen verlangen, „welche Uhrzeit es in der Ortszeit des Benutzers war“ zu bewahren, verwenden Sie datetimeoffset; wenn es nur darum geht, den Zeitpunkt festzulegen, genügt UTC in datetime2. Beachten Sie, dass datetimeoffset ebenso wie DateTimeOffset nur den Offset trägt, nicht die Zeitzone (Anpassungsregeln) selbst. Wenn Sie auch die Zone benötigen, halten Sie die IANA-ID in einer separaten Spalte.
6.2 SQLite kennt keinen Datums-/Zeittyp
SQLite hat überhaupt keinen Speichertyp für Datum/Uhrzeit, und Microsoft.Data.Sqlite speichert DateTime / DateTimeOffset als TEXT.11 TEXT im ISO-8601-artigen Format bedeutet, dass Zeichenketten-Sortierreihenfolge gleich chronologischer Reihenfolge ist, „solange Format und Zeitzone konsistent sind“ – umgekehrt gilt aber: Sobald UTC und lokale Zeit in derselben Spalte gemischt werden, brechen sowohl Sortierung als auch Bereichsabfragen lautlos. Verwenden Sie SQLite, bleibt Ihnen nur, als Anwendungskonvention festzulegen, „diese Spalte ist UTC, und das ist ihr Format“. Die praktische Seite von SQLite – einschließlich Verbindungen und Transaktionen – haben wir in „SQLite in einer C#-Business-App verwenden“ ausgearbeitet.
6.3 Vorsicht bei EF Core / Dapper — Kind verschwindet beim Lesen
Liest man ein DateTime aus einem Typ zurück, der keine Referenzinformation trägt (datetime2, SQLites TEXT usw.), erhält man selbstverständlich Kind = Unspecified. Genau so entsteht der Vorfall, bei dem „wir haben bei der Speicherung auf UTC vereinheitlicht, aber irgendwo wird ein zurückgelesener Wert durch ToUniversalTime() geschickt“, was zu einer doppelten Konvertierung führt. Die Lösung ist, Kind an der Grenze wiederherzustellen. Mit EF Core lässt sich das einmalig, gesammelt, über einen Value-Converter deklarieren.
// EF Core: "diese Spalte ist UTC" einmalig auf Modellebene deklarieren
modelBuilder.Entity<Order>()
.Property(o => o.CreatedAtUtc)
.HasConversion(
// Beim Schreiben: alles, was nicht UTC ist (einschließlich versehentlicher DateTime.Now-Werte),
// an der Grenze auf UTC normalisieren. Beachten Sie, dass Unspecified bei der Konvertierung als lokale Zeit behandelt wird
v => v.Kind == DateTimeKind.Utc ? v : v.ToUniversalTime(),
// Beim Lesen: Kind wiederherstellen
v => DateTime.SpecifyKind(v, DateTimeKind.Utc));
Behandeln Sie die Normalisierung auf der Schreibseite ausschließlich als letztes Sicherheitsnetz. Da die Konvertierung eines Unspecified-Werts von der Zeitzoneneinstellung der ausführenden Maschine abhängt, sollten Sie dennoch jeden Code finden und beheben, der DateTime.Now oder Unspecified-Werte direkt in den Speicherpfad einspeist. Betrachten Sie es als zweischichtige Verteidigung: Sicherheitsnetz plus Konvention. Bei Dapper oder reinem ADO.NET sammeln Sie den DateTime.SpecifyKind-Aufruf des Mapping-Schritts in einer einzigen Umwandlungsschicht unmittelbar nach dem Mapping der Daten. Auch eine Namenskonvention hilft – allein die Referenz in Spalten- und Eigenschaftsnamen einzubacken (CreatedAtUtc, updated_at_utc) erhöht die Wahrscheinlichkeit erheblich, dass ein Prüfer bemerkt: „Ein ToUniversalTime-Aufruf auf diesem Wert wirkt falsch“, da Namen weit häufiger gelesen werden als Dokumentation.
7. Zeitsynchronisierung und Testen — w32time und TimeProvider
7.1 Die Annahme hinterfragen, dass die Uhr der Maschine korrekt ist
Alles bisher Gesagte ging davon aus, „die Uhr der Maschine selbst ist korrekt“ – doch was diese Uhr korrekt hält, ist der Windows-Zeitdienst (w32time). w32time synchronisiert sich über NTP mit einer Netzwerk-Zeitquelle, und in einer Active-Directory-Umgebung synchronisiert er sich entlang der Domänenhierarchie. Er ist die Grundlage für alles, was empfindlich auf Uhrabweichungen reagiert, einschließlich der Kerberos-Authentifizierung.12
Das hat zwei Implikationen für das App-Design. Erstens: Behandeln Sie die Uhr des Client-PCs nicht als maßgeblich für Geschäftslogik. Ein Gerät, dessen Synchronisierung gestoppt ist, kann ohne Weiteres um mehrere Minuten abweichen, sodass die Reihenfolge von Ereignissen und die Entscheidung über Stichzeiten anhand serverseitiger Zeit getroffen werden sollten, während clientseitige Zeit nur informativ ist. Zweitens: Prüfen Sie bei einer Meldung „die Uhrzeit sieht falsch aus“ zunächst mit w32tm /query /status den Synchronisierungsstatus des Geräts, bevor Sie überhaupt mit der Untersuchung der App beginnen. Das dauert eine Minute und schließt eine ganze Fehlerkategorie aus, bevor Sie in die Suche nach einem App-Fehler eintauchen.
7.2 Hören Sie auf, DateTime.Now hart zu codieren — TimeProvider
Das größte Hindernis beim Testen von Datums-/Zeitlogik ist das über den gesamten Code verstreute, hart codierte DateTime.Now. Möchten Sie „die Erkennung der Monatsendstichzeit“, „das Zurücksetzen von Sequenznummern über einen Jahreswechsel hinweg“ oder „den Zeitplan rund um einen DST-Umstellungstag“ testen, aber können die aktuelle Zeit nicht fixieren, bleibt Ihnen nichts übrig, als auf den betreffenden Tag zu warten.
.NET 8 hat die standardmäßige Zeitabstraktion TimeProvider eingeführt. Sie lässt sich GetUtcNow() / GetLocalNow() / LocalTimeZone / die Timer-Erzeugung über eine einzige Abstraktion austauschen. Derselbe Typ ist über das NuGet-Paket Microsoft.Bcl.TimeProvider auch unter .NET Framework 4.6.2 und höher sowie .NET Standard 2.0 verfügbar, sodass er sich auch in ältere Codebasen einführen lässt. Eine Testimplementierung, FakeTimeProvider, wird vom Paket Microsoft.Extensions.TimeProvider.Testing bereitgestellt.7
public sealed class DailyReportService
{
private readonly TimeProvider _clock;
private readonly TimeZoneInfo _siteTimeZone;
public DailyReportService(TimeProvider clock, TimeZoneInfo siteTimeZone)
{
_clock = clock;
_siteTimeZone = siteTimeZone;
}
// Eine Implementierung, die ausdrücklich festlegt, "das Geschäftsdatum wird anhand der Zeitzone des Standorts geschnitten" (Abschnitt 3.2)
public string GetReportDateKey()
{
var localNow = TimeZoneInfo.ConvertTime(_clock.GetUtcNow(), _siteTimeZone);
return localNow.ToString("yyyy-MM-dd", CultureInfo.InvariantCulture);
}
}
In der Produktion übergeben Sie TimeProvider.System; in Tests verwenden Sie FakeTimeProvider, um die Uhr frei zu fixieren und vorzuspulen.
[Fact]
public void BusinessDateSwitchesCorrectlyAcrossTheYearBoundary()
{
// Start fixiert auf 23:30 Uhr JST an Silvester
var clock = new FakeTimeProvider(
new DateTimeOffset(2026, 12, 31, 23, 30, 0, TimeSpan.FromHours(9)));
var tokyo = TimeZoneInfo.FindSystemTimeZoneById("Asia/Tokyo");
var svc = new DailyReportService(clock, tokyo);
Assert.Equal("2026-12-31", svc.GetReportDateKey());
clock.Advance(TimeSpan.FromHours(1)); // Den Jahreswechsel sofort reproduzieren
Assert.Equal("2027-01-01", svc.GetReportDateKey());
}
FakeTimeProvider kann neben dem Fixieren und manuellen Vorspulen der Uhr auch die lokale Zeitzone austauschen (SetLocalTimeZone), sodass sich ein Fehler wie „auf einem für Deutschland konfigurierten Gerät springen Daten einen Tag zurück“ auf einer für Japan konfigurierten CI-Maschine reproduzieren lässt. Wenn Sie an einem älteren .NET-Framework-Projekt arbeiten, bei dem selbst das Hinzufügen eines NuGet-Pakets schwierig ist, erzielt eine selbstgebaute IClock-Schnittstelle mit nichts weiter als DateTimeOffset UtcNow { get; } denselben Effekt. Entscheidend ist nicht, wie ausgefeilt die Abstraktion ist – sondern die „aktuelle Zeit“ als injizierbare Abhängigkeit zu behandeln.
Aus Erfahrung sind diese fünf Zeitpunkte das absolute Minimum, das mit Testfällen abgedeckt werden sollte: der Jahreswechsel (Silvester/Neujahr), Monatsenden (der 31., der 30. und Februar), der 29. Februar in einem Schaltjahr, die DST-Umstellungstage der Zielzeitzone (Frühjahr und Herbst) sowie die Zeit rund um Mitternacht (die Geschäftsdatumsgrenze). Jeder davon ist ein klassischer Lebensraum für Fehler, die nur „an diesem bestimmten Tag“ auftreten, und mit FakeTimeProvider lassen sich alle in wenigen Millisekunden testen.
8. Checkliste und Entscheidungstabelle
Die Datums-/Zeitverarbeitung ist, ganz ähnlich wie UUIDs, grundlegende Infrastruktur, die dazu neigt, mit einem „Nun, scheint ja zu funktionieren“ links liegen gelassen zu werden (eine Struktur, die dem, was wir in „Kollidieren UUIDs wirklich nie?“ geschrieben haben, sehr ähnlich ist). Legt man die Punkte, die man bei neuem Design und Codeprüfung betrachtet, fest, verringert das die Abhängigkeit vom Urteil Einzelner erheblich.
Checkliste für neue Designarbeit:
- Ist die Erfassung des aktuellen Zeitpunkts auf
DateTimeOffset.UtcNow/TimeProvider.GetUtcNow()vereinheitlicht? - Ist die Referenz für Speicherung/Übertragung (UTC vs. Offset-tragend) und das Format (“o” / ISO 8601) explizit in der Spezifikation dokumentiert?
- Sind die Datenbankspaltentypen und die Referenz festgelegt (für SQL Server
datetime2(UTC) oderdatetimeoffset;Utcin den Spaltennamen einbacken)? - Wird zwischen Zeitstempeln und Geschäftsdaten unterschieden, und wurde die Zeitzone, mit der das Datum geschnitten wird, festgelegt?
- Wurde ein Verwaltungsschema für Zeitzonen-IDs (IANA-IDs empfohlen) und die Fehlerbehandlung für eine ungültige ID festgelegt?
- Wurde eine DST-Richtlinie für geplante Ausführung (UTC-basierte Planung plus Idempotenz) festgelegt?
- Ist
TimeProvider/IClockinjizierbar, und existieren Testfälle für Zeitgrenzen?
Code, nach dem sich bei der Prüfung zu greppen lohnt:
| Findet sich dieser Code, seien Sie misstrauisch | Was schiefgehen kann | Wie man es behebt |
|---|---|---|
DateTime.Now |
Verschiebt sich nach einer Servermigration. Nicht testbar | UtcNow + nur zur Anzeige konvertieren. TimeProvider injizieren |
ToLocalTime() / ToUniversalTime() |
Implizite Interpretation von Unspecified (Kapitel 2) | Kind an der Grenze festlegen; nur unmittelbar vor der Anzeige konvertieren |
DateTime.Parse(s) (keine Styles angegeben) |
Abhängig von Kultur/Zeitzone der ausführenden Umgebung | ParseExact + InvariantCulture + RoundtripKind |
Speicherung/Übertragung mittels ToString("yyyy/MM/dd HH:mm") |
Referenzinformation geht verloren | Das “o”-Format + InvariantCulture |
Ein new DateTime(...)-Wert, direkt in Vergleichen oder Speicherung verwendet |
Unspecified-Werte schleichen sich ein | SpecifyKind anwenden oder zu DateTimeOffset wechseln |
Eine neue datetime-Spalte in SQL Server |
Bedenken zu Genauigkeit, Bereich und Zukunftssicherheit | datetime2 / datetimeoffset10 |
Der Großteil dieser Checkliste lässt sich innerhalb des ersten Tags einer neuen Entwicklungsarbeit entscheiden. Die Behebung nach der Inbetriebnahme des Systems wird dagegen zur Archäologie – für gespeicherte Daten ableiten, welcher Zeitraum unter welcher Referenz gespeichert wurde, und dann migrieren – und das ändert die Kosten um eine Größenordnung.
9. Zusammenfassung
Datums-/Zeit- und Zeitzonenvorfälle zeigen sich als bunte Mischung von Symptomen – „um neun Stunden verschoben“, „das Datum springt einen Tag zurück“, „der Batch lief zweimal“ –, aber die Ursache ist durchweg dieselbe: ein Wert ohne Referenzinformation überschreitet eine Grenze. Ebenso einheitlich ist die Lösung. Erfassen in UTC; speichern und übertragen in UTC oder mit Offset, unter Verwendung von ISO 8601 (“o”); nur zur Anzeige in lokale Zeit konvertieren. Legen Sie an jeder Grenze Format und Referenz explizit in der Spezifikation fest, und backen Sie die Referenz in Datenbankspaltennamen ein. Behandeln Sie Zeitzonen über TimeZoneInfo und IANA-IDs, und fangen Sie die nicht existierenden und mehrdeutigen Zeiten der Sommerzeit mit Eingabevalidierung und idempotenter geplanter Ausführung ab. Und injizieren Sie TimeProvider, damit sich sowohl der Jahreswechsel als auch DST-Umstellungstage in Tests reproduzieren lassen. Tun Sie all das, wechseln Sie von der Rolle desjenigen, der nach einer Umgebungsänderung gerufen wird, zu der desjenigen, der das Problem meldet, bevor es dazu kommt.
Wir übernehmen Ursachenermittlungen bei Zeitabweichungen infolge von Servermigrationen oder Cloud-Umzügen, Designprüfungen für die Datums-/Zeitverarbeitung sowie Modernisierungsunterstützung für die Auslandsbüro-Tauglichkeit (Zeitzonen- und DST-Unterstützung). Falls Ihre gespeicherten Daten bereits eine Mischung von Referenzen aufweisen und Sie nicht wissen, wie Sie das ordnen sollen, können wir auch bei der Bestandsaufnahme und der Migrationsplanung helfen.
Verwandte Artikel
- Aufgabenplanungs-Aufgaben, die nicht laufen oder mit 0x1 beendet werden — Ursachenisolierung und ein zuverlässiges Betriebsdesign
- SQLite in einer C#-Business-App verwenden — WAL-Modus, Sperrverhalten, Gegenmaßnahmen bei Beschädigung und wann stattdessen EF Core
- Wie man einen Windows-Dienst baut und betreibt — Von der Wahl zwischen Aufgabenplanung bis zur Umwandlung eines BackgroundService in einen Dienst
- Zeichenkodierung und Zeilenenden unter Windows - Die Grundlagen von Mojibake und CRLF/LF
Verwandte Beratungsleistungen
Die Komura Soft LLC übernimmt Designprüfungen für die Datums-/Zeit- und Zeitzonenverarbeitung in Business-Apps, Ursachenermittlungen bei Zeit- und Datumsabweichungen infolge von Servermigrationen oder Cloud-Umzügen sowie die Modernisierung der Datums-/Zeitverarbeitung für Auslandsbüro-Unterstützung und Legacy-Bestände.
- Technische Beratung & Designprüfung
- Windows-Anwendungsentwicklung
- Nutzung und Migrationsunterstützung bestehender Bestände
- Kontakt
Referenzlinks
-
Microsoft Learn, DateTime.Kind Property. Dazu, dass Kind standardmäßig Unspecified ist, und wie sich Kind auf das Ergebnis von ToLocalTime / ToUniversalTime auswirkt (Unspecified wird von ToLocalTime als UTC und von ToUniversalTime als lokale Zeit behandelt). ↩ ↩2 ↩3
-
Microsoft Learn, Choose between DateTime, DateOnly, DateTimeOffset, TimeSpan, TimeOnly, and TimeZoneInfo. Dazu, dass DateTimeOffset als empfohlener Standard-Datums-/Zeittyp für die Anwendungsentwicklung gilt, dass DateTimeOffset nur einen Offset trägt und nicht an eine Zeitzone gebunden ist, sowie dass DateOnly / TimeOnly unter .NET Framework nicht verfügbar sind. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Standard date and time format strings. Dazu, dass das Roundtrip-Format “o” ISO-8601-konform ist und Kind von DateTime sowie den Offset von DateTimeOffset in der Zeichenkette erhält, und dass sich der ursprüngliche Wert durch Parsen mit DateTimeStyles.RoundtripKind wiederherstellen lässt. ↩ ↩2
-
Microsoft Learn, What’s new in .NET 6. Dazu, dass TimeZoneInfo.FindSystemTimeZoneById in .NET 6 sowohl IANA- als auch Windows-Zeitzonen-IDs akzeptiert und automatisch konvertiert, sowie zur Hinzufügung von TryConvertIanaIdToWindowsId / TryConvertWindowsIdToIanaId. ↩ ↩2
-
Microsoft Learn, .NET globalization and ICU. Dazu, dass die Auflösung von IANA-Zeitzonen-IDs und die gegenseitigen Konvertierungs-APIs unter Windows von ICU abhängen, dass sie im NLS-Modus / Invariant-Globalization-Modus nicht verfügbar sind, und dass auf Betriebssystemen ohne ICU eine app-lokale ICU verwendet werden kann. ↩ ↩2
-
Microsoft Learn, TimeZoneInfo.IsInvalidTime(DateTime) Method. Zur Definition und Erkennung der durch die Umstellung auf Sommerzeit entstehenden “nicht existierenden Zeit” sowie zum Gegenstück IsAmbiguousTime (Erkennung mehrdeutiger Zeiten). ↩ ↩2
-
Microsoft Learn, What is TimeProvider?. Dazu, dass TimeProvider ab .NET 8 standardmäßig enthalten ist und unter .NET Framework 4.6.2+ / .NET Standard 2.0 über das Paket Microsoft.Bcl.TimeProvider verfügbar ist, zur Abstraktion von GetUtcNow / GetLocalNow / LocalTimeZone / Timer-Erzeugung, sowie zum für Tests gedachten FakeTimeProvider aus dem Paket Microsoft.Extensions.TimeProvider.Testing. ↩ ↩2
-
Microsoft Learn, TimeZoneInfo.ConvertTime Method. Dazu, dass Konsistenz zwischen DateTime.Kind und der Quellzeitzone erforderlich ist und eine Abweichung eine ArgumentException auslöst, dass mehrdeutige Zeiten als Normalzeit interpretiert werden, und dass eine nicht existierende Zeit eine ArgumentException auslöst. ↩ ↩2
-
Microsoft Learn, Globalization APIs use ICU libraries on Windows Server 2019. Dazu, dass ab .NET 7 ICU-Bibliotheken auch auf Windows Server 2019 und ähnlichen Betriebssystemversionen ohne gebündelte ICU verwendet werden, während zuvor eine manuelle Bereitstellung einer app-lokalen ICU erforderlich war. ↩
-
Microsoft Learn, datetime (Transact-SQL). Dazu, dass neue Arbeiten datetime vermeiden und stattdessen time / date / datetime2 / datetimeoffset verwenden sollten, sowie dass datetime2 / datetimeoffset höhere Genauigkeit bieten und datetimeoffset einen Zeitzonen-Offset unterstützt. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Data types (Microsoft.Data.Sqlite). Dazu, dass SQLite nur vier primitive Speichertypen hat und Microsoft.Data.Sqlite DateTime / DateTimeOffset als TEXT speichert. ↩
-
Microsoft Learn, Windows Time Service (W32Time). Dazu, dass der Windows-Zeitdienst Computeruhren über NTP im Netzwerk synchronisiert, zur Synchronisierungshierarchie innerhalb einer Active-Directory-Domäne, und zur Abhängigkeit der Kerberos-Authentifizierung von der Zeitsynchronisierung. ↩
Verwandte Artikel
Aktuelle Artikel mit denselben Schlagwörtern führen zu verwandten Themen weiter.
High-DPI-Unterstützung in WPF — Warum es trotz „eigentlich DPI-bewusst“ immer noch verschwimmt und durchblutet, und wie Sie es beheben
WPF layoutet die UI in DIPs (1/96 Zoll) und ist von Anfang an System DPI Aware, doch das Verschieben eines Fensters auf einen Monitor mit...
High-DPI-Unterstützung in WinForms — Warum die UI auf 4K-Monitoren verschwimmt oder zerbricht, und wie Sie das praktisch beheben
Dieser Artikel ordnet die Gründe, warum WinForms-Apps auf 4K-Monitoren und bei 150 % Skalierung verschwimmen oder ihr Layout zerbricht, a...
SQLite aus C# in Business-Apps nutzen — WAL-Modus, exklusive Sperren, Schutz vor Datenbankbeschädigung und wann sich EF Core lohnt
Ein praxisnaher Überblick zur Einbindung von SQLite in eine Windows-Businessanwendung mit Microsoft.Data.Sqlite: Verbindungszeichenfolgen...
Japanische Ära, Feiertage und Abrechnungsstichtage in Business-Apps — Übergangsresistentes Design, JapaneseCalendar und Werktagsberechnung in der Praxis
Auf einem Beleg muss „Reiwa 8“ stehen, die Werktagsberechnung muss Feiertage ausschließen, die Zahlung ist am letzten Werktag des Folgemo...
Wie man die Windows-Sitzungsisolierung versteht — Session 0, RDP und die gleichzeitige Nutzung durch mehrere Benutzer
Dieser Artikel erklärt das Konzept der Windows-„Sitzung“ (Session), das Windows-App-Entwickler immer wieder verwirrt. Er behandelt, warum...
Verwandte Themen
Diese Seiten ordnen den Artikel in einen größeren Leistungs- und Entscheidungskontext ein.
Technische Windows-Themen
Portal zu Windows-Entwicklung, Fehleranalyse und der Nutzung bestehender Assets.
Leistungen zu diesem Thema
Dieser Artikel ist direkt mit den folgenden Leistungen verbunden.
Windows-App-Entwicklung
Geschäftsanwendungen, Geräteintegration und Kommunikationstools von den Anforderungen bis zur Umsetzung.
Häufige Fragen
Fragen, die in Beratungen zu diesem Artikelthema häufig gestellt werden.
- Warum verschieben sich nach einer Servermigration alle Zeitstempel um neun Stunden?
- Die Ursache liegt darin, dass ein Wert ohne Information darüber, „auf welche Referenz sich diese Zeit bezieht“, eine Grenze wie eine Datenbank oder eine API überschreitet. In .NET erhalten DateTime-Werte, die über Konstruktoren erzeugt, aus Zeichenketten geparst oder aus Datenbanken gelesen werden, standardmäßig Kind=Unspecified, und die Konvertierungsmethoden nehmen dabei stillschweigend eine Referenz an: ToLocalTime behandelt Unspecified als UTC, während ToUniversalTime es als lokale Zeit behandelt. Da das Ergebnis von der Zeitzoneneinstellung der ausführenden Maschine abhängt, bleibt der Fehler auf einem JST-konfigurierten On-Premises-Server unsichtbar und tritt schlagartig zutage, sobald die App auf eine UTC-konfigurierte Cloud-VM umzieht – ohne dass jemand am Code etwas geändert hat.
- Sollte ich in C# DateTime oder DateTimeOffset verwenden?
- Für neuen Code besagt die offizielle Richtlinie ausdrücklich, dass DateTimeOffset als Standard-Datums-/Zeittyp für die Anwendungsentwicklung gelten sollte: Er trägt immer einen UTC-Offset, sodass der Wert allein einen eindeutigen Zeitpunkt festlegt und für die implizite Interpretation von Kind kein Raum bleibt. Beachten Sie, dass ein Offset keine Zeitzone ist – +09:00 sagt nichts über den Ort aus und enthält keine Sommerzeitregeln, sodass zur Wiedergabe der lokalen Uhrzeit weiterhin TimeZoneInfo nötig ist. Bei bestehenden Codebasen, bei denen eine vollständige Umschreibung nicht realistisch ist, ist ein gängiger Kompromiss, intern und bei der Speicherung bei DateTime mit Kind=Utc zu bleiben und an den Grenzen DateTimeOffset oder ISO-8601-Zeichenketten im „o“-Format zu verwenden.
- Wie speichert man Datum und Uhrzeit korrekt in einer Datenbank?
- Speichern und übertragen Sie in UTC (oder mit explizitem Offset) und konvertieren Sie nur für die Anzeige in lokale Zeit. Verwenden Sie in SQL Server für UTC-gespeicherte Spalten datetime2 oder datetimeoffset, wenn Prüfanforderungen verlangen, den Offset der Eingabezeit zu reproduzieren – der veraltete Typ datetime wird für neue Arbeiten offiziell nicht empfohlen. SQLite kennt überhaupt keinen Datums-/Zeittyp und speichert Werte als TEXT, sodass Sie Format und UTC-Referenz als Anwendungskonvention festlegen müssen. Die Referenz in Spalten- und Eigenschaftsnamen einzubacken, etwa CreatedAtUtc, und Kind beim Lesen wiederherzustellen (zum Beispiel mit einem EF-Core-Value-Converter), verhindert doppelte Konvertierungen.
- Wie teste ich Datums-/Zeitlogik wie Jahreswechsel oder DST-Grenzen?
- Hören Sie auf, DateTime.Now hart zu codieren, und injizieren Sie stattdessen TimeProvider – das ist in .NET 8 Standard und über das Paket Microsoft.Bcl.TimeProvider auch unter .NET Framework 4.6.2+ verfügbar. In Tests lässt sich mit FakeTimeProvider (aus Microsoft.Extensions.TimeProvider.Testing) die Uhr fixieren, sofort vorspulen und sogar die lokale Zeitzone austauschen, sodass sich ein Fehler, der nur auf einem für Deutschland konfigurierten Gerät auftritt, auf einer für Japan konfigurierten CI-Maschine reproduzieren lässt. Die Mindestgrenzfälle, die es zu testen lohnt, sind der Jahreswechsel, Monatsenden einschließlich Februar, der 29. Februar in einem Schaltjahr, die DST-Umstellungstage der Zielzeitzone sowie Zeiten rund um Mitternacht, an denen sich das Geschäftsdatum ändert.
Autorenprofil
Profilseite des Artikelautors.
Go Komura
Geschäftsführer von KomuraSoft LLC
Spezialisiert auf Windows-Softwareentwicklung, technische Beratung und Fehleranalyse, insbesondere bei bestehenden Systemen und schwer reproduzierbaren Störungen.