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(() =>
{
MySlowCalculationEngine.Calculate();
});
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
{
[ThreadStatic]
private static DateTime? _threadSpecificFrozenTime;
public static DateTime Now
{
get
{
if (_threadSpecificFrozenTime.HasValue)
{
return _threadSpecificFrozenTime.Value;
}
return DateTime.Now;
}
}
public static DateTime Today
{
get { return Now.Date; }
}
public static void InvokeInTimeBubble(Action func)
{
InvokeInTimeBubble(Now, func);
}
public static void InvokeInTimeBubble(DateTime frozenTime, Action func)
{
var originalTime = _threadSpecificFrozenTime;
try
{
_threadSpecificFrozenTime = frozenTime;
func();
}
finally
{
_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(() =>
{
OldCalculation(sessionOld);
NewCalcaultion(sessionNew);
};
LogDifferences(logger, sessionOld, sessionNew);
For unit-testing scenarios, I use:
var currentTime = new DateTime(2013, 6, 10, 10, 30, 30);
SystemTime.InvokeInTimeBubble(currentTime, () =>
{
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:
var currentTime = SystemTime.Now;
Parallel.ForEach(listToProcess, (x) =>
{
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.