Use DateTimeContext to put your code in a time warp

On 12/31/08 all the 30 gb Zunes in the whole world died en masse.  This was attributed to a bug having to do with 2008 being a leap year.  Because the 31st was the 366th day of the year, something in the clock code for the Zune freaked out, locked up, and earned Microsoft a lot of scorn from the Apple fanboys of the world.

This started me thinking.  It should be easier to test what your code will do at a particular time.  Thinking aloud over Twitter, I settled on a design similar to the way TransactionScope works.  You put your code inside a using statement, and it thinks it’s whatever time you want.  It’s not quite that easy since we can’t just replace the DateTime class, or at least not that I know of.  If someone has some Ninja tricks that would allow us to do this, I’d be glad to hear them.

In the meantime you’ll have to replace all the calls to DateTime.Now and DateTime.Today with calls to similar properties on the new DateTimeContext class.  If you’re not IN a DateTimeContext at the time, then these methods will return the real date or time, just like you’d expect.  If you wrap some code in a using DateTimeContext block, then they’ll return whatever you’ve told them to.  I dislike changing code solely for the purpose of testing, but for purposes of testaBILITY is okay.

Here’s a unit test I wrote that illustrates how the DateTimeContext class is used.

[TestMethod]
public void DateTimeContext_returns_expected_values()
{
    DateTime date1 = new DateTime(2008, 12, 31, 23, 59, 59);
    DateTime date2 = new DateTime(2009, 1, 1, 0, 0, 0); 

    Assert.AreEqual(DateTime.Today, DateTimeContext.Today);
    using (new DateTimeContext(date1))
    {
        Assert.AreEqual(date1, DateTimeContext.Now);
        using (new DateTimeContext(date2))
        {
            Assert.AreEqual(date2, DateTimeContext.Now);
        }
        Assert.AreEqual(date1, DateTimeContext.Now);
    }
    Assert.AreEqual(DateTime.Today, DateTimeContext.Today);
}

Notice that the Asserts that are outside of any context are comparing against Today, and the ones inside are using Now.  This is just to get around the problem where the time could have changed between two retrievals.  I could have asserted that the difference between the times was some negligibly small amount, but I wanted the excuse to hit both the Now and Today properties anyway.

Now on to how it works.  The DateTimeContext class uses Thread Local Storage to store a reference to the current instance, and provides a static "Current" property to help us get to that instance.  When we ask for Now or Today, the static methods will check to see if there is an instance on the thread, and if so, they’ll return what they’ve been told to return.  If not, then they’ll defer to the real DateTime values.

public static DateTime Now
{
    get
    {
        DateTimeContext current = Current;
        return (context == null) ? DateTime.Now : current.Value;
    }
} 

public static DateTime Today
{
    get { return Now.Date; }
}

Each instance of the class also has a "PreviousContext" property that will remember the instance that was on the thread when it was instantiated.  This lets us nest contexts, and they’ll unwind properly when you’ve finished with them.

private object PreviousContext { get; set; }

There are multiple constructors which mirror the most commonly-used DateTime constructors as a convenience.

public DateTimeContext(long ticks)
    : this(new DateTime(ticks)) {}

public DateTimeContext(int year, int month, int day)
    : this(new DateTime(year, month, day)) {}

public DateTimeContext(int year, int month, int day, int hour, int minute, int second)
    : this(new DateTime(year, month, day, hour, minute, second)) {}

public DateTimeContext(int year, int month, int day, int hour, int minute, int second, int millisecond)
    : this(new DateTime(year, month, day, hour, minute, second, millisecond)) {}

public DateTimeContext(DateTime value)
{
    Value = value;
    var slot = Thread.GetNamedDataSlot(slotName);
    PreviousContext = Thread.GetData(slot);
    Thread.SetData(slot, this);
}

Finally, there are all the fiddly IDisposable bits and destructors that make it all tick.  When a DateTimeContext is disposed, it puts the previous instance back on the thread, allowing us to stack them up all we want (Or until we run out of memory).

Here’s the whole class.  Hopefully it proves useful for someone.  It should make it a lot easier to test what certain code will do on or just after certain transition/edge case days.  This may not be the final version, but I couldn’t stop thinking about it until I wrote it out.

public class DateTimeContext : IDisposable
{
    #region Fields

    public const string slotName = "DateTimeContext";
    private object PreviousContext { get; set; }

    #endregion

    #region Properties

    public DateTime Value { get; set; }

    public static DateTimeContext Current
    {
        get
        {
            var slot = Thread.GetNamedDataSlot(slotName);
            var current = (DateTimeContext)Thread.GetData(slot);
            return current;
        }
    }

    public static DateTime Now
    {
        get
        {
            DateTimeContext current = Current;
            return (current == null) ? DateTime.Now : current.Value;
        }
    }

    public static DateTime Today
    {
        get { return Now.Date; }
    }

    #endregion

    #region Constructors/Destructors

    public DateTimeContext(long ticks)
        : this(new DateTime(ticks)) {}

    public DateTimeContext(int year, int month, int day)
        : this(new DateTime(year, month, day)) {}

    public DateTimeContext(int year, int month, int day, int hour, int minute, int second)
        : this(new DateTime(year, month, day, hour, minute, second)) {}

    public DateTimeContext(int year, int month, int day, int hour, int minute, int second, int millisecond)
        : this(new DateTime(year, month, day, hour, minute, second, millisecond)) {}

    public DateTimeContext(DateTime value)
    {
        Value = value;
        var slot = Thread.GetNamedDataSlot(slotName);
        PreviousContext = Thread.GetData(slot);
        Thread.SetData(slot, this);
    }

    ~DateTimeContext()
    {
        RestoreState();
    }

    #endregion

    #region Methods

    private void RestoreState()
    {
        var slot = Thread.GetNamedDataSlot(slotName);
        Thread.SetData(slot, PreviousContext);
        if(PreviousContext==null)
            Thread.FreeNamedDataSlot(slotName);
    }

    public void Dispose()
    {
        RestoreState();
        GC.SuppressFinalize(this);
    }

    #endregion
}

Update 1/1/09: I decided to call the class DateTimeContext instead of the original DateTimeScope.  It just sounded better to me personally.  Also, comments about the DateTime issue "possibly" being the cause of the global Zunepocalypse were removed since that’s exactly what it turned out to be in the end.

Technorati Tags: ,,,,
Advertisement
This entry was posted in Computers and Internet. Bookmark the permalink.

4 Responses to Use DateTimeContext to put your code in a time warp

  1. Jon says:

    This is awesome. Can you add a link to the class file?

  2. Mel says:

    I\’ve uploaded the file to my SkyDrive, although the whole thing is represented in the post above. Here\’s a link: http://tdsgaq.blu.livefilestore.com/y1pKe7XDAgqPgCNKkUOR1fCU6z1KNLflSou2f76qu7UvljjwDBJ-Ivo1XVLczzOgXV6ZC31KvEYejhXuZ9aLWFASQ/DateTimeContext.zip?download

  3. Paul says:

    Thanks, this is pretty useful.

  4. Mel Grubb says:

    The above code is a little larger than it actually needs to be. The manual manipulation of the thread named slots is actually not needed, and this code can be written in less space by using the ThreadStatic attribute on the backing variable. This is what I was playing with when I wrote the class, though, because I was interested in the workings of thread-local storage.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s