|
| 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