Skip to content

Conversation

@egil
Copy link
Contributor

@egil egil commented Oct 18, 2025

Summary & Motivation

Replaces all static DateTimeOffset.UtcNow and TimeProvider.System.GetUtcNow() calls with injected TimeProvider to enable time mocking in tests. This makes time-sensitive business logic (expiration, rate limiting, token generation) fully testable.

Key changes:

  • Services inject TimeProvider: AccessTokenGenerator, RefreshTokenGenerator, BlobStorageClient, and handlers now inject TimeProvider for timestamp operations
  • Domain methods accept DateTimeOffset: Methods like EmailConfirmation.HasExpired() now accept DateTimeOffset now parameter instead of calling static time directly, maintaining clean domain boundaries
  • TimeProviderAccessor for entity construction: Introduces TimeProviderAccessor using AsyncLocal<TimeProvider> to provide CreatedAt timestamps during entity construction, ensuring computed properties like EmailConfirmation.ValidUntil calculate correctly immediately after creation
  • UpdateAuditableEntitiesInterceptor: Now uses TimeProvider from DbContext for setting ModifiedAt, with validation that CreatedAt is already set
  • Test infrastructure: Tests use TimeProvider.System by default; TimeProviderAccessor.Current can be overridden for tests requiring time control
  • DbContext updates: All SharedKernelDbContext implementations now accept TimeProvider as dependency and implement ITimeProviderSource interface
  • SecurityTokenDescriptorExtensions: Token generation now accepts both now and expires timestamps to properly set NotBefore and IssuedAt claims

Minor fixes:

  • Added DateTimeOffset type handling in SqliteConnectionExtensions.Insert() for test data
  • Updated backend AI rules documentation to reflect TimeProvider usage patterns

Downstream projects

Search for TimeProvider.System in your codebase and apply these changes:

  1. Update your DbContext implementations to accept and pass TimeProvider:
-public sealed class YourDbContext(DbContextOptions<YourDbContext> options, IExecutionContext executionContext)
-    : SharedKernelDbContext<YourDbContext>(options, executionContext);
+public sealed class YourDbContext(DbContextOptions<YourDbContext> options, IExecutionContext executionContext, TimeProvider timeProvider)
+    : SharedKernelDbContext<YourDbContext>(options, executionContext, timeProvider);
  1. Inject TimeProvider into all handlers, services, and repositories that use timestamps, and replace TimeProvider.System.GetUtcNow() with timeProvider.GetUtcNow():
-public sealed class YourHandler(IRepository repository)
+public sealed class YourHandler(IRepository repository, TimeProvider timeProvider)
     : IRequestHandler<YourCommand, Result>
 {
     public async Task<Result> Handle(YourCommand command, CancellationToken cancellationToken)
     {
-        var expiresAt = TimeProvider.System.GetUtcNow().AddMinutes(30);
+        var expiresAt = timeProvider.GetUtcNow().AddMinutes(30);
         // ...
     }
 }
  1. Update your-self-contained-system/Tests/EndpointBaseTest.cs to add TimeProvider field:
 public abstract class EndpointBaseTest<TContext> : IDisposable where TContext : DbContext
 {
     protected readonly ServiceCollection Services;
+    [UsedImplicitly] protected readonly TimeProvider TimeProvider;
     
     protected EndpointBaseTest()
     {
         Services = new ServiceCollection();
+        TimeProvider = TimeProvider.System;
         // ...
     }
 }
  1. Replace all TimeProvider.System.GetUtcNow() calls with TimeProvider.GetUtcNow() in test files to use the instance field.

Checklist

  • I have added tests, or done manual regression tests
  • I have updated the documentation, if necessary

Copilot AI review requested due to automatic review settings October 18, 2025 22:23
@CLAassistant
Copy link

CLAassistant commented Oct 18, 2025

CLA assistant check
All committers have signed the CLA.

Copy link

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull Request Overview

This PR refactors the codebase to use TimeProvider via dependency injection instead of directly using TimeProvider.System.GetUtcNow(). The goal is to enable controlled time during testing while distinguishing between business logic that should use injectable TimeProvider and infrastructure code that should use DateTimeOffset.UtcNow.

  • Creates a custom FromPlatformServicesAttribute for easier keyed service injection
  • Registers TimeProvider as a keyed singleton service in dependency injection
  • Replaces TimeProvider.System.GetUtcNow() calls with injected TimeProvider in business logic
  • Uses DateTimeOffset.UtcNow for infrastructure code that cannot work with fake TimeProvider

Reviewed Changes

Copilot reviewed 29 out of 29 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
SharedKernel.csproj Added TimeProviderExtensions package dependency
AudibleEntity.cs Changed to use DateTimeOffset.UtcNow for entity timestamps
SharedDependencyConfiguration.cs Added TimeProvider registration as keyed service
FromPlatformServicesAttribute.cs New custom attribute for platform service injection
Token generation files Updated to use DateTimeOffset.UtcNow for JWT timestamps
Test files Updated to use injected TimeProvider for controllable time
Repository/Handler files Injected TimeProvider for business logic operations
EmailConfirmation domain Updated methods to accept TimeProvider parameter
Directory.Packages.props Added TimeProviderExtensions package version
AuthenticationCookieMiddleware.cs Injected TimeProvider for token validation
backend.mdc Updated coding guidelines for TimeProvider usage

@tjementum
Copy link
Member

Hi @egil,

Thanks for the pull-request.

I introduced TimeProvider just when it was introduced in .NET 8, but never got around to truly making it possible to inject it. I will take a look at your Pull Request asap.

You should run pp check (dotnet run check from the developer-cli folder), and fix all static code analysis warnings.

As you noticed, there are conventions for commit messages, which made the Pull Request Conventions fail. One line in imperative form (no description, as nobody sees them). Also, please squash your commits into two idiomatic commits (that is, remove "Fix spelling mistake", "Remove unused parameter", "Revert line breaks") ;)

@tjementum tjementum force-pushed the correct-use-of-time-provider branch from 64bdb53 to 23a9dfe Compare December 19, 2025 00:57
@tjementum tjementum requested a review from a team as a code owner December 19, 2025 00:57
@tjementum tjementum force-pushed the correct-use-of-time-provider branch from 23a9dfe to d02acdf Compare December 19, 2025 01:04
@tjementum tjementum self-assigned this Dec 19, 2025
@tjementum tjementum added the Enhancement New feature or request label Dec 19, 2025
@tjementum tjementum moved this to 🏗 In Progress in Kanban board Dec 19, 2025
@tjementum tjementum changed the title Refactor to use TimeProvider via injected service Replace static time access with injected TimeProvider for testability Dec 19, 2025
@tjementum tjementum force-pushed the correct-use-of-time-provider branch from d02acdf to 041d17a Compare December 19, 2025 01:10
Copy link
Member

@tjementum tjementum left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @egil

I had a few comments and change requests, so your pull request became stale. I have now made the implementation a bit more straightforward by removing the keyed service and dropping the passing of timeProvider as a parameter to methods, etc.

I also introduced a new TimeProviderAccessor, which will set the CreatedAt timestamp of aggregates to the mocked TimeProvider. It's a bit hidden as it's a static class, and maybe not the best design. But I feel it's important that you can mock the CreatedAt and ModifiedAt timestamps. And I did not like the alternative of passing timeProvider as paramter when creating all domain objects.

@tjementum tjementum enabled auto-merge December 19, 2025 01:31
@tjementum tjementum merged commit ead9685 into platformplatform:main Dec 19, 2025
23 checks passed
@github-project-automation github-project-automation bot moved this from 🏗 In Progress to ✅ Done in Kanban board Dec 19, 2025
@egil
Copy link
Contributor Author

egil commented Dec 19, 2025

Hi @tjementum, apologies for the radio silence on this. Have had a lot going on in my life last few months so this slipped through the cracks. Happy that you could use what I shared and fix the problems I caused 🙂

I also introduced a new TimeProviderAccessor, which will set the CreatedAt timestamp of aggregates to the mocked TimeProvider. It's a bit hidden as it's a static class, and maybe not the best design. But I feel it's important that you can mock the CreatedAt and ModifiedAt timestamps. And I did not like the alternative of passing timeProvider as paramter when creating all domain objects.

Yep, TimeProvider works best in DI scenarios, for sure, and did notice the friction with your particular domain object creation/setup approach.

If you can figure out a good pattern that allows time(stamps) to be controlled during testing, I have no objections.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Enhancement New feature or request

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants