Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

Doing the Time-Warp

0.00/5 (No votes)
11 Jun 2013 1  
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(() =&gt;
        /// {
        ///        // 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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here