Skip to content

Commit 7064d50

Browse files
committed
chore(template): sync with dailydevops/template-dotnet [skip ci]
1 parent 2ae13f2 commit 7064d50

File tree

1 file changed

+193
-0
lines changed

1 file changed

+193
-0
lines changed
Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
<!-- List of authors who contributed to this decision. Include full names and roles if applicable. -->
2+
authors:
3+
- Martin Stühmer
4+
5+
<!--
6+
The patterns this decision applies to. Each entry is a glob pattern that matches files affected by this decision.
7+
Example:
8+
applyTo:
9+
- "**/*.cs" # Applies to all C# files
10+
- "src/**/*.razor" # Applies to all Blazor components in src folder
11+
- "tests/**/*.sql" # Applies to all SQL files in tests folder
12+
-->
13+
applyTo:
14+
- "**/*.cs"
15+
16+
<!-- The date this ADR was initially created in YYYY-MM-DD format. -->
17+
created: 2026-01-21
18+
19+
<!--
20+
The most recent date this ADR was updated in YYYY-MM-DD format.
21+
IMPORTANT: Update this field whenever the decision is modified.
22+
-->
23+
lastModified: 2026-01-21
24+
25+
<!--
26+
The current state of this ADR. If superseded, include references to the superseding ADR.
27+
Valid values: proposed, accepted, deprecated, superseded
28+
-->
29+
state: accepted
30+
31+
<!--
32+
A compact AI LLM compatible definition of this decision.
33+
This should be a precise, structured description that AI systems can easily parse and understand.
34+
Include the core decision, key rationale, and primary impact in 1-2 concise sentences.
35+
-->
36+
instructions: |
37+
MUST use DateTimeOffset instead of DateTime for all date and time values unless external constraints require DateTime.
38+
MUST use TimeProvider for obtaining current time to ensure testability.
39+
MUST inject TimeProvider via constructor dependency injection.
40+
MUST NOT use DateTime.Now, DateTime.UtcNow, DateTimeOffset.Now, or DateTimeOffset.UtcNow directly in production code.
41+
MAY use DateOnly or TimeOnly only for performance-critical scenarios or UI display requirements.
42+
---
43+
44+
# Decision: DateTimeOffset and TimeProvider Usage
45+
46+
This decision establishes the mandatory use of `DateTimeOffset` over `DateTime` and requires `TimeProvider` for all time-related operations to ensure consistent timezone handling and testability.
47+
48+
## Context
49+
50+
Date and time handling in software development presents several challenges:
51+
52+
1. **Timezone Ambiguity**: `DateTime` does not inherently store timezone information. The `Kind` property (`Local`, `Utc`, `Unspecified`) is often misinterpreted or lost during serialization, leading to incorrect time calculations across different timezones.
53+
54+
2. **Serialization Issues**: When `DateTime` values are serialized and deserialized (JSON, databases, APIs), timezone context is frequently lost, causing subtle bugs that are difficult to diagnose.
55+
56+
3. **Testability Concerns**: Direct calls to `DateTime.Now` or `DateTime.UtcNow` create hidden dependencies on the system clock, making unit tests non-deterministic and time-dependent scenarios impossible to test reliably.
57+
58+
4. **Global Applications**: Modern applications often serve users across multiple timezones, requiring precise handling of temporal data with explicit offset information.
59+
60+
5. **.NET Evolution**: .NET 8 introduced `TimeProvider` as a first-class abstraction for time operations, providing a standardized approach for testable time-dependent code.
61+
62+
## Decision
63+
64+
The project MUST adhere to the following requirements for date and time handling:
65+
66+
### DateTimeOffset over DateTime
67+
68+
* MUST use `DateTimeOffset` instead of `DateTime` for all date and time values.
69+
* MUST only use `DateTime` when external constraints require it (for example, third-party APIs, legacy database schemas, or framework limitations).
70+
* MUST document the reason when `DateTime` usage is unavoidable.
71+
* MUST convert `DateTime` values to `DateTimeOffset` at system boundaries as early as possible.
72+
73+
### DateOnly and TimeOnly Usage
74+
75+
* MAY use `DateOnly` or `TimeOnly` only when one of the following conditions applies:
76+
- Performance-critical scenarios where the reduced memory footprint is measurable and relevant.
77+
- UI display requirements where only date or time components are needed for presentation.
78+
* MUST NOT use `DateOnly` or `TimeOnly` as a general replacement for `DateTimeOffset`.
79+
* MUST convert `DateOnly` or `TimeOnly` to `DateTimeOffset` when persisting to databases or transmitting via APIs.
80+
* MUST document the reason when using `DateOnly` or `TimeOnly`.
81+
82+
### TimeProvider for Current Time
83+
84+
* MUST use `TimeProvider` to obtain the current time in all production code.
85+
* MUST inject `TimeProvider` via constructor dependency injection.
86+
* MUST NOT call `DateTime.Now`, `DateTime.UtcNow`, `DateTimeOffset.Now`, or `DateTimeOffset.UtcNow` directly.
87+
* MUST use `TimeProvider.System` as the default implementation in production.
88+
* MUST use `Microsoft.Extensions.Time.Testing.FakeTimeProvider` or custom implementations for testing.
89+
90+
### Implementation Pattern
91+
92+
```csharp
93+
public class OrderService
94+
{
95+
private readonly TimeProvider _timeProvider;
96+
97+
public OrderService(TimeProvider timeProvider)
98+
{
99+
ArgumentNullException.ThrowIfNull(timeProvider);
100+
_timeProvider = timeProvider;
101+
}
102+
103+
public Order CreateOrder(OrderRequest request)
104+
{
105+
ArgumentNullException.ThrowIfNull(request);
106+
107+
return new Order
108+
{
109+
Id = Guid.NewGuid(),
110+
CreatedAt = _timeProvider.GetUtcNow(),
111+
// ... other properties
112+
};
113+
}
114+
}
115+
```
116+
117+
### Test Implementation Pattern
118+
119+
```csharp
120+
public class OrderServiceTests
121+
{
122+
[Test]
123+
public async Task CreateOrder_ShouldSetCreatedAtToCurrentTime()
124+
{
125+
// Arrange
126+
var fakeTime = new DateTimeOffset(2026, 1, 21, 10, 30, 0, TimeSpan.Zero);
127+
var fakeTimeProvider = new FakeTimeProvider(fakeTime);
128+
var service = new OrderService(fakeTimeProvider);
129+
var request = new OrderRequest { /* ... */ };
130+
131+
// Act
132+
var order = service.CreateOrder(request);
133+
134+
// Assert
135+
await Assert.That(order.CreatedAt).IsEqualTo(fakeTime);
136+
}
137+
}
138+
```
139+
140+
## Consequences
141+
142+
### Positive Consequences
143+
144+
* **Timezone Safety**: `DateTimeOffset` explicitly stores the offset from UTC, eliminating timezone ambiguity.
145+
* **Serialization Reliability**: Timezone information is preserved during serialization and deserialization.
146+
* **Deterministic Tests**: `TimeProvider` injection enables fully controllable and repeatable time-based tests.
147+
* **Time Travel Testing**: Tests can simulate past and future dates without system clock manipulation.
148+
* **Framework Alignment**: Aligns with .NET 8+ best practices and the `TimeProvider` abstraction.
149+
* **Cross-Timezone Correctness**: Calculations involving multiple timezones produce correct results.
150+
151+
### Negative Consequences
152+
153+
* **Migration Effort**: Existing code using `DateTime` requires refactoring.
154+
* **Dependency Injection Overhead**: Every class requiring current time needs `TimeProvider` injection.
155+
* **Learning Curve**: Developers unfamiliar with `DateTimeOffset` or `TimeProvider` require onboarding.
156+
* **Storage Considerations**: Some databases handle `DateTimeOffset` differently than `DateTime`.
157+
* **Third-Party Limitations**: Some external libraries or APIs may still require `DateTime`.
158+
159+
## Alternatives Considered
160+
161+
### Continue Using DateTime with UTC Convention
162+
163+
**Description**: Enforce a convention that all `DateTime` values are UTC and rely on `DateTimeKind.Utc`.
164+
165+
**Rejection Rationale**:
166+
* Convention-based approaches are error-prone and not compiler-enforced.
167+
* `DateTimeKind` is easily lost during serialization.
168+
* Does not address testability concerns.
169+
* Requires manual discipline across the entire codebase.
170+
171+
### Custom Time Abstraction
172+
173+
**Description**: Create a project-specific `ITimeService` or `IClock` interface for time operations.
174+
175+
**Rejection Rationale**:
176+
* Reinvents functionality already provided by .NET's `TimeProvider`.
177+
* Creates maintenance burden for custom abstraction.
178+
* Reduces interoperability with libraries expecting `TimeProvider`.
179+
* `TimeProvider` is the standardized .NET solution since .NET 8.
180+
181+
### Static Time Helper with Ambient Context
182+
183+
**Description**: Use a static `TimeHelper.Now` property that can be swapped during testing via ambient context.
184+
185+
**Rejection Rationale**:
186+
* Ambient context patterns hide dependencies and reduce code clarity.
187+
* Static access makes parallel test execution problematic.
188+
* Violates explicit dependency principle.
189+
* Not thread-safe without careful implementation.
190+
191+
## Related Decisions
192+
193+
* [.NET 10 and C# 13 Adoption](./2025-07-11-dotnet-10-csharp-13-adoption.md) - This decision builds upon the .NET version adoption which provides `TimeProvider` as a standard framework feature.

0 commit comments

Comments
 (0)