#40 Inject TimeProvider - #46
Merged
Merged
Conversation
Signed-off-by: Matthew DeVenny <matt@codecargo.com>
Extract the duplicated relative-expiration resolution shared by GetTtl and CreateCacheEntry into a private ResolveAbsoluteExpiration helper so the clock-source logic lives in one place. Slim AddNatsCache_UsesRegisteredTimeProvider to assert that the registered provider drives the computed expiration, instead of re-testing the Advance->expire flip already covered by TimeProviderExpirationUnitTests. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Matthew DeVenny <matt@codecargo.com>
…usive Rename IsExpired -> IsAbsolutelyExpired to make clear it only checks absolute expiration (sliding expiration is enforced via the NATS entry TTL), and tighten the comparison from > to >= so the absolute-expiration instant is treated as elapsed. This matches GetTtl, which already treats an absolute expiration at "now" as expired, and prevents the sliding refresh path from extending an entry past its absolute expiration at the exact boundary tick. Add AbsoluteExpirationIsExpiredAtExactInstant to lock in the inclusive boundary semantics. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Matthew DeVenny <matt@codecargo.com>
GetTtlComputesRelativeExpirationFromProviderTime -> GetTtlReturnsConfiguredRelativeExpiration. A relative expiration produces a TTL equal to the configured duration regardless of the clock, so the old name overpromised provider-time dependence. Signed-off-by: Matthew DeVenny <matt@codecargo.com> Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
matthewdevenny
marked this pull request as ready for review
June 30, 2026 20:39
Keep the NatsCache constructor at its original 4-parameter signature (no ABI break) and expose the clock as an internal init-only TimeProvider property. The DI registration resolves an optional TimeProvider from the container and sets it via object initializer, defaulting to TimeProvider.System when none is registered. Override the clock by registering a TimeProvider in DI (documented in the README); external consumers no longer set it through the ctor. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Signed-off-by: Matthew DeVenny <matt@codecargo.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #40
Problem
NatsCacheusedDateTimeOffset.Nowthroughout its expiration logic (TTL computation, entry creation, absolute-expiration checks, sliding-window updates). That relies on the local wall clock and makes expiration behavior impossible to unit-test without real delays.Change
Route every clock read through an injected
TimeProvider(defaultTimeProvider.System), resolved from DI. This is the .NET 8+ idiom and enables deterministic, instant expiration unit tests viaFakeTimeProvider.Library (
src/NatsDistributedCache)NatsCache.cs: replaced allDateTimeOffset.Nowusages withTimeProvider.GetUtcNow(). The clock is exposed as aninternalinit-onlyTimeProviderproperty (defaultTimeProvider.System) rather than a constructor parameter, so the public constructor is unchanged and there is no ABI break. The absolute-expiration read check was extracted intoIsAbsolutelyExpired, and the shared relative-vs-absolute resolution intoResolveAbsoluteExpiration.NatsDistributedCacheExtensions.cs: the DI registration resolves an optionalTimeProviderviasp.GetService<TimeProvider>()and sets the property, falling back toTimeProvider.Systemwhen none is registered.README.md: documents overriding the clock by registering aTimeProviderin DI.Switching from local-time
DateTimeOffset.Nowto UTCGetUtcNow()is behavior-preserving: every use is either an instant comparison or a duration, andDateTimeOffsetoperations are offset-aware. The one deliberate behavior change is thatIsAbsolutelyExpireduses an inclusive>=boundary, matchingGetTtl(which already treats an absolute expiration at "now" as elapsed) and BCLMemoryCache.Tests (
test/UnitTests)Microsoft.Extensions.TimeProvider.Testingpackage and regenerated the RID lock files.TestBaseinjects aFakeTimeProvider(exposed as a property) via the init property and exposes the concreteNatsCache.TimeProvider.GetUtcNow().TimeProviderExpirationUnitTests: instant, no-delay tests for TTL computation,CreateCacheEntry, and absolute expiration flipping withFakeTimeProvider.Advance(including the exact-boundary case).TimeProvideris used, and that resolution falls back toTimeProvider.System(verified against the real UTC clock) when none is registered.The integration time-expiration tests (
TimeExpirationTests/TimeExpirationAsyncTests) are intentionally left with real delays — they exercise NATS's server-side TTL, whose clock aFakeTimeProvidercan't drive.Acceptance criteria
DateTimeOffset.Nowremains in the library.FakeTimeProvider(no real delays).TimeProvider, defaulting toTimeProvider.System.Note on API surface
Injecting via the
internalinit property (instead of a constructor parameter) keeps the change purely additive — consumers compiled against 0.3.0 keep working without recompiling. The idiomatic way to override the clock is to register aTimeProviderin DI; consumers resolveIDistributedCache, so no public surface on the concreteNatsCachetype is required.Verification
dotnet build -p TreatWarningsAsErrors=true→ 0 warnings (net8.0 + net10.0); the public constructor is byte-identical tomain.dotnet test test/UnitTests→ 46/46 passing on both frameworks.🤖 Generated with Claude Code