Stop Using DateTime.Now: Make Time a Testable Dependency in .NET
Time is one of those dependencies that developers often forget to treat as a dependency.
A database connection is obviously external. An HTTP API is external. A file system is external. An email service is external. Because those resources can change or fail, we normally place abstractions around them when they are part of important application logic.
The current date and time are different.
They look like simple values that can be retrieved whenever a piece of code needs them:
var now = DateTime.Now;That line is short, familiar, and perfectly valid C#. The problem begins when direct access to the machine clock becomes scattered throughout business logic.
Once an application starts depending on the current time for decisions, expiration rules, scheduling, validation, billing, subscriptions, reports, notifications, or other workflows, the clock becomes part of the application’s environment.
And an environmental dependency can make tests surprisingly difficult.
Modern .NET provides a cleaner approach. Starting with .NET 8, TimeProvider is included in the framework and gives applications an abstraction for obtaining time and working with related timing functionality.
Instead of asking the operating system for the current time from inside a business rule, the application can receive a TimeProvider through dependency injection.
That small architectural change creates something valuable:
The application can use the real clock in production and a controlled clock during testing.
This article explains why that matters, how to implement it, how FakeTimeProvider works, when UTC should be used, what changes on older .NET versions, and how to gradually introduce this approach into an existing codebase.
Why the Current Time Is an External Dependency
Consider a simple subscription rule.
A subscription has an expiration timestamp, and the application needs to determine whether the subscription is still active.
A straightforward implementation might look like this:
public bool IsActive(DateTimeOffset expiresAt)
{
return DateTimeOffset.UtcNow < expiresAt;
}At first glance, there is nothing obviously wrong with this code.
It is concise.
It uses UTC.
It compares two points in time.
It may work perfectly in production.
But the method has an invisible dependency: the real-world clock.
The result of the method depends not only on the expiresAt argument, but also on when the method happens to execute.
That means this method is not completely controlled by its inputs.
For example, imagine:
Expiration: 2026-08-31 00:00:00 UTCIf the method runs at:
2026-08-30 23:59:59 UTCthe subscription is active.
Run the same method a few seconds later:
2026-08-31 00:00:01 UTCand the result changes.
That behavior is correct for a real application.
The testing problem is that the test itself does not control the clock.
The Problem With Scattered Calls to DateTime.Now
DateTime.Now is not inherently forbidden or broken.
The issue is architectural.
A small utility that needs the current local time may use it without creating a significant problem. But when business rules repeatedly access DateTime.Now, DateTime.UtcNow, or similar system-clock APIs directly, the application’s behavior becomes coupled to the environment in which the code runs.
Imagine a codebase containing:
DateTime.Nowin a validator.
Another occurrence appears in:
OrderServiceAnother appears in:
SubscriptionServiceAnother appears in:
BackgroundJobAnd another appears inside:
InvoiceCalculatorEach individual usage may look harmless.
The problem appears when you need to test a particular moment.
You might want to test:
- exactly midnight,
- one second before expiration,
- one second after expiration,
- the final day of a trial,
- a date several months in the future,
- a historical date,
- a scheduled job before its execution time,
- a timeout after a known interval.
If every class reads the machine clock directly, the tests have no clean way to tell those classes what time it should be.
The test becomes dependent on the real calendar.
That is the dependency we want to remove.
Time Changes Even When Your Code Does Not
One reason time-related bugs can be frustrating is that the source code can remain unchanged while the result changes.
Suppose a rule says:
A discount is available until midnight.A developer writes a test based on the current date.
The test passes.
A few weeks later, the same test may execute under a different date or time condition.
The production code has not changed.
The test environment has not necessarily changed.
But the clock has.
This is fundamentally different from testing a pure calculation.
For example:
int Add(int a, int b)
{
return a + b;
}Given:
2 + 3the result is always:
5The current date has no influence.
But a method like:
bool IsExpired(DateTimeOffset expiresAt)
{
return DateTimeOffset.UtcNow >= expiresAt;
}has another input that is not visible in the method signature:
the current time.
That hidden input is exactly what makes time-dependent tests difficult.
Time Is a Dependency Just Like Other Infrastructure
Dependency injection is often discussed in terms of services:
Database
Email sender
Payment gateway
HTTP client
Message queue
File storageThe clock belongs in the same conceptual category when business logic depends on it.
The application needs an answer to a question:
What time is it?
Instead of having every class answer that question by directly consulting the operating system, we can give the class a component responsible for providing time.
That component becomes an explicit dependency.
The difference is small in code but significant in design.
Instead of:
public sealed class TrialService
{
public bool IsActive(DateTimeOffset endsAt)
{
return DateTimeOffset.UtcNow < endsAt;
}
}we can write:
public sealed class TrialService(TimeProvider timeProvider)
{
public bool IsActive(DateTimeOffset endsAt)
{
return timeProvider.GetUtcNow() < endsAt;
}
}Now the service does not need to know where the time comes from.
It only knows that it has a TimeProvider.
In production, that provider can represent the real system clock.
During a test, the provider can represent whatever moment the test needs.
What Is TimeProvider?
TimeProvider is an abstraction provided by .NET for accessing time and related timing functionality.
In .NET 8 and later, it is part of the framework. Microsoft also documents a package-based option for earlier supported frameworks through Microsoft.Bcl.TimeProvider.
Among other capabilities, TimeProvider exposes methods for obtaining UTC and local time, retrieving timestamps, measuring elapsed time, and creating timers.
For many business applications, the most important member to start with is:
GetUtcNow()which returns a DateTimeOffset.
For example:
var currentTime = timeProvider.GetUtcNow();The important architectural difference is that the code is no longer tied directly to the operating system’s clock.
The source of time has become replaceable.
Register the System Clock With Dependency Injection
In an ASP.NET Core application, the real clock can be registered with the dependency injection container.
For example:
builder.Services.AddSingleton(TimeProvider.System);TimeProvider.System is the framework-provided implementation that represents the system clock. Microsoft documents it as the default implementation for obtaining system-based time and related timing functionality.
Once registered, application services can receive TimeProvider through constructor injection.
For example:
public sealed class TrialService(TimeProvider timeProvider)
{
public bool IsActive(DateTimeOffset trialEndsAt)
{
return timeProvider.GetUtcNow() < trialEndsAt;
}
}The service does not create the clock.
It does not call a static global clock.
It does not know whether the clock is real or fake.
That responsibility belongs to the application’s composition root and dependency injection configuration.
Why Constructor Injection Matters
The constructor tells you what a class requires.
For example:
public sealed class TrialService(TimeProvider timeProvider)
{
}Anyone reading the class can immediately see that the service depends on time.
Compare that with:
public sealed class TrialService
{
public bool IsActive(DateTimeOffset trialEndsAt)
{
return DateTimeOffset.UtcNow < trialEndsAt;
}
}The dependency is hidden inside the implementation.
There is no indication from the constructor that the service relies on the current time.
That distinction becomes increasingly useful as applications grow.
A class with explicit dependencies is generally easier to reason about because its required external inputs are visible at the boundary.
Prefer UTC for Stored Instants
When an application records an event such as:
Order created
Payment received
Subscription started
Subscription expired
Password reset requested
User registeredthe application usually cares about a specific point in time.
For these kinds of timestamps, UTC is generally the appropriate representation for persistence and comparisons.
With TimeProvider, that commonly means:
var createdAt = timeProvider.GetUtcNow();Because GetUtcNow() returns DateTimeOffset, the value carries offset information as well as the date and time.
This is useful when values cross application boundaries.
A server in one geographic region and a server in another should not interpret a persisted event according to their respective local clocks.
The application can store and compare an absolute point in time consistently, then convert it to a user’s local time when displaying it.
UTC and Local Time Have Different Jobs
UTC and local time are not interchangeable concepts.
Suppose an application stores:
2026-08-31 18:00:00 UTCA user interface may need to display that event according to the user’s time zone.
That is a presentation concern.
The business layer can work with the instant, while the UI determines how that instant should appear to a particular user.
This separation is especially important in applications serving users across multiple time zones.
A common architecture is therefore:
Database
↓
UTC / absolute timestamp
↓
Application logic
↓
User's time zone
↓
DisplayThis does not mean local time is never appropriate.
There are legitimate business rules based on local calendar time, such as:
- a store opens at 9:00 AM local time,
- a report is generated at local midnight,
- a booking belongs to a particular calendar day,
- a meeting occurs at a specified local time.
The key is to model those requirements deliberately instead of allowing the server’s local time zone to decide accidentally.
DateTime.Now Can Hide a Time-Zone Assumption
One of the common problems with DateTime.Now is that it represents local system time.
The word “local” sounds harmless until an application moves between environments.
For example, an application might be developed on one machine and deployed to infrastructure configured with a different time zone.
If business logic says:
if (DateTime.Now.Hour >= 18)
{
// evening behavior
}what does “18” mean?
The developer’s local time?
The server’s local time?
The customer’s local time?
The company’s headquarters?
The answer cannot be determined from the code.
The code has silently inherited the machine’s local time zone.
That may be exactly what you want in a small application.
But if the rule is important, the time-zone requirement should be explicit.
Using TimeProvider does not automatically solve every time-zone problem. What it does provide is a clear abstraction through which time can be obtained and tested. TimeProvider supports both UTC and local-time access, with GetLocalNow() deriving local time according to the provider’s time-zone notion.
The architectural decision still belongs to the application.
Testing Is Where the Difference Becomes Obvious
The biggest benefit of injecting a clock appears in unit tests.
Imagine a trial that ends at:
2026-08-31 00:00:00 UTCWe want to verify two conditions.
Before the expiration moment:
2026-08-30 23:59:59 UTCthe trial should be active.
At the expiration moment:
2026-08-31 00:00:00 UTCthe trial should no longer be active.
If production code reads the real clock, the test cannot simply choose those moments.
With a controllable time provider, it can.
Using FakeTimeProvider
The Microsoft.Extensions.TimeProvider.Testing package provides FakeTimeProvider, a synthetic TimeProvider designed for deterministic tests. It allows tests to set and advance the clock deliberately.
A test can start at an exact moment:
var clock = new FakeTimeProvider(
new DateTimeOffset(
2026,
8,
30,
23,
59,
59,
TimeSpan.Zero));Then pass that clock to the service:
var service = new TrialService(clock);Now the test controls the application’s notion of the current time.
For example:
var trialEndsAt = new DateTimeOffset(
2026,
8,
31,
0,
0,
0,
TimeSpan.Zero);
Assert.True(service.IsActive(trialEndsAt));The test is no longer waiting for a particular date.
It explicitly creates the date it wants.
Move the Clock to the Exact Boundary
The fake clock can then be moved to the expiration instant:
clock.SetUtcNow(trialEndsAt);And the same business rule can be tested again:
Assert.False(service.IsActive(trialEndsAt));This is much more useful than creating a test that depends on the day on which the test happens to run.
The boundary condition becomes part of the test’s data.
The test says, in effect:
At 23:59:59 → active
At 00:00:00 → inactiveThat is much easier to understand when reading the test months later.
Microsoft’s documentation describes FakeTimeProvider as a synthetic provider intended to enable deterministic test behavior, and it supports operations such as setting the UTC time and advancing the clock.
Test the Boundary, Not Just the Happy Path
Time-dependent business logic often fails around boundaries.
For example, suppose a rule is:
A coupon is valid until 12:00 UTC.Testing only this scenario:
10:00 UTC → validdoes not tell you whether the implementation handles the boundary correctly.
More valuable tests include:
11:59:59 → valid
12:00:00 → invalid
12:00:01 → invalidThe exact boundary depends on the application’s business rule.
The important principle is that the test should be able to place the application immediately before, exactly at, and immediately after the boundary.
A controllable clock makes those tests straightforward.
A Better Subscription Example
Consider a subscription service:
public sealed class SubscriptionService(TimeProvider timeProvider)
{
public bool IsSubscriptionActive(
DateTimeOffset expiresAt)
{
return timeProvider.GetUtcNow() < expiresAt;
}
}The rule is simple:
Current time < expiration timeThat gives us a clear boundary.
If:
current < expirationthe subscription is active.
If:
current == expirationthe subscription is not active.
If:
current > expirationthe subscription is not active.
The test can represent each state explicitly.
That is a major advantage of making time injectable: the business rule remains simple while the test gains precise control.
Time-Dependent Code Is Not Limited to Expiration
Expiration is only one example.
Many common application features depend on the clock.
Password Reset Tokens
A password-reset link might have a limited lifetime.
The application needs to determine whether:
issuedAt + lifetimehas already passed.
Testing this with the real clock can be awkward.
With a fake clock, you can create a token and advance the clock to its exact expiration point.
Account Verification
An email verification link may be valid for a particular duration.
A test might need to verify:
Immediately after creation → valid
Just before expiration → valid
At expiration → invalid
After expiration → invalidA controllable clock lets each state be reproduced.
Promotional Discounts
Suppose an offer ends at a specific UTC timestamp.
The application could have a rule like:
public bool IsOfferActive(DateTimeOffset endsAt)
{
return timeProvider.GetUtcNow() < endsAt;
}Tests can then choose the exact moment at which the offer should stop.
Scheduled Notifications
Applications frequently need to determine whether something is due.
For example:
Send reminder 24 hours before an appointment.Instead of waiting 24 hours in a test, the test can move its clock.
This is particularly useful when the application also uses timers or delay-based operations.
TimeProvider is not limited to simply retrieving a date. Microsoft’s documentation also describes support for timers, timestamps, elapsed-time measurement, and integration with certain asynchronous timing APIs.
Testing Time Without Waiting for Time
This is one of the simplest ways to understand the value of a fake clock.
Suppose production behavior says:
Wait 30 minutes.A conventional test might actually wait 30 minutes.
That is obviously undesirable.
A test using a controllable time source can model the passage of time without making the test suite sit idle for half an hour.
The same idea applies to:
5 minutes
1 hour
24 hours
7 days
30 days
1 yearThe test controls the time instead of waiting for the wall clock.
For asynchronous operations, TimeProvider is also integrated with several .NET APIs that accept a provider for timing behavior, including Task.Delay and Task.WaitAsync.
This makes the abstraction useful beyond simple GetUtcNow() calls.
FakeTimeProvider Can Also Advance Time
A test does not always need to jump directly to a particular timestamp.
Sometimes it is more natural to move time forward by a duration.
Conceptually:
clock.Advance(TimeSpan.FromHours(24));This allows a test to model the passage of time.
For example:
Start
↓
Create trial
↓
Advance 7 days
↓
Check trialThis can make long-duration workflows easier to express.
Microsoft’s API documentation lists Advance(TimeSpan) along with SetUtcNow, SetLocalTimeZone, and other operations on FakeTimeProvider.
Automatic Time Advancement
FakeTimeProvider also supports an AutoAdvanceAmount setting.
This controls how much the fake clock advances whenever the clock is read.
That can be useful in tests where each clock read is intended to represent the passage of a known amount of time.
However, this should be used carefully.
A test is often easier to understand when important time transitions are explicit.
Compare:
Read clock
Automatically move forward
Read clock againwith:
Start at 10:00
Advance one hour
Read clockThe second version makes the intended scenario easier to see.
The best choice depends on what behavior the test is trying to model.
Time Zones Still Need Deliberate Design
Injecting TimeProvider does not mean developers can stop thinking about time zones.
It solves a different problem.
TimeProvider gives the application an abstraction for time.
Your application still needs to decide whether a business rule concerns:
- an absolute instant,
- UTC,
- a user’s local time,
- a business location’s local time,
- a calendar date,
- or a recurring local-time schedule.
These are different concepts.
For example:
2026-09-01 09:00 UTCand:
2026-09-01 09:00 Bangladesh timedo not represent the same instant.
A reliable application should not accidentally treat them as interchangeable.
DateTime and DateTimeOffset Are Not the Same
When dealing with real-world timestamps, developers often need to decide between DateTime and DateTimeOffset.
For many application-level events representing an actual point in time, DateTimeOffset can make the intended representation clearer because it carries an offset alongside the date and time.
TimeProvider.GetUtcNow() returns a DateTimeOffset.
That fits naturally with expiration timestamps and persisted event times.
For example:
DateTimeOffset createdAt = timeProvider.GetUtcNow();The type communicates that the value represents a timestamp rather than merely a calendar date.
This does not eliminate all time-zone concerns, but it makes the data model more explicit.
What If You Are Using an Older .NET Version?
The TimeProvider API is built into .NET 8 and later. Microsoft’s documentation also describes Microsoft.Bcl.TimeProvider for earlier supported .NET versions and .NET Framework targets.
If introducing that package is not practical for a particular application, the same architectural principle can still be implemented manually.
For example:
public interface IDateTimeProvider
{
DateTimeOffset UtcNow { get; }
}A production implementation might use the system clock:
public sealed class SystemDateTimeProvider
: IDateTimeProvider
{
public DateTimeOffset UtcNow =>
DateTimeOffset.UtcNow;
}Then the service depends on:
IDateTimeProviderinstead of directly calling:
DateTimeOffset.UtcNowA test can provide a fake implementation.
The API is different from TimeProvider, but the design principle is the same:
make the clock replaceable.
Why TimeProvider Is Preferable in Modern .NET
If you are targeting modern .NET, TimeProvider has an important advantage over creating your own one-method abstraction for every project.
It is a framework-level abstraction.
It already covers more than retrieving the current date and time.
Microsoft documents capabilities including:
- UTC time,
- local time,
- timestamps,
- elapsed-time measurement,
- timers,
- and integration with timing-related asynchronous APIs.
That means an application can use one consistent abstraction instead of creating multiple unrelated interfaces for different timing requirements.
It also aligns application code with the APIs provided by the modern .NET ecosystem.
Avoid Creating a Global Static Clock
A tempting workaround is to create something like:
public static class Clock
{
public static DateTimeOffset Now =>
DateTimeOffset.UtcNow;
}Then application code can call:
Clock.NowThis may appear cleaner than writing:
DateTimeOffset.UtcNoweverywhere.
But it still leaves the application dependent on a global static source.
The test still needs a mechanism to change global state.
That can create other problems, especially when tests run concurrently.
Dependency injection avoids that global-state problem.
Each service receives the clock it should use.
A test can create its own clock.
Different tests can use different moments without modifying shared application state.
Keep Business Rules Independent of the System Clock
The goal is not to remove all references to real time from your application.
The goal is to control where those references live.
A useful structure is:
Application startup
↓
TimeProvider.System
↓
Dependency Injection
↓
Application service
↓
Business ruleDuring a test:
Test
↓
FakeTimeProvider
↓
Application service
↓
Same business ruleThe production and test versions use the same service.
Only the time source changes.
That is exactly what we want from dependency injection.
A Complete Minimal Example
Here is a simple service:
public sealed class TrialService(TimeProvider timeProvider)
{
public bool IsActive(DateTimeOffset trialEndsAt)
{
return timeProvider.GetUtcNow() < trialEndsAt;
}
}Register the system clock:
builder.Services.AddSingleton(TimeProvider.System);Then a test can create its own provider:
var clock = new FakeTimeProvider(
new DateTimeOffset(
2026,
8,
30,
23,
59,
59,
TimeSpan.Zero));
var service = new TrialService(clock);
var trialEndsAt = new DateTimeOffset(
2026,
8,
31,
0,
0,
0,
TimeSpan.Zero);
Assert.True(service.IsActive(trialEndsAt));
clock.SetUtcNow(trialEndsAt);
Assert.False(service.IsActive(trialEndsAt));The production behavior is based on the real clock.
The test behavior is based on a controlled clock.
The business logic is identical in both environments.
A Useful Pattern for Existing Applications
You do not need to rewrite an entire codebase overnight.
If an existing application contains many calls to:
DateTime.Nowor:
DateTime.UtcNowstart with the areas where time affects business behavior.
Look for code related to:
Expiration
Billing
Subscriptions
Authentication
Authorization
Discounts
Scheduling
Notifications
Reports
Caching
Retries
Background jobs
Audit records
Token lifetimeThese areas are more likely to benefit from controllable time.
A simple migration strategy is:
Step 1: Find direct clock access
Search the solution for:
DateTime.Now
DateTime.UtcNow
DateTimeOffset.Now
DateTimeOffset.UtcNowAlso inspect custom helper classes that may hide those calls.
Step 2: Identify business-critical usages
Not every occurrence needs immediate refactoring.
Separate simple presentation code from logic that makes decisions based on time.
Step 3: Introduce TimeProvider
For modern .NET applications:
builder.Services.AddSingleton(TimeProvider.System);Step 4: Inject it
Change services that own time-dependent rules:
public MyService(TimeProvider timeProvider)
{
_timeProvider = timeProvider;
}Step 5: Replace direct clock access
Instead of:
DateTimeOffset.UtcNowuse:
_timeProvider.GetUtcNow()Step 6: Add boundary tests
Test:
before
exactly at
afterthe relevant time boundary.
This approach lets the architecture evolve incrementally.
Do Not Inject Time Into Every Class Automatically
There is an important balance here.
Using dependency injection does not mean every class in the application should receive a TimeProvider.
If a class does not depend on time, it does not need the dependency.
For example:
public sealed class TaxCalculator
{
public decimal Calculate(decimal amount)
{
return amount * 0.15m;
}
}There is no reason to add:
TimeProviderjust because the application uses one elsewhere.
Dependencies should represent actual requirements.
If a class needs the current time to make a decision, inject the clock.
If it does not, keep the class simple.
Be Careful With “Today”
Date-only business rules deserve particular attention.
Consider:
The offer is available today.What does “today” mean?
It depends on a time zone.
At the same instant, one location may already be on the next calendar date while another is still on the previous date.
Therefore, code like:
DateTime.Now.Datecan be problematic if the business meaning of “today” belongs to a specific customer, store, or geographic region.
Before implementing a date-based rule, define whose calendar is being used.
Then obtain the relevant time according to that rule.
TimeProvider gives you a way to centralize and test the clock, but the business requirement still determines the correct time zone.
Testing Midnight Correctly
Midnight is a classic boundary case.
Suppose a report is available only during a particular calendar day.
A weak test might simply create a date and verify the normal case.
A stronger set of tests would deliberately check the transition:
23:59:59 → previous day
00:00:00 → new day
00:00:01 → new dayThe exact expected behavior depends on the rule.
What matters is that the test can create those conditions without relying on the test machine’s actual clock.
This is where fake time becomes particularly useful.
Testing Long-Term Expiration
The same idea applies to longer periods.
Suppose a trial lasts 30 days.
A test should not need to wait 30 days.
Instead, the test can create the trial at a known instant and move the fake clock forward.
Conceptually:
Trial starts
↓
Advance 29 days
↓
Still active
↓
Advance 1 day
↓
ExpiredThe test becomes both fast and deterministic.
It also documents the business rule clearly.
Someone reading the test can understand exactly how the trial behaves without needing to know when the test suite was executed.
Testing Historical Scenarios
Controllable time is not only useful for future dates.
Sometimes an application contains rules that depend on historical dates.
For example:
A feature existed only after a specific release date.or:
A legacy pricing rule applied before a certain date.Testing such behavior with the real clock can be awkward because the current date is far away from the historical scenario.
A fake clock can place the application at the relevant historical instant.
This makes time-based behavior reproducible.
Time and Background Services
Background workers are another area where time can become difficult to test.
Imagine a worker that checks every few minutes whether something needs to happen.
If the worker uses real time directly, tests can become slow or complicated.
By using the TimeProvider abstraction consistently, time-related behavior can be modeled under a controlled clock.
The framework’s TimeProvider abstraction includes timer support in addition to its clock APIs.
This is one reason it can be more useful than a custom interface containing only:
UtcNowThe abstraction can cover a broader range of time-related behavior.
Time Is Not the Same as Duration
Another important distinction is between a point in time and an elapsed duration.
For example:
2026-08-31 12:00 UTCis a point in time.
While:
30 minutesis a duration.
Applications often need both.
A timeout might say:
wait for 30 secondswhile an expiration rule might say:
valid until 12:00 UTCTimeProvider supports both current-time access and timing-related operations, including timestamps and elapsed-time measurement.
Understanding the difference helps avoid mixing calendar calculations with duration calculations.
What About Performance?
Replacing:
DateTimeOffset.UtcNowwith:
timeProvider.GetUtcNow()is primarily an architectural decision.
The main purpose is not to make the clock faster.
The purpose is to make time replaceable and testable.
Do not introduce TimeProvider because you expect it to improve every application’s runtime performance.
Introduce it where controllable time improves design, testing, and correctness.
What About Logging?
Logging timestamps is slightly different from business rules.
For example, application infrastructure may automatically add timestamps to logs.
That does not necessarily mean every logging operation needs to receive a business-level clock.
The important distinction is whether the timestamp is being used to make an application decision.
If a timestamp is simply infrastructure metadata, the logging framework may already own that concern.
If the timestamp determines whether an order is expired, a discount is valid, or a token can be accepted, then the business logic should have a controllable time source.
What About Database Timestamps?
Databases can also generate timestamps.
For example, a database may set a creation timestamp automatically.
That can be useful.
But again, distinguish between:
recording an eventand:
making a business decisionIf the application needs to determine whether something has expired, the application needs a reliable notion of the current time.
If the database is simply recording when a row was inserted, database-generated timestamps may be an appropriate infrastructure choice.
The architecture should define which system owns each timestamp.
Common Mistakes When Refactoring Time
Moving from direct clock access to TimeProvider is simple, but there are still a few mistakes worth avoiding.
Mistake 1: Injecting DateTime
Do not solve the problem by passing the current time into every method manually unless that is genuinely the desired API.
For example:
service.IsActive(
DateTimeOffset.UtcNow,
expiration);This can work, but it pushes clock acquisition responsibility onto every caller.
A service that owns a time-dependent business rule can instead receive TimeProvider.
Mistake 2: Mixing Local Time and UTC
A service should not randomly compare:
DateTime.Nowagainst:
DateTimeOffset.UtcNowwithout deliberately handling the difference.
Choose the representation that matches the business rule.
For persisted instants and cross-system comparisons, UTC is generally the safer foundation.
Mistake 3: Testing Only Normal Cases
Time bugs frequently appear at boundaries.
Do not test only:
10:00 → validAlso consider:
just before boundary
exactly at boundary
just after boundarywhen those conditions matter to the rule.
Mistake 4: Assuming TimeProvider Solves Time Zones Automatically
It does not.
It gives you a clock abstraction.
Your application still needs to define the correct time zone for local-calendar rules.
A Practical Rule of Thumb
A useful way to decide whether to inject a clock is to ask:
Would the result of this method change if I ran it one second later?
If the answer is no, the method probably does not need the clock.
If the answer is yes, consider making time an explicit dependency.
For example:
public bool IsExpired(DateTimeOffset expiresAt)is actually dependent on the current time even though the signature does not show it.
With:
public sealed class TokenService(TimeProvider timeProvider)that dependency becomes visible.
This makes the code easier to understand and the test easier to control.
The Goal Is Predictable Software
Good tests should be repeatable.
If a test depends on:
the current date
the server time zone
the time of day
the speed of executionthen it has dependencies that may be difficult to control.
A fake clock removes one major source of uncertainty.
The test can say:
Start at this exact instant.
Move forward by this exact amount.
Execute the operation.
Verify the result.That is much closer to a deterministic experiment.
A Small Refactoring Can Create a Useful Seam
You do not need a massive architectural rewrite to start benefiting from this approach.
A class that currently contains:
DateTimeOffset.UtcNowcan often be changed to:
public sealed class MyService(TimeProvider timeProvider)and:
timeProvider.GetUtcNow()The production application registers:
builder.Services.AddSingleton(TimeProvider.System);The test provides:
new FakeTimeProvider(...)That is a relatively small change.
But it creates a clear seam between business logic and the environment’s clock.
When Direct DateTime.Now Is Not a Disaster
It is also worth avoiding an overly rigid rule.
The presence of DateTime.Now does not automatically mean that an application is badly designed.
There are situations where direct time access is perfectly adequate.
For example, a small script might simply display the current local time:
Console.WriteLine(DateTime.Now);There may be no meaningful business logic to test.
Likewise, code at an infrastructure or presentation boundary may reasonably obtain the current time.
The important question is not:
Is
DateTime.Nowsyntactically allowed?
It obviously is.
The better question is:
Is this code making an important decision based on an uncontrolled environmental value?
If yes, introducing an explicit clock dependency is often worth considering.
The Modern .NET Approach
For applications targeting .NET 8 or newer, TimeProvider gives developers a built-in abstraction for this problem.
The basic architecture can be summarized as:
Production
----------
TimeProvider.System
↓
Dependency Injection
↓
Business Serviceand:
Testing
-------
FakeTimeProvider
↓
Business ServiceThe business service does not change.
Only the source of time changes.
Microsoft’s documentation specifically describes FakeTimeProvider as a controllable implementation intended to make time-dependent behavior deterministic in tests.
That is the central idea.
Final Thoughts
Time looks simple because every computer already has a clock.
But from the perspective of application architecture, the current time is an external input.
It changes continuously.
It can differ between environments.
It can interact with time zones.
And many important business rules depend on exact moments or boundaries.
When those rules read the system clock directly, testing can become unnecessarily difficult.
Modern .NET provides a practical alternative.
Register:
builder.Services.AddSingleton(TimeProvider.System);Inject:
TimeProviderRead UTC time through:
timeProvider.GetUtcNow();Then use:
FakeTimeProviderwhen tests need complete control over the application’s notion of time.
The benefit is not simply that the code looks cleaner.
The real benefit is that time becomes explicit.
A test can move from yesterday to tomorrow without changing the machine’s clock.
An expiration boundary can be tested without waiting.
A historical scenario can be reproduced.
A production service can use the real system clock without changing its business logic.
And time-zone decisions can be made deliberately instead of being inherited accidentally from the server.
If a class makes decisions based on “now,” consider making “now” one of its dependencies.
That small design decision can make time-sensitive code significantly easier to reason about and test.
The production application can use the real clock. The test can use a clock it controls. The business rule stays the same.
That is the real value of treating time as a dependency.



