Using a Domain Model for Time in .NET
As a developer, I’ve had to write a lot of code related to dates and time. Time is important. Time is money! Time is on my side; yes it is.
Ok, so .NET provides a number of classes in System for dealing with time, and 3rd party libraries like NodaTime provide even more, which means anything time-related is readily available to today’s .NET developer.
So what exactly is the problem?
The problem is that these libraries don’t know anything about the business domain you’re coding for, and they make it easy to use them inconsistently or even incorrectly. Let’s look at an example.
Have you ever written something like this?
while (true) {
if (DateTime.Now.Hour == 12 && DateTime.Now.Minute == 0)
{
Console.WriteLine("Time for lunch!");
SetCalendar("Back in 30 minutes.");
return;
}
Thread.Sleep(1000);
}
You run it on your computer and at noon, sure enough, the console message appears and then whatever SetCalendar does executes and all is well. Time to ship! You install it on your Azure VM and at 5 AM your calendar lets you know that you’ve just gone to lunch. Wait, what? DateTime.Now() returns a DateTime object set to the local time for the Windows computer on which it runs, which on your workstation might be Pacific Daylight Time; but on an Azure VM it will be UTC.
You go back to your program and make some adjustments. You know that your time zone is 7 hours behind UTC, so you write a quick and dirty method that takes a UTC time and returns the local equivalent.
private DateTime GetLocalTime(DateTime datetime, int offset)
{
if (datetime.Kind != DateTimeKind.Utc)
{
throw new Exception("Needs to be UTC");
}
var local = new DateTime(datetime.Year, datetime.Month, datetime.Day, datetime.Hour, datetime.Minute, datetime.Second, DateTimeKind.Local);
local = local.Add(TimeSpan.FromHours(offset));
return local;
}
and update your code to look something like this:
while (true)
{
var now = GetLocalTime(DateTime.UtcNow, -7);
if (now.Hour == 12 && now.Minute == 0)
{
Console.WriteLine("Time for lunch!");
SetCalendar("Back in 30 minutes.");
return;
}
Thread.Sleep(1000);
}
Now you run it on the server and it works! Great news. But now this is awkward as you have to remember to use that translation method every time you need a date or time value. Are there times when you wouldn’t want to use it? Are there times when you need to use a different time zone offset?
Enter domain time.
Domain time is simply writing your own custom classes for handling time values which are tailored to your business domain.
Imagine writing something like this instead:
var domainTime = MyDomainTime.FromLocal(TimeZoneHelper.UsPacific);
if (domainTime.IsLunchtime())
{
Console.WriteLine("Time for lunch!");
SetCalendar("Back in 30 minutes.");
return;
}
Thread.Sleep(1000);
Notice a couple of things here.
First, we’re defining the perspective from which the time value has meaning. FromLocal() returns an instance of the MyDomainTime class which then exposes properties and methods that execute from the local time zone’s perspective. Here we’ve passed in a timezone value, but you could also define a default. You can imagine overloads of FromLocal() that maybe take geographical coordinates instead of a timezone, or a database dependency, or even more specific versions like FromEasternDivision() or FromKathysOffice() or whatever, as long as it has relevant meaning in your problem domain.
The other thing to notice is that the actual hour and minute of lunchtime is abstracted away from the consuming code. This suggests that “lunchtime” is a concept with a common meaning across the domain. That is, a concept where you might want to define it once and reuse rather than run the risk of one developer deciding to use noon in one place, someone else using 11:45, etc. Depending on the complexity of the domain, you might expand on this in a Lunch class that exposes different properties and methods dealing with lunch, where that class decorates or otherwise references MyDomainTime to handle the mechanics.
Pretty simple, right?
But now you’ve thought of something. This logic depends on the concept of “now” which is always different. Like “now.” And “now” again. And the second “now” is different from that first “now.” You know? How can you write a unit test for this new domain logic without setting your system clock to 11:59 AM, running your program and waiting a minute?
We can add functionality to our class to override what “now” means by defining our own Now() method that invokes a Func of type DateTime:
private static Func<DateTime> _funcForDeterminingNow;
public DateTimeOffset Now()
{
var now = _funcForDeterminingNow.Invoke();
return TimeZoneHelper.GetDateTimeOffset(_timeZone, now);
}
whose default implementation we can set to the standard DateTime.UtcNow property in our static constructor:
_funcForDeterminingNow = () => DateTime.UtcNow;
but which can be overridden with a static method that looks like this:
public static void SetNow(Func<DateTime> nowFunc)
{
_funcForDeterminingNow = nowFunc;
}
Now in our tests, we can set a “now” value as part of the test arrange steps:
[TestMethod]
public void MyTest()
{
MyDomainTime.SetNow(() => new DateTime(2015, 11, 4, 8, 45, 12, DateTimeKind.Utc));
var now = MyDomainTime.FromLocal("US/Pacific").Now();
Assert.AreEqual(2015, now.Year);
Assert.AreEqual(11, now.Month);
Assert.AreEqual(4, now.Day);
Assert.AreEqual(0, now.Hour); // should be the local hour, given the UTC-8 for this date
Assert.AreEqual(45, now.Minute);
Assert.AreEqual(12, now.Second);
Assert.AreEqual(new TimeSpan(-8, 0, 0), now.Offset);
}
Note the use of UTC as the basis for any “now” comparison. This is important as internally, we should deal with only one timezone from which “local” is relative, and it helps that UTC doesn’t change with daylight saving. It’s not ideal to have methods exposed by the class that are only intended to be used in testing; however, it’s useful and important enough that I think it’s worth making the exception.
Beyond “now”
A lot of these examples deal with “now,” but you can imagine using a domain model to express any meaningful time values used in your business logic. You could have static methods or properties, where the time component/perspective is not important, e.g.,:
public static int CompanyYearOfIncorporation => 2013;
public static string MonthThatContainsAllTheMadness => "March";
and non-static methods or properties that do care about the time component:
public bool IsExpired(DateTime expirationDate)
{ /* ignore the time component of the input date and compare against midnight local time */ }
public DateTimeOffset StartOfTotalSolarEclipse();
In summary
The power of using a domain model for time is that it forces developers to think up front about what time values mean as they relate to the business problem at-hand. Later on, once the domain model is defined, it reduces the number of decisions that a developer has to make while coding. It contextualizes those decisions and abstracts them away from the level of minutes and hours and timezones and to the level of “application deadline” or “delivery date.” And by using a framework like this one these values are also testable.
A full project demonstrating this concept may be downloaded here: DomainTime Demo