Doing the Time-Warp






3.67/5 (2 votes)
Temporarily freeze time by injecting a thread-safe time bubble into your C# code.
Introduction
I was recently refactoring a legacy calculation-service that was making lots of calls to DateTime.Now
to get the current time. About 1% of calculations had small discrepancies caused by the time changing in the middle of an invocation.
I needed a way of making freezing the system-time while each calculation is running ... something with an API like this:
SystemTime.InvokeInTimeBubble(() =>
{
// The current time is 2013-06-01 10:30:50.123
MySlowCalculationEngine.Calculate();
// The current time is still 2013-06-01 10:30:50.123
});
// The current time is back to normal.
The solution had to be thread-safe, fast, and I could not change lots of method-signatures in the legacy code.
The technique can also be applied for unit testing of time-sensitive code.
The code
Here’s the code - with lots of comments:
using System;
namespace AndysStuff
{
public static class SystemTime
{
// The ThreadStatic attribute forces each thread to maintain its own copy.
[ThreadStatic]
private static DateTime? _threadSpecificFrozenTime;
/// <summary>
/// Read-only access to the thread's current time.
/// </summary>
public static DateTime Now
{
get
{
if (_threadSpecificFrozenTime.HasValue)
{
return _threadSpecificFrozenTime.Value;
}
return DateTime.Now;
}
}
/// <summary>
/// Read-only access to the thread's current date.
/// </summary>
public static DateTime Today
{
get { return Now.Date; }
}
/// <summary>
/// Execute a Delegate or Anonymous-function within a temporary time-bubble.
/// </summary>
/// <example>
/// SystemTime.InvokeInTimeBubble(() =>
/// {
/// // Code that needs to execute within the time-bubble.
/// });
/// </example>
public static void InvokeInTimeBubble(Action func)
{
InvokeInTimeBubble(Now, func);
}
/// <summary>
/// Execute a Delegate or Anonymous-function within a temporary time-bubble
/// using a specific time.
/// </summary>
public static void InvokeInTimeBubble(DateTime frozenTime, Action func)
{
// Make a copy of the current frozen-time.
var originalTime = _threadSpecificFrozenTime;
// Use a try-block so that we can guarantee that we have tidied-up.
try
{
// Set the thread's time to the specified frozen time.
_threadSpecificFrozenTime = frozenTime;
// Invoke the action within the time-bubble.
func();
}
finally
{
// Tidy-up.
_threadSpecificFrozenTime = originalTime;
}
}
}
}
Using the code
Any code that previously read from DateTime.Now
or DateTime.Today
needs to be changed to read from the Now
or Today
properties of the new SystemTime
Class.
For my legacy calculation-service scenario, I use something like this:
var sessionOld = sessionNew.Clone();
SystemTime.InvokeInTimeBubble(() =>
{
// Time is frozen.
// Calculate using old and new methods.
OldCalculation(sessionOld);
NewCalcaultion(sessionNew);
};
// Time is now unfrozen.
LogDifferences(logger, sessionOld, sessionNew);
For unit-testing scenarios, I use:
var currentTime = new DateTime(2013, 6, 10, 10, 30, 30);
SystemTime.InvokeInTimeBubble(currentTime, () =>
{
// This version of DoAddMinutes adds 10 minutes to the current system time.
var result = DateTimeHelper.DoAddMinutes(10);
Assert.AreEqual(new DateTime(2013, 6, 20, 10, 30, 30), result);
};
If your code creates a new thread then it will drop back to using the proper system time. TPL tasks may-or-may-not execute within the time bubble. The following example shows how to set up time-bubbles in parallel code:
// Get the "current" system-time.
var currentTime = SystemTime.Now;
Parallel.ForEach(listToProcess, (x) =>
{
// Create a new time-bubble for each parallel thread to run within.
SystemTime.InvokeInTimeBubble(currentTime, () =>
{
Calculate(x);
});
});
Alternative solution
My solution was constrained because I was trying to make minimal changes to legacy code. If I was coding the calculations from scratch then I would have seriously considered using constructor-injection to pass a context object around.