Closed YohDeadfall closed 1 year ago
Actually I do not think it is needed we already have DateTime.UtcNow
. However it could be useful for those who do not know about DateTime/ DateTimeOffset.
Also what is the reasoning for DateTimeOffset.UtcNow
over just DateTime.UtcNow
anyway?
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 IClock
that you're asking for).
It's super important for testing. Mocking things that are non deterministic is important for testing things like timeouts.
I'd probably suggest a SystemClock.Instance
singleton instead of forcing everybody to new up an instance of it. But the idea is sound on its face. Any objections to moving this forward to "ready for review"?
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 SystemClock.Instance.UtcNow
. Though not quite sure why you'd want to do that over DateTimeOffset.UtcNow
. :)
@davidfowl what's your recommendation here, should we expose this in Microsoft.Extensions
namespace in Microsoft.Extnsions.Primitives
System.* namespace it feels that core
We have plenty of API it would be good to mock, but is either sealed or static. Example: File
. It seems it wasn't a primary design criterion way back. Should we be thinking holistically about our attitude towards mockability, rather than thinking about just this one type? It might not need to be solved through API for example this proposal. Even if we want to enable it through API design, we should have a consistent pattern so it's discoverable, useable etc. I haven't kept up with the latest in mocking/DI - do we have established patterns?
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 :smile:.
The guidelines we do have would say that there shouldn't be an ISystemClock
interface, because it's representing a type more than a capability. (DO favor defining classes over interfaces.
... then some exceptions), and end up with
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 SystemClock.Instance.UtcNow
to get the time.
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? :smile:"
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 Func<System.DateTimeOffset>
for everything that was going to get the current time. Saves looking for weird redefinitions, and makes it really easy to deal with injection/forwarding.
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 System.DateTimeOffset.Now
, you pass in something to get it for you.
@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 ISystemClock
, like auth? Would we break them?
Since ISystemClock
is usually an internal type (which is forced to be public, IIRC) I don't think people would be too upset if there was a breaking change for 5.0 or even 6.0, since that's a major version bump.
If anyone does any sort of testing, they almost certainly used these types, even if they are internal; I know I did in several important projects. Though I'd accept the breaking change if it meant this thing would never break again, because it was really supported instead of tacked on to several different libraries.
I'd have to change code in a few different projects to deal with this, and it would be an annoying change that only breaks at runtime (like all DI changes), which is a really frustrating class of changes to have to deal with, because often we fail to notice before rolling the change out to production.
@bartonjs - I didn't comment on this earlier:
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.
... normally the way something like this would be used is as a singleton per application (Or, in a web context, cached singleton per resolved user timezone), and injected into all methods requiring "getting" current time (note that most methods that "use" current time should be instead accepting it as a parameter). Am I missing something about why this would be GC unfriendly? Or were you expecting all the relevant methods to be new
-ing up their own copy, or something?
@maryamariyan @davidfowl would we be OK with a breaking change to reconcile the ISystemClock
types into a single core type? If not, we shouldn't do this. If yes, then I think we can make progress.
@terrajobst, please fix the link. The right one is https://youtu.be/WW65sGkDINQ?t=2548 (starts at 4:28).
@terrajobst Yes I think we need to pick when to do it but we should have a core type that does this. I've now seen lots of examples of this and think it's required (especially for testing). There are likely other things to look at besides the UtcNow (like timers and TickCount etc)
Mention ISC in Reactive. Also I was on 5 projects, and each of these created their own...
https://github.com/search?l=C%23&q=org%3Adotnet+%22interface+ISystemClock%22&type=Code
UPDATE: Milestone 6 to add interface? 5 not released yet, not all will grab it fast. So 2+ years to simple shared interface.......
There are a few more time based APIs that are worth considering here that we hit in YARP as well:
When writing unit tests that try to use these APIs it's important to be able to provide fake implementations (our own time machine) rather than having to delay tests waiting for time to pass.
cc @Tratcher @alnikola
The main delays, we are observing currently, are caused by real Timer
s used in EntityActionSchedulerTests
. I filed an issue microsoft/reverse-proxy#508 to replace it with an abstract ITimer
interface.
To clarify the above. Having a clock abstraction built directly into BCL would be really convenient since testing a time dependent code is a common problem in many of systems.
On top of that, it would also be very handy to have a ITimerFactory
and ITimer
abstractions, too.
Hi. I've invested some effort exploring this in detail and would like to share my opinions and results.
First, I want to say that I understand that normally one doesn't want to spend time implementing a feature before it's gone through the full API design review process. However, in this case it seemed like much of the discussion has occurred already and the the best way to move forward was to actually implement it and see how it panned out. I have working and tested code that implements the revised proposal below. You can review it in #48681. I do not expect it to be merged at this time.
Let's go point by point:
It was suggested that mocking the clock is like mocking the file system, but IMHO these aren't quite the same things. After all, one can (without mocking) use temp directory, set up files before a test run, delete or modify files between tests, etc. Sure, the performance might be poor, but at least you can design for consistency (somewhat). One cannot do that with the current time or local time zone, both of which are essentially random during a test for all intent and purposes. Thus, this isn't about mockability, but rather it's about being able to have tests that guarantee application behavior when faced with the ever changing state of time. We can add this to .NET without having to solve mockability for everything.
In particular, there is tremendous value in being able to test code for leap year bugs well ahead of leap day and day-366 events. I led some of the efforts in that space at Microsoft recently, and I can tell you that having these capabilities built-in would have helped us significantly. I also believe many would benefit from being able to test their code for daylight saving time transitions. As someone who reads almost every time zone issue on Stack Overflow, I can report that about twice annually, after the major DST transitions in North America and Europe, there is a rash of questions about DST bugs in application code and how to prevent them.
I agree that the base type for the clock should be a class, not an interface. In trying the interface approach, I can confirm naming conflicts with other IClock
interfaces, as well as challenges when implementing common functionality such as ensuring all implementations actually return UTC-based values. Using an abstract base class let me do that easily, and also gave me the ability to implement a fast-path for how certain operations are used internally.
I believe that we need more than just a built-in copy of these clock objects. To make a meaningful improvement over the status quo, we also need to provide a way to override the behavior of the existing current time properties - Specifically DateTime.UtcNow
, DateTimeOffset.UtcNow
, DateTimeOffset.Now
, DateTime.Now
, and DateTime.Today
. And since three of these (and many other places) rely on the system's local time zone, and for the DST testing reason mentioned earlier, it is equally important to be able to override the system local time zone. In the revised proposal below, I show how this API can be represented with a TimeContext
class. In my PR, I implement that using using AsyncLocal<T>
so that it follows the execution context.
A nice added benefit of this is that we can use the TimeContext
to set the local time zone for any execution context, not just in test code. For example, one could write an ASP.Net middleware component that sets the user's local time zone for the lifetime of an HTTP request. PHP has this since 2005 with their date_default_timezone_set
function, but it's currently impossible with .NET.
Where should this all live? Well, in order to do all of what I mentioned, the implementation must live in System.Private.CoreLib
(so that DateTime
, DateTimeOffset
, and TimeZoneInfo
can hook into the TimeContext
). However, I'm up for suggestions on which namespace the public API surface should live in. For my PR, I placed them in System.Diagnostics
, as it seemed like a good place for testing-related things (i.e. Stopwatch
is there already).
The proposed API is below, followed by some usage examples.
// This is the low-level abstraction for objects that provide a current UTC-based time value.
// It is an abstract class rather than an interface, as per design guidelines.
// The two provided implementations are ActualSystemClock and VirtualClock (described below).
// Additional implementations (such as an NtpClock) could be added later or created by others.
public abstract class TimeClock
{
// There are two public methods that callers can use to get the current time - one that
// returns a DateTime, and one that returns a DateTimeOffset. Both are UTC based.
// They are methods instead of properties, as some implementations (like VirtualClock)
// can mutate internal state of the clock.
// Having two different methods prevents errors such as calling .DateTime instead of
// .UtcDateTime on a DateTimeOffset result (the difference being Unspecified vs Utc DateTimeKind).
public DateTime GetCurrentUtcDateTime();
public DateTimeOffset GetCurrentUtcDateTimeOffset();
// Implementations are only required to implement the following method.
protected abstract DateTimeOffset GetCurrentUtcDateTimeOffsetImpl();
// However, there's also an internal fast path that is implemented by the ActualSystemClock
// This is invoked by DateTime.UtcNow via the current TimeContext (more on this later).
internal virtual DateTime GetCurrentUtcDateTimeImpl();
}
// This implementation always returns the actual system time from the underlying OS.
// (The internal OS-specific native functions can move from DateTime to this class.)
// The name was chosen to prevent conflicts with exiting "SystemClock" classes, and to provide a clue
// to its behavior - it is always the clock that is actually provided by the operating system.
public sealed class ActualSystemClock : TimeClock
{
// no public construction
private ActualSystemClock();
// a singleton instance is used in all cases
public static ActualSystemClock Instance { get; }
}
// This implementation is used to provide artificial time values, and is primarily intended for testing.
// The name "Virtual Clock" has often been used to describe this design pattern, and thus makes a great
// choice for the class name (vs "FakeClock", "TestClock", etc.)
public sealed class VirtualClock : TimeClock
{
// A variety of constructors can be used. These allow creation of a clock that returns either
// a fixed value always, or one that ticks forward by an amount or a function with each usage.
public VirtualClock(DateTimeOffset timeValue);
public VirtualClock(DateTimeOffset initialTimeValue, TimeSpan advancementAmount);
public VirtualClock(DateTimeOffset initialTimeValue, Func<DateTimeOffset, DateTimeOffset> advancementFunction);
// There are also DateTime-based variations of the constructors, which prevents usage errors from implicit
// conversion from DateTime to DateTimeOffset, especially when DateTime.Kind is Local or Unspecified and
// the current TimeContext has changed the local system time zone (more on this below).
public VirtualClock(DateTime timeValue);
public VirtualClock(DateTime initialTimeValue, TimeSpan advancementAmount);
public VirtualClock(DateTime initialTimeValue, Func<DateTimeOffset, DateTimeOffset> advancementFunction);
}
// This class provides an ambient context (implemented with AsyncLocal) that can be used to run existing
// code using a specific TimeClock, or a specific TimeZoneInfo (affecting which time zone is the "local" time
// zone), or both. When code is run under a TimeContext, all existing properties and methods that work with the
// current time and/or the local time zone are routed through this class to the appropriate clock or time zone.
// For example, DateTime.UtcNow might be redirected to GetCurrentUtcDateTime of a VirtualClock, which could
// be used to validate application behavior on leap days or other important dates.
// Likewise, DateTime.ToLocalTime might use a new time zone instead of the actual system local time zone.
// The local time zone functionality helps for testing, such as application behavior during DST transitions,
// and also allows for other useful non-testing scenarios,such as setting the local time zone for the lifetime
// of an HTTP request.
public sealed class TimeContext
{
// no public construction
private TimeContext();
// Gets the current ambient time context (there will always be one).
// The default context uses the actual system clock and actual system local time zone.
public static TimeContext Current { get; }
// Some properties to return the actual clock and local time zone.
// The former is just for convienience/consistency (pointing to ActualSystemClock.Instance),
// but the latter is required here since TimeZoneInfo.Local might be redirected to a differnet time zone.
public static ActualSystemClock ActualSystemClock { get; }
public static TimeZoneInfo ActualSystemLocalTimeZone { get; }
// Some convienience properties to check if the actual clock and local time zone are active.
// This is just slightly easier than comparing the values from the current context.
public static bool ActualSystemClockIsActive { get; }
public static bool ActualSystemLocalTimeZoneIsActive { get; }
// Gets the clock and local time zone in use by the time context.
// Usage is like this: TimeContext.Current.Clock or TimeContext.Current.LocalTimeZone.
public TimeClock Clock { get; }
public TimeZoneInfo LocalTimeZone { get; }
// The below Run / RunAsync methods actually kick off a new time context and apply it
// to the given action or function. The user doesn't create the TimeContext directly,
// but rather supplies either a clock, a time zone, or both, along with an action or
// function to run. I've pivitod on sync/async and return-value/no-return-value.
// (These could be expanded further given CancelationToken or other parameters to pass through.)
public static void Run(TimeClock clock, Action action);
public static void Run(TimeZoneInfo localTimeZone, Action action);
public static void Run(TimeClock clock, TimeZoneInfo localTimeZone, Action action);
public static TResult Run<TResult>(TimeClock clock, Func<TResult> function);
public static TResult Run<TResult>(TimeZoneInfo localTimeZone, Func<TResult> function);
public static TResult Run<TResult>(TimeClock clock, TimeZoneInfo localTimeZone, Func<TResult> function);
public static Task RunAsync(TimeClock clock, Func<Task> function);
public static Task RunAsync(TimeZoneInfo localTimeZone, Func<Task> function);
public static Task RunAsync(TimeClock clock, TimeZoneInfo localTimeZone, Func<Task> function);
public static Task<TResult> RunAsync<TResult>(TimeClock clock, Func<Task<TResult>> function);
public static Task<TResult> RunAsync<TResult>(TimeZoneInfo localTimeZone, Func<Task<TResult>> function);
public static Task<TResult> RunAsync<TResult>(TimeClock clock, TimeZoneInfo localTimeZone, Func<Task<TResult>> function);
}
Using a fixed-value VirtualClock
. Leap day testing shown here:
TimeClock clock = new VirtualClock(new DateTime(2024, 2, 29));
TimeContext.Run(clock, () => {
var dt = DateTime.Now; // 2024-02-29T00:00:00
});
Using a incrementing VirtualClock
. Y2K testing shown here:
var initialValue = new DateTimeOffset(1999, 12, 31, 23, 59, 59, TimeSpan.Zero);
var advancementAmount = TimeSpan.FromSeconds(1);
TimeClock clock = new VirtualClock(initialValue, advancementAmount);
TimeContext.Run(clock, () => {
var dt1 = DateTime.UtcNow; // 1999-12-31T23:59:59Z
var dt2 = DateTime.UtcNow; // 2000-01-01T01:00:00Z
});
Using a incrementing VirtualClock
with a specific time zone. End-of-DST testing shown here:
var initialValue = new DateTime(2020, 11, 1);
var advancementAmount = TimeSpan.FromHours(1);
TimeClock clock = new VirtualClock(initialValue, advancementAmount);
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time"); // "America/Chicago" on Linux
TimeContext.Run(clock, tz, () => {
var dto1 = DateTimeOffset.Now; // 2020-11-01T00:00:00-05:00 (CDT)
var dto2 = DateTimeOffset.Now; // 2020-11-01T01:00:00-05:00 (CDT)
var dto3 = DateTimeOffset.Now; // 2020-11-01T01:00:00-06:00 (CST)
var dto4 = DateTimeOffset.Now; // 2020-11-01T02:00:00-06:00 (CST)
});
Dependency injection approach.
Service implementation:
public class MyService : IMyService
{
private readonly TimeClock _clock;
public MyService(TimeClock clock)
{
_clock = clock;
}
public int SomeFunction()
{
DateTimeOffset utcNow = _clock.GetCurrentUtcDateTimeOffset();
// ...
}
}
DI registration:
services.AddSingleton<TimeClock, ActualSystemClock>();
services.AddSingleton<IMyService, MyService>(); //or AddScoped, etc.
Unit Tests:
[Fact]
public void SomeTest()
{
var clock = new VirtualClock(new DateTime(2024, 2, 29));
var myService = new MyService(clock);
var result = myService.SomeFunction();
Assert.Equal(expected, result);
}
We can also combine these approaches if we need to apply a different local time zone
when testing a service that injects a TimeClock
. Async works too.
[Fact]
public async Task SomeAsyncTest()
{
await TimeContext.RunAsync(someTimeZone, async () =>
{
var clock = new VirtualClock(new DateTime(2020, 11, 1));
var myService = new MyService(clock);
var result = await myService.SomeAsyncFunction();
Assert.Equal(expected, result);
});
}
I explored but rejected the following other approaches:
IClock
interface pattern. As stated, this both had naming conflicts with existing interfaces, and also didn't allow for base functionality.
Making a TimeClock.Current
static property with public get
and set
accessors. The main problem with this approach is that when used in it can lead to changing the clock in one unit test affecting the clock used by another unit test if it happens to run on the same thread.
Making the exiting TimeZoneInfo.Local
settable. This has the same problem as above, but also has the problem of how to set it back to the default behavior, and how to test if it's been set to something other than the real system local time zone or not. There's also an issue that arises if an end-user actually changes the time zone of their computer and an application calls TimeZoneInfo.ClearCachedData
to pick up the change. If TimeZoneInfo.Local
was settable, then clearing the cache would have to clear any time zone that was set also, leading to some confusing behavior and potentially a race condition.
Having TimeContext
implement a scope via IDisposable
, as in:
using (new TimeContext(clock, localTimeZone))
{
//...
}
This looked good at first, but had problems if it was constructed before it was used and code ran in between, as in:
var context new TimeContext(clock);
var now = DateTime.UtcNow; // would unexpectedly be from the time context already
using (context)
{
//...
}
It also left open the possibility to be left open and not disposed correctly.
The Run
methods being instance methods on the clock, avoiding having a TimeContext
class.
For example:
var clock = new VirtualClock(someDateTime);
clock.Run(() => { ... });
This seemed ok until supporting changing the local time zone. Placing the time zone in the clock itself wouldn't work when getting values from the clock directly, and placing it as parameter on the Run
method wouldn't allow changing the time zone without changing the clock.
It also didn't make much sense having such a method on the ActualSystemClock
.
The Run
methods being static methods on TimeClock
, avoiding having a TimeContext
class.
For example:
var clock = new VirtualClock(someDateTime);
TimeClock.Run(clock, () => { ... });
This would have worked, but again seemed strange when changing only the local time zone, since that doesn't have anything to do with the clock functionality, as in:
var tz = TimeZoneInfo.FindSystemTimeZoneById("Eastern Standard Time");
TimeClock.Run(tz, () => { ... });
Ultimately, a lot of these arguments are also about Single Responsibility Principle, thus why I landed on TimeContext
being a separate class, even if somewhat related.
I look forward to feedback. Thanks.
@tarekgh
It's a really well thought solution for the problem, thank you for working on it!
The only thing I strongly disagree with is Run
methods which lead to additional allocations and flexibility loss since only in case of a static closure no allocations will happen, but if it captures some variables which will happen most of the time there will be at least two allocations: for a delegate and a closure instance.
With asynchronous Run
methods things are getting worse because there's ValueTask
conversion from which will lead to creation of Task
while no allocations were involved during method execution.
Therefore, instead of Run
methods I propose to add a few methods to create a context without public constructor exposure:
public static class TimeContext
{
public static Disposable WithClock(TimeClock clock);
public static Disposable WithClockAndTimeZone(TimeClock clock, TimeZoneInfo timeZone);
public static Disposable WithTimeZone(TimeZoneInfo timeZone);
// It's a struct for zero allocations and storing previous values of clock and zone
// in place: on the stack for sync methods and in the same region of memory
// for asynchronous methods involving a boxed state machine.
public readonly struct Disposable : IDisposable
{
}
}
Here is how usage might look:
var clock = new VirtualClock(new DateTime(2024, 2, 29));
using (TimeContext.WithClock(clock) {
var dt = DateTime.Now; // 2024-02-29T00:00:00
};
var initialValue = new DateTimeOffset(1999, 12, 31, 23, 59, 59, TimeSpan.Zero);
var advancementAmount = TimeSpan.FromSeconds(1);
var clock = new VirtualClock(initialValue, advancementAmount);
using (TimeContext.WithClock(clock) {
var dt1 = DateTime.UtcNow; // 1999-12-31T23:59:59Z
var dt2 = DateTime.UtcNow; // 2000-01-01T01:00:00Z
};
var initialValue = new DateTime(2020, 11, 1);
var advancementAmount = TimeSpan.FromHours(1);
TimeClock clock = new VirtualClock(initialValue, advancementAmount);
TimeZoneInfo tz = TimeZoneInfo.FindSystemTimeZoneById("Central Standard Time"); // "America/Chicago" on Linux
using (TimeContext.WithClockAndTimeZone(clock, tz) {
var dto1 = DateTimeOffset.Now; // 2020-11-01T00:00:00-05:00 (CDT)
var dto2 = DateTimeOffset.Now; // 2020-11-01T01:00:00-05:00 (CDT)
var dto3 = DateTimeOffset.Now; // 2020-11-01T01:00:00-06:00 (CST)
var dto4 = DateTimeOffset.Now; // 2020-11-01T02:00:00-06:00 (CST)
};
As you can see not so much changed, just a bit more wordy method name and less braces, but with that approach there will be much less memory allocations involved.
Calling it "TimeClock" is a little weird, because it's got date information in it... and "TimeClock" feels like, well, a time clock on the wall... a thing that doesn't have a date component. Though "ISystemClock" isn't much better in that regard, at least it doesn't reinforce the "Time" part of it, when the date part is equally important. "DateTime" and "DateTimeOffset" both include the fact that dates are involved (They could have just been "Time" and "TimeOffset"), so it feels like this concept, if the word "Time" is going to be used, the word "Date" needs to be too.
It's also a bit surprising that the behavior of the property accessors on DateTime and DateTimeOffset are going to change after so long. That's a fairly significant behavior change to some pretty core functionality in .NET. Maybe that's ok? I'd personally probably never use that side effecting behavior: I'd make sure any code that wanted to be sensitive to time contexts always got one of these objects from somewhere. I would be worried about third party libraries I'm using failing in strange ways if the behavior of DateTime.Now changed out from under them.
Thanks very much for the feedback thus far.
@ChadNedzlek - I would also be ok with DateTimeClock
, or just Clock
. Naming is hard. 😉 Also, the point you made about a library's behavior changing is exactly why having a TimeContext
is valuable. If one is intending to test the behavior of application code, and that code uses a library that in turn gets the current time, then there's not going to be a direct way to provide a clock unless the library author decides to expose some way to pass one in. Since most code just uses DateTime.Now
and its friends, we need some ability to have that emit test values within an outer scope.
@YohDeadfall - I like your idea about using a lightweight struct to avoid allocations. However I still have concerns about the disposable scope around what happens if it's not disposed correctly, and when code falls between when the context is declared and the using
statement. Perhaps an analyzer would be helpful to ensure correct usage there?
I had one other idea I haven't explored yet, but would like to discuss. Since there's some concern about how the TimeContext
might be abused (say a library author uses TimeContext
in the library's code instead of in a test), we could add some application configuration flags that would have to be set to be able to detour the clock or local time zone. This would be very similar to how one can set the runtime host configuration to use NLS instead of ICU. There would be two new boolean flags, something like AllowCurrentDateTimeFromTimeContext
and AllowLocalTimeZoneFromTimeContext
that would have to be set true
for the context to have any effect (the default being false
). These would then be set true on unit test projects that wanted to take advantage of this feature, or perhaps automatically set true by test runners. This would also solve some of the perf concerns, as this would become an opt-in feature. Thoughts?
say a library author uses
TimeContext
in the library's code instead of in a test
It may be advantageous to use a hypothetical TimeContext
to affect ambient code to reflect a user's timezone, similar to how ASP.NET's RequestLocalizationMiddleware updates CultureInfo.CurrentCulture
to enable localization.
@FiniteReality - Indeed! Such an ASP.net project could have AllowLocalTimeZoneFromTimeContext
turned on in their runtime config, yes?
@FiniteReality - Indeed! Such an ASP.net project could have
AllowLocalTimeZoneFromTimeContext
turned on in their runtime config, yes?
Possibly - I'd assume such a change would have a higher barrier to entry though, since it would take a while to become available everywhere for ASP.NET to be able to use it
However I still have concerns about the disposable scope around what happens if it's not disposed correctly, and when code falls between when the context is declared and the using statement. Perhaps an analyzer would be helpful to ensure correct usage there?
Yes, it should be covered by an analyzer that any disposable is actually disposed. As I remember JetBrain Rider highlights all such places and notify the developer than that should be fixed. While some code (read SQL Client) allows abandoning disposable objects most of .NET ecosystem doesn't support that.
I can see the compat breaking change of how DateTime works causing some really hard to diagnose problems. Like a caching library with a background "cleanup" thread. That code isn't going to expect "UtcNow" to change so drastically between contexts (because it's been stable for a few decades). If anyone uses the static altering TimeContext, things could go badly (things either never expiring, or instantly expiring). And that caching might be 4 levels deep in a dependency tree, so you, the user of TimeContext, have no way to know if you're going to break something... because it's static, and static state changes are impossible to judge the scope of. It's not even really something that library could have been hardened against, because this behavior isn't possible today.
Maybe that means "Ok, only use it for testing, never in real code", but that's an odd design choice for something in the framework. Something that is dangerous enough that needs recommendations not to use it in "real" code.
I'm happy about having a clock someone can request and use, meaning that code has explicitly stated their presumed understanding that this clock has context associated with it. So DateTimeClock.Current is fine. It's changing the very old behavior of DateTime.Now that worries me the most.
The original proposal (perhaps with the few additional method suggested later) seems like a safe, cheap, understandable solution, without any of the compatibility problems, that solves a very large number of scenarios. In particular, even if this revised proposal were accepted, I would still continue to use Microsoft.Extensions.Internal.ISystemClock in all my code.
I personally, regardless of slight performance hits, vastly prefer explicitly providing some sort of IClock
interface to anything needing to get time-related information. Time is one of the more insidious system dependencies, and I like to be explicit that it's being used.
Note that, for most applications, there will be exactly one clock for the runtime of an application, instantiated during initialization, and the specific class known at compile time.
However I still have concerns about the disposable scope around what happens if it's not disposed correctly, and when code falls between when the context is declared and the using statement. Perhaps an analyzer would be helpful to ensure correct usage there?
Yes, it should be covered by an analyzer that any disposable is actually disposed. As I remember JetBrain Rider highlights all such places and notify the developer than that should be fixed. While some code (read SQL Client) allows abandoning disposable objects most of .NET ecosystem doesn't support that.
That is IDisposableAnalyzers you are asking for (which is outside the scope of this issue).
I think if there was a primitive for ISystemClock
that all .NET 6 developers who care about mocking datetime's could agree on, you'd see code bases migrate to it over time rather than roll their own. An optional analyzer could detect accesses of DateTime and friends outside of an ISystemClock implementation and say "hey have you considered using ISystemClock for better mockability" or something similar. I can see the appeal of changing the behaviour of DateTime
and friends within a given context using the awesome soluton above, but that also makes me very nervous around edge cases where third party libraries undergo time travel at runtime in ways they might not have expected - perhaps that caution is unjustified though :-). Also what about nested contexts - would that be a thing?
@dazinator I like that addition to the proposal, it would help developers move away from statics in their code base
I noticed a surprising lack of discussion around the name of the abstraction itself. While ISystemClock
is now present in a bunch of different places, I'm not convinced it is a good name at all, to be honest. I'd even go as far as to say that it has this name in multiple places due to influence from the originator and was just replicated as-is later.
First, "System" in the name to me implies a concrete implementation already. The fact it provides the "system"'s clock and not "another clock" is an implementation detail that should not be in the interface name itself. Thus, IClock
would make more sense for this aspect.
Having said that, "clocks" don't usually provide date information, only time of day. With that in mind, I think using clock
(in either IClock
or ISystemClock
forms) is already misleading conceptually. I know this is can be a bit silly, but perhaps something more direct and obvious, like IDateTimeProvider
, would be more semantic and match existing DateTime
types? In that case, you'd then have a SystemDateTimeProvider
concrete implementation which is the wrapper that delegates to the static method (again, System
here being the implementation detail).
I'm not convinced that IDateTimeProvider
is a perfect name. I just think ISystemClock
is a fairly bad one.
One thing to note: Outside of the current instant, it can be incredibly useful to also provide the "current" timezone, especially for something like a server web application (it provides the user's "local" clock).
public interface ISystemClock
{
DateTimeOffset UtcNow { get; }
TimeZoneInfo LocalZone { get; }
// also include?
DateTimeOffset LocalNow => TimeZoneInfo.ConvertTime(UtcNow, CurrentZone);
}
For tests, as well, this would free us from having to rely on system-provided zones at all, since you could manually construct a number of them ahead of time to use for the clock.
I feel like the "System" part is the implication that it's only job is to return what time the system thinks it is. And, indeed, that's what the interface does (it only has "now" type properties). An IDateTimeProvider doesn't have the same implicit "this is all about now", and feels like it might provide arbitrary times, which isn't really what the abstraction is about. INowProvider is... weird.
An IDateTimeProvider doesn't have the same implicit "this is all about now", and feels like it might provide arbitrary times, which isn't really what the abstraction is about.
To an extent it is, actually. Besides testing, it might be handy to provide a "future time" clock for certain simulation/scheduling scenarios, and it makes fun use-cases like time dilation or contraction much easier.
I think that's a very different object. If I'm looking at code that's using an ISystemClock, I understand based on that that the code is just trying to deal with the current time, which is useful information. If it's just random, arbitrary times, the usage will have to look very different, and that would be important to know, so it should probably be a separate abstraction.
An IDateTimeProvider doesn't have the same implicit "this is all about now"
ICurrentDateTimeProvider
?
Clock
is more concise than CurrentDateTimeProvider
though.. as long as you can glance past the point about a clocks purpose not providing dates but only time information. Some digital clocks do have supplementary date information, it wouldn't keep me awake at night personally.
ISystemClock
seems like a fine name to me:
var x = clock.Now; var y = clock.Now;
, y
can be wildly different to x
)midnight, January 1, 0001 Anno Domini (Common Era)
(thanks MS docs) and can also be compared against the Unix epochWhat about the property name itself? Similarly to my argument on the class name, I think the Utc
part is an implementation detail.
Instead of
public class SystemClock : ISystemClock
{
public DateTimeOffset UtcNow => DateTimeOffset.UtcNow;
}
Wouldn't it be better to have:
public class UtcSystemClock : ISystemClock
{
public DateTimeOffset Now => DateTimeOffset.UtcNow;
}
?
You could then potentially have 2 distinct implementations: one that returns UTC, and the other that returns the adjusted time:
public class SystemClock : ISystemClock
{
public DateTimeOffset Now => DateTimeOffset.Now;
}
... NodaTime and Java get around this by having an explicit absolute timestamp type (something without a zone attached at all).
I personally feel that it's better to have something that explicitly returns a UTC (or some zone-ignorant) stamp + a zone, as both NodaTime and Java do, and maybe including an additional method/property that gets the local time. You cannot avoid including the zone, I don't think, because otherwise you lose too much context (for one thing, "did DST just occur?").
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.