Click here to Skip to main content
15,899,679 members
Articles / DevOps / Unit Testing
Tip/Trick

Doing the Time-Warp

Rate me:
Please Sign up or sign in to vote.
3.67/5 (2 votes)
11 Jun 2013CPOL1 min read 13K   4   5
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:

C#
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:

C#
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:

C#
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:

C#
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:

C#
// 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, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
Software Developer (Senior)
United Kingdom United Kingdom
Andy has been a professional software developer for over 20 years. He is currently writing a high-performance calculation engine for a large insurance company.

Comments and Discussions

 
GeneralMy vote of 3 Pin
johannesnestler13-Jun-13 3:30
johannesnestler13-Jun-13 3:30 
QuestionA neat solution but... Pin
George Swan11-Jun-13 8:16
mveGeorge Swan11-Jun-13 8:16 
AnswerRe: A neat solution but... Pin
andyharman11-Jun-13 11:45
professionalandyharman11-Jun-13 11:45 
GeneralRe: A neat solution but... Pin
George Swan11-Jun-13 19:19
mveGeorge Swan11-Jun-13 19:19 
GeneralMy vote of 5 Pin
nobodyxxxxx11-Jun-13 2:13
nobodyxxxxx11-Jun-13 2:13 

General General    News News    Suggestion Suggestion    Question Question    Bug Bug    Answer Answer    Joke Joke    Praise Praise    Rant Rant    Admin Admin   

Use Ctrl+Left/Right to switch messages, Ctrl+Up/Down to switch threads, Ctrl+Shift+Left/Right to switch pages.