Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige
Lesedauer 6 Min.

Übersehen seit .NET 8: TimeProvider für testbare Zeit

Seit .NET 8 gibt es TimeProvider als offizielle Zeit-Abstraktion. Doch noch zweieinhalb Jahre später ist sie in vielen Projekten unbekannt.
© EMGenie

Kaum eine Anwendung kommt ohne Bezug zur aktuellen Uhrzeit aus. Tokens laufen ab, Caches werden invalidiert, geplante Aufgaben starten zu festen Zeitpunkten, Audit-Logs erhalten Zeitstempel. Üblicherweise greifen Entwicklerinnen und Entwickler dafür zu DateTime.Now oder DateTime.UtcNow. Beide Aufrufe sind statisch, liefern bei jedem Aufruf einen anderen Wert und entziehen sich jeder Kontrolle von außen. Damit wird Zeit zu einem impliziten Seiteneffekt, der sich in Tests nur mit Klimmzügen simulieren lässt.

Bereits mit .NET 8 hat Microsoft im November 2023 eine Abstraktion eingeführt, die genau dieses Problem adressiert: TimeProvider. Inzwischen sind zwei weitere LTS-Releases ins Land gegangen, das aktuelle .NET 10 ist seit Ende 2025 verfügbar. Trotzdem zeigt der Blick in viele Codebasen ein anderes Bild: Statische DateTime-Aufrufe und selbstgestrickte IClock-Interfaces dominieren weiterhin. Dabei lohnt sich der Wechsel auf den offiziellen Standard auch in bestehenden Projekten.

Warum DateTime.UtcNow ein Testproblem ist

Ein typisches Beispiel ist die Validierung eines Tokens. Eine Methode prüft, ob das in einem Token hinterlegte Ablaufdatum bereits überschritten ist. Wird dafür direkt DateTime.UtcNow verwendet, hängt das Ergebnis vom realen Systemzeitpunkt ab, zu dem der Test ausgeführt wird, siehe Listing 1.

Listing 1: Untestbare Token-Validierung mit DateTime.UtcNow

 

public class TokenValidator
{
    public bool IsValid(Token token)
    {
        return token.ExpiresAt > DateTime.UtcNow;
    }
}


 

Das Problem zeigt sich beim Versuch, einen Test zu schreiben, der das Ablaufverhalten reproduzierbar prüft. Soll der Test verifizieren, dass ein Token nach Ablauf seiner Gültigkeitsdauer als ungültig erkannt wird, müsste er entweder warten, bis die Zeit tatsächlich verstrichen ist, oder das Ablaufdatum künstlich in die Vergangenheit setzen. Beides ist unbefriedigend: Wartende Tests werden langsam und flaky, manipulierte Daten testen nicht das eigentliche Verhalten, sondern eine Abkürzung.

Die alte Lösung mit selbstgebauten IClock-Interfaces

Über Jahre hat sich in der .NET-Welt ein Pattern etabliert, das diesem Problem begegnet: Ein eigenes Interface, oft IClock oder ISystemClock genannt, abstrahiert den Zugriff auf die Uhrzeit. Eine Produktiv-Implementierung delegiert an DateTime.UtcNow, eine Test-Implementierung liefert kontrollierbare Werte.

Der Ansatz funktioniert, hat aber Schwächen. Jedes Projekt erfindet das Interface neu, mit leicht abweichenden Methoden und Properties. Manche Varianten liefern DateTime, andere DateTimeOffset, wieder andere bieten zusätzlich eine Today-Property oder Methoden für relative Zeitberechnungen. Bibliotheken, die ihrerseits auf Zeit angewiesen sind, müssen entweder ein eigenes Interface mitbringen oder eines aus einer fremden Bibliothek übernehmen. Eine konsistente Lösung über Projektgrenzen hinweg gibt es nicht.

Hinzu kommt, dass die meisten dieser Eigenbauten nur das Auslesen der aktuellen Zeit abbilden. Operationen wie Task.Delay oder das Erzeugen eines Timer bleiben außen vor und sind damit ebenfalls untestbar. Wer einen Hintergrunddienst schreibt, der alle fünf Minuten eine Aktion ausführt, muss für den Test entweder die Intervalldauer drastisch verkürzen oder den eigentlichen Scheduling-Mechanismus durch eine Test-Variante ersetzen. Beides verfälscht das, was eigentlich geprüft werden soll.

TimeProvider als offizieller Standard

Mit TimeProvider hat .NET 8 diese Lücke geschlossen. Die abstrakte Klasse aus dem System-Namespace stellt Methoden bereit, um die aktuelle UTC-Zeit über GetUtcNow() und die lokale Zeit über GetLocalNow() abzufragen, liefert die aktuelle Zeitzone via LocalTimeZone und bietet darüber hinaus eine eigene CreateTimer()-Variante. Damit deckt sie nicht nur das einfache Auslesen ab, sondern auch zeitgesteuerte Operationen, die in der Vergangenheit besonders schwer zu testen waren.

Die Standardimplementierung ist über die statische Property TimeProvider.System verfügbar. Sie verhält sich exakt wie die bekannten statischen Methoden auf DateTime und DateTimeOffset und bildet damit den Default für den Produktivbetrieb. Der entscheidende Unterschied: Statt direkt auf einen statischen Aufruf zuzugreifen, erhält die Klasse ihre Zeitquelle als Abhängigkeit injiziert, siehe Listing 2.

 


Listing 2: Token-Validierung mit injiziertem TimeProvider

 

public class TokenValidator
{
    private readonly TimeProvider timeProvider;
    public TokenValidator(TimeProvider timeProvider)
    {
        this.timeProvider = timeProvider;
    }
    public bool IsValid(Token token)
    {
        return token.ExpiresAt > timeProvider.GetUtcNow();
    }
}

Aus dem impliziten Seiteneffekt wird so eine explizite Abhängigkeit, die sich wie jede andere injizieren, ersetzen und kontrollieren lässt.

Deterministische Tests mit FakeTimeProvider

Für Tests stellt Microsoft das NuGet-Paket Microsoft.Extensions.TimeProvider.Testing bereit. Es enthält die Klasse FakeTimeProvider, die von TimeProvider ableitet und vollständige Kontrolle über die simulierte Zeit erlaubt. Der initiale Zeitpunkt lässt sich im Konstruktor festlegen, die Methode Advance() spult die Zeit um eine definierte Spanne vor, SetUtcNow() springt zu einem absoluten Zeitpunkt.

Besonders wertvoll ist das Verhalten bei Timern: Ein über den FakeTimeProvider erzeugter Timer reagiert nicht auf reale Zeit, sondern ausschließlich auf die simulierte. Ein Aufruf von Advance() löst ihn deterministisch aus, ohne dass der Test auch nur eine Millisekunde warten muss. Damit lassen sich auch zeitgesteuerte Hintergrundprozesse zuverlässig prüfen, siehe Listing 3.


Listing 3: Unit-Test mit FakeTimeProvider

 

[Fact]
public void IsValid_ReturnsFalse_WhenTokenHasExpired()
{
    var startTime = new DateTimeOffset(2026, 1, 1, 12, 0, 0, TimeSpan.Zero);
    var timeProvider = new FakeTimeProvider(startTime);
    var validator = new TokenValidator(timeProvider);
    var token = new Token(ExpiresAt: startTime.AddMinutes(5));
    timeProvider.Advance(TimeSpan.FromMinutes(10));
    Assert.False(validator.IsValid(token));
}

Der Test ist vollständig deterministisch, läuft in Mikrosekunden und beschreibt das fachliche Verhalten präzise: Nach zehn Minuten ist ein Token mit fünf Minuten Gültigkeit abgelaufen. Keine Wartezeit, keine Flakiness, keine künstlichen Datumsmanipulationen.

Über das einfache Vorspulen hinaus erlaubt der FakeTimeProvider auch das Steuern der sogenannten Auto-Advance-Amount. Damit lässt sich konfigurieren, dass jeder Aufruf von GetUtcNow() die Zeit automatisch um einen festen Betrag weiterstellt. Solche Feinheiten helfen, komplexere Szenarien zu testen, etwa wenn mehrere Operationen in kurzem zeitlichem Abstand erfolgen sollen, ohne dass jeder Aufruf einzeln synchronisiert werden muss.

Integration in Dependency Injection und Migration

Die Registrierung im Dependency-Injection-Container ist denkbar einfach. Ein einziger Aufruf genügt, um TimeProvider.System als Singleton zu hinterlegen:

 

services.AddSingleton(TimeProvider.System);

 

Ab diesem Punkt kann jede Klasse TimeProvider als Konstruktorabhängigkeit deklarieren und erhält im Produktivbetrieb die Standardimplementierung, in Tests den FakeTimeProvider. Bestehende Projekte, die bereits ein eigenes IClock-Interface verwenden, müssen nicht in einem großen Schritt migriert werden. Ein schmaler Adapter, der IClock auf TimeProvider abbildet, erlaubt eine schrittweise Umstellung. Neue Komponenten nutzen direkt TimeProvider, alte werden bei Gelegenheit nachgezogen.

Erwähnenswert sind außerdem die neuen Überladungen einiger Methoden im Basis-Framework. So akzeptiert Task.Delay inzwischen einen TimeProvider als Parameter. Damit lassen sich auch Wartezeiten innerhalb asynchroner Workflows kontrolliert testen, ohne eigene Wrapper schreiben zu müssen. Ähnliches gilt für CancellationTokenSource, die einen TimeProvider für ihre Time-out-Funktionalität entgegennimmt. Auch PeriodicTimer lässt sich mit einem TimeProvider instanzieren und reiht sich damit nahtlos in die neue Welt ein.

Für Bibliotheksautorinnen und -autoren ergibt sich daraus eine wichtige Empfehlung: Wer eine wiederverwendbare Komponente schreibt, die mit Zeit umgeht, sollte TimeProvider als optionalen Konstruktorparameter anbieten und im Standardfall auf TimeProvider.System zurückfallen. Damit bleibt das API für einfache Anwendungsfälle unverändert, während fortgeschrittene Szenarien wie Tests oder Simulationen sauber unterstützt werden.

Zeit als erstklassige Abhängigkeit

TimeProvider ist auf den ersten Blick eine kleine Ergänzung. Die Auswirkungen auf das Design sind jedoch beträchtlich. Zeit verliert ihren Sonderstatus als impliziter, global verfügbarer Wert und wird zu einer Abhängigkeit wie jede andere auch. Das verbessert nicht nur die Testbarkeit einzelner Klassen, sondern macht auch architektonische Entscheidungen sichtbarer: Welche Komponente braucht überhaupt Zugriff auf die Zeit? Wo wird sie weitergereicht, wo entsteht sie?

Für neue Projekte gibt es seit .NET 8 keinen Grund mehr, eigene Abstraktionen für die Uhrzeit zu bauen. Für bestehende Projekte lohnt sich der Blick auf TimeProvider als Zielbild einer schrittweisen Migration. Der offizielle Standard löst ein Problem, das die .NET-Community zwei Jahrzehnte lang mit unterschiedlichen Eigenbauten umschifft hat. Wer zweieinhalb Jahre nach der Einführung immer noch DateTime.UtcNow direkt im Code stehen hat, sollte den nächsten Refactoring-Durchgang dafür nutzen.

Neueste Beiträge

Sicherheit, Offline-Betrieb und Recovery mit Cursor - Die KI-IDE Cursor in der Praxis, Teil 3
Cursor schützt Code durch den Privacy Mode und verhindert so das Training von Modellen mit Nutzerdaten. Während die KI-Rechenleistung primär cloudbasiert ist, erfolgt das Indexing der Codebase lokal. Ausfallsicherheit und Recovery werden durch Multi-File-Undo-Workflows gewährleistet.
8 Minuten
17. Jun 2026
GitHub Copilot mit Markdown-Dateien steuern
GitHub Copilot liest Markdown-Dateien, die an bestimmten Orten im System oder im Projekt liegen. Wer diese Dateien gezielt einsetzt, gibt Copilot dauerhaften Kontext – ohne ihn bei je-dem Chat-Start neu erklären zu müssen.
5 Minuten
22. Jun 2026
KI-gestützte Softwareentwicklung: Zurück zu den Wurzeln
Die Grundsätze und bewährten Methoden guter Softwareentwicklung bleiben relevant, auch wenn die Transformation der Entwicklungspraktiken durch KI-gestützte Codegenerierung radikal verläuft.
5 Minuten
15. Jun 2026

Das könnte Dich auch interessieren

Elektronische Schaltkreise im Browser simulieren - Simulation
Statt mit Steckfeld oder Lötkolben kann man auf dieser Website Schaltungen per Drag and Drop zusammenstellen und deren Verhalten testen.
2 Minuten
26. Jul 2018
C#-.NET-Apps mit WinUI 3 - Komponentenbasierte Apps mit Fluent/FAST, Teil 3
Microsoft macht mit WinUI 3 ein natives User-Experience-Framework für Windows verfügbar, dessen Komponenten auf dem Microsoft-eigenen Design-System Fluent 2 basieren.
23 Minuten
13. Mai 2024
C# 14, Blazor und die Desktop-Frage - Was sind die Killer Features der aktuellen Versionen?
C# 14 bringt echte Verbesserungen für den Entwickleralltag – aber nicht jedes neue Feature ist ein Game Changer. Microsoft MVP Thomas-Claudius Huber sortiert, was in der Praxis zählt, erklärt, wann Blazor React schlägt, und warum WPF noch lange nicht zum alten Eisen gehört.
19. Mai 2026
Anzeige
Anzeige
Anzeige
Anzeige
Anzeige