Domain model purity and the current time

Time as an ambient context

I received a couple of interesting questions to the previous post that I thought I would address with this article.

public static class DateTimeServer
{
private static Func<DateTime> _func;
public static DateTime Now => _func();
public static void Init(Func<DateTime> func)
{
_func = func;
}
}
// Initialization code for production
DateTimeServer.Init(() => DateTime.Now);
// Initialization code for unit tests
DateTimeServer.Init(() => new DateTime(2020, 1, 1));
  • This implementation pollutes the production code: it introduces production code that’s only needed for testing. Mixing up test and production code increases the maintenance costs of the latter.
  • The static field becomes a shared dependency, thus transitioning the tests using it into the sphere of integration testing because they can no longer run in parallel.
  • The Ambient Context pattern in general and this implementation in particular masks potential problems in code. If injecting a dependency explicitly becomes so inconvenient that you have to resort to ambient context, that’s a certain sign of trouble. Most likely, you have too many layers of indirection.

Time as an explicit dependency

A better approach is to inject the time dependency explicitly, either as a service or as a plain value:

public interface IDateTimeServer
{
DateTime Now { get; }
}

public class DateTimeServer : IDateTimeServer
{
public DateTime Now => DateTime.Now;
}

public class MyController
{
private readonly IDateTimeServer _dateTimeServer;

public MyController(
IDateTimeServer dateTimeServer) // Injecting time as a service
{
_dateTimeServer = dateTimeServer;
}

public void MyAction(int id)
{
MyDomainClass entity = GetById(id);
entity.DoAction(_dateTimeServer.Now); // Injecting time as a plain value
Save(entity);
}
}

A more complex example

The second question is a more complex example regarding this guideline:

public class ServiceAgreementEntity
{
public DateTime StartDate { get; }
public DateTime EndDate { get; private set; }
public ServiceAgreementEntity(DateTime startDate)
{
if (startDate < DateTime.Today)
throw new ArgumentException();
StartDate = startDate;
}
public void SetEndDate(DateTime endDate)
{
if (StartDate > DateTime.Today.AddDays(-5))
throw new ArgumentException();
EndDate = endDate;
}
}
  • On the one hand, you can’t set a past date to the StartDate property
  • But on the other, a past StartDate is required in order to test the SetEndDate method
public class ServiceAgreementEntity
{
public DateTime StartDate { get; }
public DateTime EndDate { get; private set; }
public ServiceAgreementEntity(DateTime startDate, DateTime now)
{
if (startDate < now.Date)
throw new ArgumentException();
StartDate = startDate;
}
public void SetEndDate(DateTime endDate, DateTime now)
{
if (StartDate > now.Date.AddDays(-5))
throw new ArgumentException();
EndDate = endDate;
}
}

Summary

  • Representing the current time as an ambient context pollutes the production code and makes testing more difficult.
  • Inject time as an explicit dependency — either as a service or as a plain value.
  • Inject it as a service into controllers.
  • Inject it as a plain value in domain classes.

Subscribe

Subscribe to read more articles like this: https://enterprisecraftsmanship.com/subscribe

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store