Closed YohDeadfall closed 1 year ago
The vast majority of cases should be covered by current time + current timezone;
using System;
public interface IClock
{
public abstract DateTimeOffset UtcNow {get;}
// I'm explicitly including this because otherwise it usually results in relying on the implicit zone from the culture.
// This makes dealing with "current local time" much easier in server scenarios handling multiple cultures/users.
public abstract TimeZoneInfo Zone {get;}
}
... for some additional cases it may be worth considering system/process time...
public abstract long TickCount64 {get;}
(this one in theory enables at least some timer-related uses to be based on this, but that is non-trivial and quite likely to break something)
@geeknoid
Replacing just the "get time" function is completely insufficient to ensure robust tests. Code does things like waiting for tasks to complete with a timeout. The timeout that is used follows wall-clock time so it needs to be virtualized.
One issue with code that is waiting on Task's is that there are many Task's created and awaited in applications, and I see these as being more part of the execution environment as opposed directly related with a clock. A Task.Delay
seems special in your case because it sounds like you need to take control of it because its linked with time, and you want to control time. One way to control time is (flux capacitor!) to do both of the following:-
Both these are possible today by refactoring code for testability, but I think this issue is about promoting something that will help solve these issues in a common way, so refactoring isn't going to be encountered as often - via some agreed standards / interfaces etc.
With that in mind, I'm imagining a kind of Delay
task factory, so a bit like what we are proposing here with IClock
but the guidance would be if you want to do Task.Delay
we'd warn against that and instead inject the IDelayFactory
(or whatever it's called) and use that to delay - this would support mocking so you could return task's that you control at test time.
As a more of a pie in the sky runtime idea - I was also imagining a runtime level feature where you could switch something on with the runtime, that meant that every Task
that was running in the environment became accessible via some environmental service that could be used during testing, so that there were ways to access specific tasks's and trigger them to return results prematurely - not sure whether such a thing is really desirable or possible though ;-) - it could be pretty chaotic.
There are many ways .NET delivers functionality that derives from wall-clock time: getting the time, waiting for time, various timer abstractions, various timeout semantics, etc. I want to control all of these through a single common clock, where I control the passage of time within my test process.
Aside from I/O completion, the forward movement of time is the other primary source of non-determinism in a process. The key to reliable and systematic testing is to eliminate non-determinism in your tests. We use mocks and fake services to eliminate non-determinism in I/O, but there currently isn't a mechanism to eliminate non-determinism introduced by the passage of time. This makes it considerably more difficult to write tests that are reliable and that provide 100% test coverage (since hitting some time-related edge cases in your code can be nearly impossible).
So what I'm really after here is an effective way to test, given the non-determinism of wall-clock time.
I want to control all of these through a single common clock
You can't, because as was previously mentioned some of the timeouts get pushed below the runtime, into the OS/hardware. And even some of those that refer back to runtime-space clocks for related behaviour can still be relying on external functionality for the timing.
Further, without completely rearchitecting significant portions of the runtime it's likely that this would introduce hidden implementation dependencies on the clock; you'd have to know what methods were being called on the clock to be able to control them, which makes it brittle.
Generally, I think people generally get around this in tests by making the timeouts configurable (and setting a low value), or pushing the timeouts into whatever other part is being mocked/injected, which generally also simplifies testing as well.
What about doing the following:
public interface IClock
{
DateTimeOffset UtcNow { get; }
// We can think adding Time Zone or related thing we think is important enough for retrieving the time from the clock
}
public interface IStoppableClock : IClock // We can decide better name than IStoppableClock so I am not discussing naming for now :-)
{
// Add here all interfaces for handling timers, periods...etc.
}
The benefits are:
If the main idea here is reasonable, we can start digging the details as needed.
What about doing the following:
public interface IClock { DateTimeOffset UtcNow { get; } // We can think adding Time Zone or related thing we think is important enough for retrieving the time from the clock } public interface IStoppableClock : IClock // We can decide better name than IStoppableClock so I am not discussing naming for now :-) { // Add here all interfaces for handling timers, periods...etc. }
The benefits are:
- IClock is still simple and satisfies most of the scenarios needed today. Users can easily implement that.
- IStoppableClock can satisfy more specialized clocks which can be used for other needed scenarios.
- IStoppableClock can still be used in simple scenarios as it is implementing IClock.
- Concrete implantation can be provided for common cases (like SystemClock, SystemStoppableClock, CustomizedStoppableClock ...etc).
If the main idea here is reasonable, we can start digging the details as needed.
@tarekgh
I'd rather see something like this without the interface inheritance:
public interface IClock
{
DateTimeOffset UtcNow { get; }
}
public interface IAlarm
{
// Add here all interfaces for handling timers, periods...etc.
}
And then a concrete implementation:
public class AlarmClock : IClock, IAlarm
@tarekgh - I'd agree in general, but just wanted to add that it may then be better to then split the new interface up into multiple seperate ones depending on the api's it's intended to house - for example, Task
related stuff (such as delay's) vs Timer related (there are different timers in different namespaces etc). This could form a set of services and you'd need to use the correct one/s for your scenarios. This may end up quite granular but I think it would be factored better to align with dependencies.
For what it's worth, I don't expect this to pass API Review if proposed as interfaces.
public class Clock
{
public virtual DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
...
}
is more likely (though we'd almost certainly want a the class name to have two words as "Clock" is a bit of an over-reach)... so I'll throw out TimeProvider
as a strawman).
@julealgon
I'd rather see something like this without the interface inheritance:
I want to have the fact that IAlarm
depends on IClock in general and not just the concrete implementation. Not having such a relationship in the interface can cause some of IAlram
APIs need to have IClock
parameters I guess.
@dazinator
then split the new interface up into multiple seperate ones depending on the api's it's intended to house
This is helpful feedback. At the same time, I want to reduce exposure to many interfaces which are mostly related to each other and can cause some complications. So, we can look carefully at which functionality can fit in the proposed interfaces and which can fit in some other places or new interfaces. IMHO using Task stuff inside the timer interface is still ok as I am viewing Task as a concept/type which can be used anywhere and not necessarily be part of Task special types.
@bartonjs
Thanks for the design tips we need to consider when we start working on the actual proposal.
A few thoughts:
I think it's important that the interface only define what is needed from the consuming code, not what a potential implementation might use. Whether a clock is stoppable, will auto-advance when read from, or is just frozen - those are behaviors of the concrete class that will implement the interface, not part of the interface design.
I'm not sure what members would be on IAlarm
I do think that we should make getting the current time a method, not a property. Having it be a property could lead to things like an auto-advancing clock implementation doing the advancing while debugging and showing the output in a watch window or tooltip.
I agree that mocking the local time zone could be useful. I would expect that on a separate interface, but not necessarily on a separate concrete type. I also agree that avoiding interface inheritance is probably a good move.
Naming-wise, I can see that IClock
and ISystemClock
both might be confusing with respect to time zone, and I like @bartonjs's naming idea.
Thus, I propose:
public interface IUtcTimeProvider
{
public DateTimeOffset GetUtcTime();
}
public interface ILocalTimeProvider
{
public TimeZoneInfo TimeZone { get; }
public DateTimeOffset GetLocalTime();
}
A possible concrete implementation for the real time would be:
public class SystemTimeProvider : ILocalTimeProvider, IUtcTimeProvider
{
public static SystemTimeProvider Instance { get; } = new();
private SystemTimeProvider() { }
public TimeZoneInfo TimeZone => TimeZoneInfo.Local;
public DateTimeOffset GetLocalTime() => DateTimeOffset.Now;
public DateTimeOffset GetUtcTime() => DateTimeOffset.UtcNow;
}
One possible concrete implementation to be used during testing, with behavior of only a single fixed point in time, would be:
public class FixedTimeProvider : ILocalTimeProvider, IUtcTimeProvider
{
private readonly DateTimeOffset _utcTime;
public FixedTimeProvider(DateTimeOffset time, TimeZoneInfo? timeZone = default)
{
_utcTime = time.ToUniversalTime();
TimeZone = timeZone ?? TimeZoneInfo.Local;
}
public TimeZoneInfo TimeZone { get; }
public DateTimeOffset GetLocalTime() => TimeZoneInfo.ConvertTime(_utcTime, TimeZone);
public DateTimeOffset GetUtcTime() => _utcTime;
}
This also helps separate applications that legitimately use local time (such as desktop or mobile apps) from application that should probably only use UTC. For example, an ASP.NET Core web controller might inject IUtcTimeProvider
into their controller, where a MAUI app might inject an ILocalTimeProvider
.
@mattjohnsonpint
I'm not sure what members would be on IAlarm
Most of the functionality requested in this comment https://github.com/dotnet/runtime/issues/36617#issuecomment-1302734728. Naming is a different issue, so we don't have to stick with Alram
if you think it is confusing.
I agree that mocking the local time zone could be useful. I would expect that on a separate interface
What is the benefit of splitting that? If someone exposes a new API that wants to have it to work with time, it will not be good to have this API taking 2-time provider parameters instead of just one. I expect the API will take the interfaces and not use the concrete types.
I think @bartonjs was pointing to not use interfaces and instead use class with virtual members. We can clarify that.
I think @bartonjs was pointing to not use interfaces and instead use class with virtual members. We can clarify that.
Correct.
Several quotes from Framework Design Guidelines, 3rd edition:
4.3 Choosing Between Class and Interface
In general, classes are the preferred construct for exposing abstractions. ...
DO favor defining classes over interfaces.
DO use abstract classes instead of interfaces to decouple the contract from implementations.
RICO MARIANI: Good interface candidates often have this “mix in” feel to them. All sorts of objects can be
IFormattable
—it isn’t restricted to a certain subtype. It’s more like a type attribute. Other times we have interfaces that look more like they should be classes—IFormatProvider
springs to mind. The fact that the interface’s name ended in "er" speaks volumes.JEREMY BARTON: One hint that Jeffrey’s "can-do" paradigm is being followed is when the interface ends in the suffix "able." For example,
IDisposable
can be loosely rewritten "a thing that can do Dispose." If we compare this toICryptoTransform
, that interface isn’t describing a capability; it’s instead defining a set of related actions. When we addedSystem.Span<T>
in .NET Core 2.0, we found that we couldn’t add Span support to symmetric cryptography because we used an interface instead of an abstract base class. It may have taken 15 years to realize that it was a mistake to have that type as an interface, but it was nonetheless a mistake.
Since it doesn't make sense as IClockable
, it shouldn't be an interface; but rather designed as an unsealed class with virtual members.
Ok, I understand now. How about something like this then?
public class TimeProvider
{
public static TimeProvider Default = new();
public TimeProvider(TimeZoneInfo? timeZone = default)
{
TimeZone = timeZone ?? TimeZoneInfo.Local;
}
public TimeProvider(string timeZoneId)
{
TimeZone = TimeZoneInfo.FindSystemTimeZoneById(timeZoneId);
}
public TimeZoneInfo TimeZone { get; init; }
public DateTimeOffset GetLocalTime() => TimeZoneInfo.ConvertTime(GetUtcTime(), TimeZone);
public virtual DateTimeOffset GetUtcTime() => DateTimeOffset.UtcNow;
}
A possible fixed-time implementation, overriding the virtual GetUtcTime
method:
public class FixedTimeProvider : TimeProvider
{
private readonly DateTimeOffset _utcTime;
public FixedTimeProvider(DateTimeOffset time, TimeZoneInfo? timeZone = default)
: base(timeZone)
{
_utcTime = time.ToUniversalTime();
}
public FixedTimeProvider(DateTimeOffset time, string timeZoneId)
: base(timeZoneId)
{
_utcTime = time.ToUniversalTime();
}
public override DateTimeOffset GetUtcTime() => _utcTime;
}
@mattjohnsonpint this going same direction I am proposing. We can talk about the details when we create the full proposal as we are missing the TimerProvider
or whatever we'll call it.
If there is to be a discussion about TimerProvider
(or whatever it will be called), can it please move to another proposal? We've already said a few times that they are separate things. I don't want to wait for a whole other debate for something that has already taken 2.5 years to get this far.
If there is to be a discussion about TimerProvider (or whatever it will be called), can it please move to another proposal? We've already said a few times that they are separate things
What is "they"? IClock
vs. TimeProvider
? They're the same thing. interface IClock
isn't consistent with the design guidelines for the base class layer (as quoted), class TimeProvider
is the shape that it would need to make progress. (Maybe I'm wrong, and everyone else in the review group would think an interface is natural and correct here; but to me it feels pretty solidly against our design guidelines)
By "they", I meant TimeProvider
vs TimerProvider
. This proposal being about an API to provide the current system time, abstracting DateTime.UtcNow
and the like. A proposal for abstracting Task.Delay
and other timers shouldn't creep in to this proposal, IMHO.
Ah, I missed the r
in Timer
, sorry. (I choose to blame it on learning to "ignore" adjectival declension suffixes from German. And now all Deutschsprachen Deutschsprachige can roll their eyes at me.)
All good.
Actually, the framework guidance about the "able" suffix is quite useful here, and I think bifurcates the two nicely. One wouldn't say that a clock is "clockable", but one might say that a task is "delayable". So perhaps TimeProvider
for this proposal, and something like IDelayable
for a separate proposal?
and other timers shouldn't creep in to this proposal, IMHO.
@mattjohnsonpint as @stephentoub mentioned in the comment https://github.com/dotnet/runtime/issues/36617#issuecomment-1307558843 both abstracts we want to have been related and it is good to design them together. No worries, we are doing a good progress here and we'll keep it up :-)
I'm trying to wrap my head around all the proposals. It sounds like step one is coming up with "what concepts are we even trying to encode here" (and we'll worry about shape later). It feels like there are... 4? big concepts I think:
Am I missing something or misunderstanding the high level concepts?
@ChadNedzlek you have captured the high-level concepts correctly. The third one we didn't discuss it though. We need to decide if it is required here.
What I was trying to get into is to have an agreement to split the abstractions so time provider (previously we were calling it IClock), should be simple and provide Utc and local time only. This one covers the first two concepts you have. Then the fourth concept will be in its own abstractions.
No methods for return current time as TimeOnly and current date as DateOnly (yeah, it's not "time")?
No methods for return current time as TimeOnly and current date as DateOnly (yeah, it's not "time")?
No. We don't expose any API to return current date/time as TimeOnly or DateOnly. This is intentional. These types are not tied to specific time zones to report system time.
Tagging subscribers to this area: @dotnet/area-extensions-primitives See info in area-owners.md if you want to be subscribed.
Author: | YohDeadfall |
---|---|
Assignees: | - |
Labels: | `api-needs-work`, `area-Extensions-Primitives`, `partner-impact` |
Milestone: | 8.0.0 |
I have made changes to the proposal at the top and added comments to explain the reasoning behind this design.
CC @stephentoub @geeknoid @davidfowl @joperezr @ericstj
Having tried to build a similar API for a proprietary system, I find WaitAsync(Task task, TimeSpan timeout, CancellationToken cancellationToken)
quite intriguing.
I would have thought that CancellationTokenSource
+ CancelAfter
would be the ideal way to introduce a timeout to APIs that already take a CancellationToken
, but that loses the ability to mock time in testing, such as by speeding it up or skipping the delay entirely.
So therefore, logically the solution is to double-up and have two ways to cancel the wait, one explicitly via timeout and one as a general-purpose cancellation token.
However I can see that you've also proposed void CancelAfter(CancellationTokenSource source, TimeSpan delay)
(genius, why didn't I think of that?!). IMO this solves the first problem entirely and would allow for WaitAsync(Task task, CancellationToken cancellationToken)
to be usable on it's own.
Is there another reason for WaitAsync
to take TimeSpan timeout
, or is it kind of redundant when the rest of the API surface is used? As far as I can tell, it could become an extension method on TimeProvider
.
Is there another reason for
WaitAsync
to takeTimeSpan
timeout, or is it kind of redundant when the rest of the API surface is used? As far as I can tell, it could become an extension method onTimeProvider
.
It would be for situations where the method receives a CancellationToken
, which you want to forward, but also want to be able to cancel any async calls you make independently of the token being cancelled. The traditional way to do this is to get a linked source, this just makes it a bit easier. The pattern is relatively common for web apis (especially for things like functions-as-a-service, where you only write such a leaf method, and never see the token source).
@tarekgh -
nit: I would prefer to call out the source of the time for the default provider, eg TimeProvider.System
, instead of just labeling it "default".
Are we also going to be including analyzers to nudge people away from referencing TimeProvider.Default
directly, and DateTime(Offset).(Utc)Now
?
I agree the name System
is much more preferable over Default
. Some other thoughts:
- To allow for potential future extensions, TimeProvider has been designed as a class rather than an interface. This design decision allows for virtual methods and properties to be easily overridden for testing or other purposes.
I can get behind that, but would it perhaps make sense to make it an abstract class? (Currently it's simply non-sealed.) Just thinking out loud, but TimeProvider
by itself just sounds more like an abstract type rather than something to be new
ed up arbitrarily. (I don't believe there's a philosophical requirement that abstract members need to exist for the class to be abstract.)
- The TimeProvider will be implemented in the Microsoft.Extensions.Primitives library.
I'm assuming that's for the down-level support case, but is there no expectation that this might want to be consumed in the runtime in the future?
public virtual DateTimeOffset UtcNow { get; }
Does this type really need to keep using the same (arguably mistaken) shape as DateTime{Offset}
, or should these (finally) be made methods instead of properties?
@tarekgh
I have made changes to the proposal at the top and added comments to explain the reasoning behind this design.
The name TimeProvider
to me now seems slightly inadequate/potentially misleading. It would fit when we were just getting the current time, but as the API surface has been expanded, I think the name should be reconsidered.
Perhaps a broader name such as TimeManager
or TimeHandler
(or something else)?
It just feels to me that "TimeProvider" == "gives you the time", which is too limiting for the current functionality.
@Joe4evr
Does this type really need to keep using the same (arguably mistaken) shape as DateTime{Offset}, or should these (finally) be made methods instead of properties?
Are you saying that because of the dynamic nature of the value, meaning it would go against best practices for properties which should be (mostly) fixed values and/or zero-computation-cost accessors? Or is there more to it?
Also, what would the name be for it if it was a method? GetUtcNow
? Or perhaps something different like GetCurrentTimeUtc
(or GetCurrentDateTimeUtc
)?
/// <summary>
/// Get current ticks count and ticks' frequency per second which can be used to measure elapsed time.
/// </summary>
/// <returns></returns>
public virtual (long ticks, long frequency) Timestamp { get; }
I can't get to like this one due to returning a tuple :)
.NET Framework does not define a Task.WaitAsync method but the proposed API includes an equivalent. That can be useful even if the application does not care about wall-clock time at all. I don't know whether a downlevel implementation is being shipped in any other supported package. .NET Runtime already has such an implementation in test code, though:
That implementation does not propagate the canceled CancellationToken to the resulting Task. TaskCompletionSource\<TResult>.TrySetCanceled(CancellationToken) is part of .NET Standard 2.0 so this should be fixable.
Also, I suspect those await
expressions keep only one inner exception of Task.Exception.
/// <summary>
/// Retrieve the current Utc time
/// </summary>
/// <returns>DateTimeOffset object representing the current UTC time</returns>
public virtual DateTimeOffset UtcNow { get; }
/// <summary>
/// Retrieve the current local time
/// </summary>
/// <returns>DateTimeOffset object representing the current local time</returns>
public virtual DateTimeOffset LocalNow { get; }
/// <summary>
/// time zone info object to report the local time with
/// </summary>
public virtual TimeZoneInfo TimeZone { get; }
This design allows one to implement UtcNow
and LocalNow
such that they return entirely different points in time. It also requires implementers of LocalNow
to correctly apply the TimeZone
.
I suggest LocalNow
not be marked virtual
. Its implementation should only exist in TimeProvider
, not in derived class. Its implementation should simply be:
public DateTimeOffset LocalNow => TimeZoneInfo.ConvertTime(UtcNow, TimeZone);
@yaakov-h
Is there another reason for WaitAsync to take TimeSpan timeout, or is it kind of redundant when the rest of the API surface is used? As far as I can tell, it could become an extension method on TimeProvider.
@Clockwork-Muse answer is correct regarding that.
@Clockwork-Muse
nit: I would prefer to call out the source of the time for the default provider, eg TimeProvider.System, instead of just labeling it "default".
It is a good suggestion to me.
Are we also going to be including analyzers to nudge people away from referencing TimeProvider.Default directly, and DateTime(Offset).(Utc)Now?
In general, this is a good idea, and it is related to @Joe4evr point if TimeProvider will be outside the core. If recommend using the TimeProvider.UtcNow that may require users to reference the Microsoft.Extensions library. I'll look more at this part first and we can track having the analyzer later.
@Joe4evr
I'm assuming that's for the down-level support case, but is there no expectation that this might want to be consumed in the runtime in the future?
Yes, I am still looking at this part evaluating the complication and the need inside the core.
I can get behind that, but would it perhaps make sense to make it an abstract class? (Currently it's simply non-sealed.) Just thinking out loud, but TimeProvider by itself just sounds more like an abstract type rather than something to be newed up arbitrarily. (I don't believe there's a philosophical requirement that abstract members need to exist for the class to be abstract.)
It can be an abstract class and instead of constructors we'll have factory methods. internal implementation will need to add extra internal class to create the System instance. Initially I thought to have it abstract and then I found it would be more convenient if it is not abstract. I'll think more about it.
Does this type really need to keep using the same (arguably mistaken) shape as DateTime{Offset}, or should these (finally) be made methods instead of properties?
I thought about this for a long time and debating between having it as a method and having some consistency with the existing type that is used a lot today. I am inclining to consistency with one slight change to have LocalNow
instead of Now
for more clarity. Anyway, this can be discussed in design review.
@julealgon
The name TimeProvider to me now seems slightly inadequate/potentially misleading.
I'll add a comment on the proposal for suggested other names like TimeManager
or TimeHandler
or other names. Personally, I am still like TimeProvider
but we can discuss the naming during the design review.
@ilmax
I can't get to like this one due to returning a tuple :)
Can you explain why?
@KalleOlaviNiemitalo
I don't know whether a downlevel implementation is being shipped in any other supported package.
No, it is not shipped in other packages you may look at https://apisof.net/catalog/7d2ef1a2-1113-dda8-37cd-1061a7e4b3f5.
@mattjohnsonpint
I suggest LocalNow not be marked virtual. Its implementation should only exist in TimeProvider, not in derived class. Its implementation should simply be:
Do you think there is no test scenario needed to mock LocalNew even with having time zone and Utc time are available? I marked it as virtual for more flexibility. Whoever subclasses this can choose not to override if need the normal behavior but can choose to override it if they need to support more special behavior. I can make up one scenario is to create a time provider object which tests with multiple time zones in same time. return local time according to some condition inside the time provider. what do you think?
Do you think there is no test scenario needed to mock LocalNow even with having time zone and Utc time are available?
To mock the local time, one implements the mock for the UTC time and the time zone. The local time is thus provided through conversion.
I can make up one scenario is to create a time provider object which tests with multiple time zones in same time.
I think that scenario should require multiple time providers. There should be a consistent view of time within a single instance of a time provider.
Similarly, it feels strange to me that WaitAsync
and WaitAsync<T>
would have to be mocked separately. Shouldn't there be one thing to implement that would would affect them both?
For LocalNew I'll change it to be non-virtual for now and watch in the future if needed to be virtual.
Similarly, it feels strange to me that WaitAsync and WaitAsync
would have to be mocked separately. Shouldn't there be one thing to implement that would would affect them both?
We can have only one that takes Task
but this will require users of Task
Task<T> task = ...;
await provider.WaitAsync(task, ...);
T result = task.Result; // or await task.Result
What's the behaviour of new TimeProvider(null)
? Will the TimeZoneInfo TimeZone
getter of the resulting instance read TimeZoneInfo.Local
on each call so that the result is usable even if TimeZoneInfo. ClearCachedData()
has been called in the meantime?
What's the behaviour of new TimeProvider(null)? Will the TimeZoneInfo TimeZone getter of the resulting instance read TimeZoneInfo.Local on each call so that the result is usable even if TimeZoneInfo. ClearCachedData() has been called in the meantime?
Yes, it will always use TimeZoneInfo.Local when getting the local time. This will be the same behavior of TimeProvider.Default.
Yes, it will always use TimeZoneInfo.Local when getting the local time. This will be the same behavior of TimeProvider.Default.
Wait, is that getting the "live" view of the local timezone, or a frozen one?
I think that scenario should require multiple time providers. There should be a consistent view of time within a single instance of a time provider.
Or a single utc-only time provider, because in most real-world instances I'm thinking of the called method will be doing conversion of the destination timezones itself, based on passed parameters.
Wait, is that getting the "live" view of the local timezone, or a frozen one?
will get the live one. If you want to get fixed time zone behavior can just call the constructor which take the TimeZoneInfo and pass TimeZoneInfo.Local if needed.
@mattjohnsonpint I got scenarios for LocalNow that may need allow overriding it. Trying to report local time that is greater than 14 hours from UTC (for testing purpose). Currently the max TZ offsets cannot exceed 14 hours. Also, we cannot have tZ offset with a fraction of minutes. I know users may try to work around it in other ways, but it will be more involving. What do you think about that?
will get the live one. If you want to get fixed time zone behavior can just call the constructor which take the TimeZoneInfo and pass TimeZoneInfo.Local if needed.
This difference will need to be explicitly called out in documentation (although practically the difference will be rare).
What do you think about that?
Users would have to create their own timezone-related classes to handle such cases in the first place; at that point the rest of the effort is small potatoes.
Trying to report local time that is greater than 14 hours from UTC (for testing purpose).
Testing what exactly? I don't see how overriding LocalNow
would let you get around any of the invariants on TimeZoneInfo
or DateTimeOffset
.
If you're talking testing the internals of DateTimeOffset
or TimeZoneInfo
itself - I think that should be out of scope for this API.
Just also want to add, I don't like the revised API. I suggested previously:
So perhaps
TimeProvider
for this proposal, and something likeIDelayable
for a separate proposal?
And @tarekgh, you had said
@mattjohnsonpint as @stephentoub mentioned in the comment https://github.com/dotnet/runtime/issues/36617#issuecomment-1307558843 both abstracts we want to have been related and it is good to design them together.
That link to @stephentoub's comments say:
I'm suggesting this issue needs to account for both. I don't want to design one abstraction in isolation from the other. If it ends up with two abstractions, fine. If it ends up with one abstraction, fine. But they're highly related and should be designed together.
I had taken away from this that we would be looking at a TimeProvider
and IDelayable
together in this API design proposal, but not that we would smoosh them into a single class. The main issue I have is that it violates Single-Responsibility Principle. Mocking the current DateTime
has nothing to do with mocking how the passage of time is measured or interpreted by delayable functions.
They need to be split into two abstractions.
They need to be split into two abstractions.
They obviously don't need to be. You want them to be. Why is that critical? What can't you do with it as proposed? What code you have to write ends up being objectively worse?
You're right, it probably doesn't need to be split. It just feels to me like it should. Sorry for my poor word choice.
Still, I'm struggling to come with a scenario that would benefit from them being together. It feels a bit like a generic utility class, getting the kitchen sink of all things time related.
Mocking the current
DateTime
has nothing to do with mocking how the passage of time is measured or interpreted by delayable functions.
I would strongly disagree here. If I want to accelerate or slow down the passage of time, or skip time completely, I would need to mock out both the current time as well as anything that relies on time elapsing.
The currently proposed design has both in one class which means there's only one object responsible for time. Splitting them means I need up to two objects which have to be kept in sync - one for "what is the time" and one for "wait for a period of time".
(Granted, at the moment you would need to keep all the methods in sync, but I don't see that as much of a problem.)
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.