-
Notifications
You must be signed in to change notification settings - Fork 48
Replace static time access with injected TimeProvider for testability #785
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Replace static time access with injected TimeProvider for testability #785
Conversation
There was a problem hiding this 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
FromPlatformServicesAttributefor 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.UtcNowfor 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 |
application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs
Outdated
Show resolved
Hide resolved
application/shared-kernel/SharedKernel/Configuration/SharedDependencyConfiguration.cs
Outdated
Show resolved
Hide resolved
application/account-management/Tests/Signups/StartSignupTests.cs
Outdated
Show resolved
Hide resolved
|
Hi @egil, Thanks for the pull-request. I introduced You should run 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") ;) |
64bdb53 to
23a9dfe
Compare
23a9dfe to
d02acdf
Compare
d02acdf to
041d17a
Compare
tjementum
left a comment
There was a problem hiding this 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.
|
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 🙂
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. |
Summary & Motivation
Replaces all static
DateTimeOffset.UtcNowandTimeProvider.System.GetUtcNow()calls with injectedTimeProviderto enable time mocking in tests. This makes time-sensitive business logic (expiration, rate limiting, token generation) fully testable.Key changes:
AccessTokenGenerator,RefreshTokenGenerator,BlobStorageClient, and handlers now inject TimeProvider for timestamp operationsEmailConfirmation.HasExpired()now acceptDateTimeOffset nowparameter instead of calling static time directly, maintaining clean domain boundariesTimeProviderAccessorusingAsyncLocal<TimeProvider>to provideCreatedAttimestamps during entity construction, ensuring computed properties likeEmailConfirmation.ValidUntilcalculate correctly immediately after creationModifiedAt, with validation thatCreatedAtis already setTimeProvider.Systemby default;TimeProviderAccessor.Currentcan be overridden for tests requiring time controlSharedKernelDbContextimplementations now acceptTimeProvideras dependency and implementITimeProviderSourceinterfacenowandexpirestimestamps to properly setNotBeforeandIssuedAtclaimsMinor fixes:
DateTimeOffsettype handling inSqliteConnectionExtensions.Insert()for test dataDownstream projects
Search for
TimeProvider.Systemin your codebase and apply these changes:TimeProvider:TimeProviderinto all handlers, services, and repositories that use timestamps, and replaceTimeProvider.System.GetUtcNow()withtimeProvider.GetUtcNow():your-self-contained-system/Tests/EndpointBaseTest.csto 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; // ... } }TimeProvider.System.GetUtcNow()calls withTimeProvider.GetUtcNow()in test files to use the instance field.Checklist