-
Notifications
You must be signed in to change notification settings - Fork 4.8k
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
API to provide the current system time #36617
Comments
Actually I do not think it is needed we already have Also what is the reasoning for Edit: after further reading I understand now about the places that depend on it. |
It's impossible to mock statics in case of tests. The last but not least is that DI means that there should be no statics and ability to replace any implementation by what you want, different and incompatible patterns to be clear. |
Probably the main reason that interface exists internally in 4 separate Microsoft libraries is because MS doesn't want their building blocks to take dependencies on third-party packages, if they can at all avoid it. For any project outside of MS, it's more recommended to use NodaTime if you need to do any serious date/time math or just want semantically better date/time types to work with (including |
It's super important for testing. Mocking things that are non deterministic is important for testing things like timeouts. |
I'd probably suggest a Edit: The singleton would look like: public static sealed SystemClock : ISystemClock
{
private SystemClock();
public static ISystemClock Instance { get; }
public DateTimeOffset UtcNow { get; }
} In theory this would also allow the JIT to devirtualize |
@davidfowl what's your recommendation here, should we expose this in |
System.* namespace it feels that core |
We have plenty of API it would be good to mock, but is either sealed or static. Example: cc @bartonjs do we have design guidelines? cc @stephentoub |
We either don't have guidelines here, or our soft stance is "mockability isn't important", whichever one feels better to you 😄. The guidelines we do have would say that there shouldn't be an public class SystemClock
{
public virtual DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
} If we wanted to aggressively support mockability, we'd leave it at that, but then someone would ask (perhaps not for this feature, since there's already DateTimeOffset.UtcNow, but let's keep playing the scenario out) why everyone has to create their own version of SystemClock to ask what the time is, because that's very GC inefficient. So we'd add a static Instance property: public class SystemClock
{
public static SystemClock Instance { get; } = new SystemClock();
public virtual DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
} And now we're back at being non-mockable, because "everything" is going to just call As a suggestion to bring back a semblance of mockability, someone might propose making the singleton property settable, or having some other set mechanism. Depending on who is present at the meeting, it'll either be a drawn out conversation about the value of such an API, or immediately shot down as us having learned our lessons about mutable statics. Also, we may decide to slap a "Provider" (or "Factory" or similar) suffix on the type name, since it's actually a system time factory, not a system time. It's a massive oversimplification, but the BCL is designed with declarative programming models, and mockability requires looser coupling with "magic"/ambient state, like a DI system. So, in the end, I'd say "perhaps this belongs alongside a DI technology", though that then makes a tight coupling between a consumer of the time and the DI layer. Then I'd say "maybe one level higher than that?" which is, perhaps, Microsoft.Extensions or Microsoft.Extensions.Primitives. (I'd then go on to suggest that mocking time seems weird, and that (as a declarative programmer) it seems like whatever things are pulling from DateTimeOffset.UtcNow just want to take the time as an input... except for things like logging, where you really want "now" (whatever that means) vs "mocked now"... how to do that in a DI system is, as we say in mathematics, an exercise left to the reader (which then leads to this proposal, and we go 'round and 'round)). I'm pretty sure I've said this before for a laugh, and I'll do so again: "Should I just say 'no' now and save us all 15 minutes? 😄" |
I confirmed offline with @davidfowl that he's in agreement with my thoughts above that attempting to add mockability one type at a time is unlikely to give a good result. This doesn't help any place where we ourselves use the API directly. If we do anything here it should start with thinking about mockability for the core libraries as a whole and coming up with guidelines or technology that doesn't lead to a proliferation of types and overloads and that we're confident would be a significant value. Meanwhile if we're looking for a type that the core libraries themselves don't depend on, any mocking library could provide it. |
While a single, re-usable definition of a "clock" would be useful, I feel the only main uses would be unit testing, which I suppose could be done using compile-time definitions, e.g.: using System;
using Another.Thing;
#if MOCK_TIME
using SystemClock = My.Custom.DateTimeOffset;
#else
using SystemClock = System.DateTimeOffset;
#endif
// etc. using SystemClock.UtcNow The only disadvantage to this would be that you're not strictly testing the same code you're about to publish |
@FiniteReality - If I was worried about a dependency on getting the current time, but was bound to only first-party libraries (that is, no NodaTime), I'd instead inject a Note that, while mocking the current system time is useful for testing, perhaps the bigger area is for making sure things are consistent with regards to a timezone (or here, offset). That is, rather than setting the current timezone in a context object so you can use |
@GrabYourPitchforks thoughts about my note above? Wondering whether this ought to be un-marked api-ready-for-review as several folks aren't sure this is a good direction for the base libraries. (Unless you want to use the tag to trigger a larger discussion in that group about mocking) |
@danmosemsft As manager, isn't it your privilege to say "Let's spin up a working group!"? :) I'd be supportive of this if we wanted to have a wider discussion regarding mockability within the BCL. Though honestly the Microsoft.Extensions.* libraries have become something of a dumping ground for these sorts of APIs, so I don't know how much discussion need take place. We still consider these libraries separate from / at arm's length from the BCL. |
We could expose a new type, but what does this mean for the existing packages that already defined their own version of |
Inside the Akka.NET project we have had our own virtual time abstraction ( The benefits I'd see us realizing from this API, were it to be introduced as currently proposed:
I can't tell you how much time I've personally wasted trying to fix these racy tests due to scheduling imprecision in our suite - hundreds of hours over the past ten years probably. Framework authors will appreciate this feature - others probably won't interact with it much. Please add it! |
@ploeh, thank you for the feedback and for your follow-up response. I appreciate it.
I would hope in time (no pun intended) you'd find use for it. But you can certainly ignore it if it's not something you need, and I don't foresee any adverse effects to doing so; nothing forces you to use it, e.g. no APIs in the core libraries will ever require you to supply one. It's entirely optional. |
@Aaronontheweb and @egil, it's great to hear this should suffice for your needs. Just to confirm, is there anything critical missing in the abstraction for your needs? Once it ships, it would be great if it could entirely replace the heart of those custom abstractions. |
After using
sealed class LocalTimeProvider(TimeZoneInfo local) : TimeProvider
{
public override LocalTimeZone => local;
}
What’s currently checked in is: namespace System
{
public abstract class TimeProvider
{
public static TimeProvider System { get; }
public static TimeProvider FromLocalTimeZone(TimeZoneInfo timeZone);
protected TimeProvider(long timestampFrequency);
public abstract DateTimeOffset UtcNow { get; }
public DateTimeOffset LocalNow { get; }
public abstract TimeZoneInfo LocalTimeZone { get; }
public long TimestampFrequency { get; }
public abstract long GetTimestamp();
public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp);
public abstract ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period);
}
} If we adopt all that, it would instead be massaged to be: namespace System
{
public abstract class TimeProvider
{
public static TimeProvider System { get; }
protected TimeProvider();
public virtual DateTimeOffset GetUtcNow();
public virtual long GetTimestamp();
public virtual long TimestampFrequency { get; } // or `public long TimestampFrequency { get; }` and `protected virtual long TimestampFrequencyCore { get; }`
public virtual ITimer CreateTimer(TimerCallback callback, object? state, TimeSpan dueTime, TimeSpan period);
public virtual TimeZoneInfo LocalTimeZone { get; }
public DateTimeOffset GetLocalNow();
public TimeSpan GetElapsedTime(long startingTimestamp, long endingTimestamp);
}
} |
Best way for me to express that is with an example - this is one of the aforementioned racy specs in our suite: public void KeepAlive_must_emit_elements_periodically_after_silent_periods()
{
this.AssertAllStagesStopped(() =>
{
var sourceWithIdleGap = Source.Combine(Source.From(Enumerable.Range(1, 5)),
Source.From(Enumerable.Range(6, 5)).InitialDelay(TimeSpan.FromSeconds(2)),
i => new Merge<int, int>(i));
var result = sourceWithIdleGap
.KeepAlive(TimeSpan.FromSeconds(0.6), () => 0)
.Grouped(1000)
.RunWith(Sink.First<IEnumerable<int>>(), Materializer);
result.Wait(TimeSpan.FromSeconds(3)).Should().BeTrue();
result.Result.Should().BeEquivalentTo(
Enumerable.Range(1, 5).Concat(new[] {0, 0, 0}).Concat(Enumerable.Range(6, 5)));
}, Materializer); The code being tested behind the scenes relies on our scheduler operating on a fixed interval - checking for idleness in the stream and injecting "keep-alive" values when pauses are detected. With these new methods, would I be able to create my own TimeProvider abstraction that can do the following:
Thanks so much for your time @stephentoub |
Yes... but you shouldn't even need to create your own for that. Our intention is to ship a ManualTimeProvider (or FakeTimeProvider or choose some other suitable name) that lets you set/change/advance/etc. the current time, which will of course not only update what's returned from Utc/LocalNow but also invoke all the relevant timers that need to be invoked and the appropriate number of times (based on dueTime and period). I expect it's very similar to what your implementation already does. |
Music to my ears. Sounds perfect. |
FWIW, I'll probably end up publishing a nuget of a properly implemented The |
The suggested improvements are excellent, when I saw Nick Chapsas recent video, the constructor was his standout criticism and I think it was a fair one. In general there still seems to be a theme (as @CZEMacLeod has pointed out), the @stephentoub - is this something that is recognised? Would having an lighter interface and letting people chose the level of complexity they need not be a viable option? In an earlier comment you said you didn't want to subdivide the API method by method and this is a single abstraction to flow the concept of time, but it seems to me like there is a high-level and low-level split. |
@slang25 with the updated proposal in the comment #36617 (comment), could you explain what complexity you will have when using |
I have seen this come up a couple of times now:
Partially implementing/mocking dependencies is an anti-pattern that leads to fragile tests. Please do not promote this, or design around it as a testing practice. A simple example in this case would be code that initially uses This is not a hypothetical fear; other than too many high-level tests which are fragile and take too long to run, this is the pathological failure case of automated testing that I have observed as it has finally become standard practice in the last decade+. Instead of being the enabler for safe, rapid change that it is supposed to be, the test suite becomes one of the primary roadblocks to change. This is also the practical impact of the SRP conversations that have been discussed abstractly in this thread. It's not just about "are all of these things related to the same concept" or "does the implementation of these things need to change together"; it's also about "are these things generally used together, and if not what impact does that have on the consumer who has to care about things they don't need". It has been asserted that consumers can simply ignore what they don't need; but it's often not that simple. The more pieces a dependency has, the harder it is to implement, which means the harder it is to fake; and the more likely a consumer is to bail out to partial mocking as a shortcut (especially if they have not yet been burned by it and don't know to avoid id), setting themselves up for pain in the future. Having a standard Overall, I am happy the problem of testing time is being addressed, and |
It's not the usage of it that's the issue, some will say "I dot into it and see a bunch of things I'm not looking for", well yes you can just ignore it. The problem is the concept count and perceived complexity, let's say I'm defining some domain logic that checks if something has expired, I wan't my domain to be as clear as possible. Taking an Saying one size fits all would work, for sure, but I think this is where the community frustration is stemming from, there are different contexts requiring different levels of abstraction. A high-level and a low-level. If this is fundamentally problematic then fine, but it seems to me that it solves a lot of the community feedback so far. |
I just wanted to share a SystemClock and tests that we've used and battle tested in case it helps. https://github.com/FoundatioFx/Foundatio/blob/master/src/Foundatio/Utility/SystemClock.cs |
My use cases thus far are covered. I do think it would be better to have two protected constructors for |
I'm astonished that I've only just become aware of this issue - I'm really pleased to see it being discussed. Consistent abstractionsOne point from earlier, by @aredfox:
I'm not sure I agree with that. I'd absolutely expect any interface/abstract class for this feature to express itself in BCL types - What I'm more likely to do is create a parallel interface/abstract class to whatever the BCL provides, which exposes the same functionality in Noda Time concepts, along with adapters between the two... assuming that's possible. I'd really like Noda Time users to be able to write code that:
Currently users who only need "current instant" functionality can do that with Of course this assumes that Noda Time will still be required in the future. One little idea is that if .NET is going to get all of this new functionality, maybe it would be a good time to introduce a Clocks and zonesOne other minor correction to a comment from @Clockwork-Muse:
JSR-310 does (Clock.getZone() but Noda Time doesn't. Our |
My bad.
... and now I'm rethinking one of my earlier comments around the |
Forgive my naivete, but what is stopping us from just adding NodaTime's abstractions and classes to the new |
Due diligence, for starters :) I would have absolutely no problem with .NET making Noda Time effectively obsolete... and hopefully doing a better job of it in some cases! (There are i18n aspects where I simply don't know enough to do the job fully.) But just like JSR-310 started with Joda Time, looked at it carefully and then came out with something clearly related but different, I'd expect the same to happen with a System.Time set of abstractions. |
Part of the problem is that that got debated and mostly shut down years ago: #14744 . We ended up with new date/time types, but no new namespace, and no real motion towards one either..... |
This proposal is edited by @tarekgh
Proposal
The aim of this proposal is to introduce time abstraction. This abstraction will include the ability to retrieve the system date and time, either in UTC or local time, as well as timestamps for use in performance or tagging scenarios. Additionally, this abstraction can be used in the Task operations like
WaitAsync
andCancellationTokenSource CancelAfter
.By introducing this new abstraction, it will become possible to replace other existing abstractions that are currently being used in a nonuniform way across various interfaces such as ISystemClock and ITimer. The following are some examples of such interfaces:
In addition, this new abstraction will enable the creation of tests that can mock time functionality, providing greater flexibility in testing through the ability to customize time operations.
Below are some design notes to consider:
CancellationTokenSource
constructor that works with abstraction.CancellationTokenSource
support the abstraction.APIs proposal
APIs for .NET 8.0 and Down-levels
APIs for .NET 8.0 Only
APIs for down-level Only
Possible APIs addition for .NET 8.0 and down-level
End of the @tarekgh edit
The Original Proposal
Motivation
The
ISystemClock
interface exists in:Microsoft.AspNetCore.Authentication
Microsoft.AspNetCore.ResponseCaching
Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure
Microsoft.Extensions.Internal
There is a small difference exists between implementations, but in most cases it can be moved out. In case of Kestrel's clock which has a specific logic inside, there could be made a new interface
IScopedSystemClock
which will provide the scope start time asUtcNow
does now. Therefore, looks like all of them could be merged into a single class/interface and put intoMicrosoft.Extensions.Primitives
.The same interface often implemented by developers themselves to be used by microservices and applications utilizing dependency injection.
Having a common implementation of the data provider pattern will free users from repeating the same simple code many times and will allow to test apps in conjunction with ASP.NET internals without changing an environment.
Proposed API
The
ISystemClock
defines a way to get the current time in UTC timezone, and it has a simple implementation:Originally proposed in dotnet/aspnetcore#16844.
The text was updated successfully, but these errors were encountered: