Adding Domain Events to Business Logic: 10 Years of .Net Compressed into Weeks #7
on
This post is part of a blog series ASP.Net 10 Years On. Even though this is part of a series I have tried to make each post standalone.
We are faced with the task of selecting a winner from the valid entrants who picked the correct answer. The logic is to select a winner at random and then contact the winner via the appropriate contact method e.g. SMS, Email etc. We started off with the competition class in the previous post.
Simple right? We could just write a method such as the below:
public void PickWinner()
{
var winner = GetCompetitionWinner();
switch (winner.Entrant.Source)
{
case BB.SmsQuiz.Model.Entrants.EntrantSource.Email:
MailMessage mail = new MailMessage("noreply@example.com", winner.Entrant.Contact.Contact);
mail.Body = "Congratulations...";
SmtpClient smtp = new SmtpClient();
smtp.Send(mail);
break;
case BB.SmsQuiz.Model.Entrants.EntrantSource.Sms:
// some code to send the SMS here
break;
default:
break;
}
}
Look at this code and then repeat after me… NO!
How could this be unit tested? How do we test that an SMS/Email has been sent? We could send out real emails but that would be time consuming and I want to test my business logic and not whether the email has been sent. I'm only interested in checking that the line of code to generate an email is called when expected.
We could hide the implementation behind an interface and use the factory pattern such as the below:
public void PickWinner()
{
var winner = GetCompetitionWinner();
IContactWinner contact = Factory.ContactFactory(winner.Entrant.Source);
contact.SendMessage(winner);
}
If you are unfamiliar with factories, the Factory.ContactFactory method will take the contact type of the given entrant and return an implementation like our EmailWinner example class that follows:
public interface IContactWinner
{
void SendMessage(Entrants.Entrant entrant);
}
public class EmailWinner : IContactWinner
{
public void SendMessage(Entrants.Entrant entrant)
{
MailMessage mail = new MailMessage("noreply@example.com", entrant.Contact.Contact);
mail.Body = "Congratulations...";
SmtpClient smtp = new SmtpClient();
smtp.Send(mail);
}
}
This is an improvement but we've introduced the dependency of IContactWinner into the domain entity and we also have the added complexity of the Factory.ContactFactory method. These two elements are barriers we would have to pass in order to write unit tests for the PickWinner method.
Introducing Domain Events
The trick is to raise some form of event that will be handled by some kind of service outside of the domain model. All we care about is that the event is raised in the correct context and that the parameters we expect are passed in. The concept is known as a domain event.
Let's start with a test that checks both the raising of the event and that the winner is not null. Note: following on from the principle of roughly 7 items in a class in the previous post we will create a new class called WinnerSelector:
/// Tests that selecting a winner raises winner selected event with the expected instance parameters.
[TestMethod]
public void SelectingAWinnerRaisesWinnerSelectedEvent()
{
// Arrange
WinnerSelector winnerSelector = new WinnerSelector(new CompetitionStatistics(GetMockCompetition(), GetMockEntrants()));
WinnerSelectedEvent winnerSelectedEvent = null;
// Tell the the DomainEvents class about our test delegate
DomainEvents.Register<WinnerSelectedEvent>(evt => winnerSelectedEvent = evt);
// Act
winnerSelector.PickWinner();
// Assert
Assert.IsNotNull(winnerSelectedEvent);
Assert.IsNotNull(winnerSelectedEvent.Competition);
}
Here is the key line:
DomainEvents.Register<WinnerSelectedEvent>(evt => winnerSelectedEvent = evt);
Here we are registering the action of setting our winnerSelectedEvent variable to the value being set within the PickWinner method. In the real implementation we would be registering an event that accepts the winner detail and uses it to send the actual email. If our event was not being raised winnerSelectedEvent would be null and our test would fail.
The pick winner method now looks as follows:
public void PickWinner()
{
if (CompetitionStatistics.CorrectAnswers.Count() == 0)
// if there are no winners, the event will not be called.
return;
CompetitionStatistics.Competition.Winner = GetCompetitionWinner();
DomainEvents.Raise(new WinnerSelectedEvent(CompetitionStatistics.Competition));
}
With this third iteration of the PickWinner method we've removed the extra interface and the factory method.
Note that in the event of no correct answers we don't raise the event.
The WinnerSelectedEvent is a simple class that holds the required data. This will be captured by the event and will perform the desired action.
public class WinnerSelectedEvent : IDomainEvent
{
public Competition Competition { get; set; }
public WinnerSelectedEvent(Competition competition)
{
Competition = competition;
}
}
The Domain Events Class
The key element that holds this together is the DomainEvents class. Let's examine this class:
public static class DomainEvents
{
/// Marked as ThreadStatic that each thread has its own callbacks</remarks>
[ThreadStatic]
private static List<Delegate> actions;
/// Registers the specified callback for the given domain event.
public static void Register<T>(Action<T> callback) where T : IDomainEvent
{
if (actions == null)
actions = new List<Delegate>();
actions.Add(callback);
}
/// Raises the specified domain event and calls the event handlers.
public static void Raise<T>(T domainEvent) where T : IDomainEvent
{
if (actions != null)
foreach (var action in actions)
if (action is Action<T>)
((Action<T>)action)(domainEvent);
}
}
This acts as a simple shell that allows us to register what actions should be raised when the Raise method is called.
Later in this series we will enhance this class to use an IoC container as it's designed to work with tests in its current form. We will need to add a method of injecting one or more IDomainEventHandler instances that will be called via the Handle method as defined by the following interface:
public interface IDomainEventHandler<T> where T : IDomainEvent
{
void Handle(T domainEvent);
}
The code for this post can be found at this Github branch.
In the next post we will finish designing our domain model. See the RSS subscription links at the bottom of the page to follow this series.