Domain Driven Design with Web API extensions part 4: domain events in code
November 2, 2015 1 Comment
Introduction
In the previous post we looked at some theory behind domain events. We said that domain events were events that happen within a domain and that other components of the system may be interested in. The domain becomes the publisher – or producer – and the listeners will be the subscribers, or consumers of the domain messages.
We established that we’d solve the communication of consumers and subscribers through a mediator which stands in between. Publishers and subscribers will stay decoupled that way.
In this post we’ll implement the theory in C# code.
The mediator
Open the WebSuiteDDD demo project and locate the WebSuiteDDD.SharedKernel layer. Add a folder called DomainEvents to it.
Let’s first add an abstraction for all domain event handlers, i.e. the subscribers, to that folder:
public interface IDomainEventHandler { void Handle(EventArgs eventArgs); }
There’s only one method called Handle which accepts an object ob type EventArgs from the standard .NET library. EventArgs are normally associated with events and delegates in .NET. Here we’ll try to approximate those.
Next let’s add the mediator called DomainEventMediator:
public class DomainEventMediator { private static readonly DomainEventMediator _instance = new DomainEventMediator(); private static List<IDomainEventHandler> _domainEventHandlers; private DomainEventMediator() { } public static DomainEventMediator Instance { get { return _instance; } } public static void RegisterDomainEventHandler(IDomainEventHandler domainEventHandler) { if (_domainEventHandlers == null) { _domainEventHandlers = new List<IDomainEventHandler>(); } _domainEventHandlers.Add(domainEventHandler); } public static void RaiseEvent(EventArgs eventArgs) { if (_domainEventHandlers != null && _domainEventHandlers.Any()) { foreach (var eventHandler in _domainEventHandlers) { eventHandler.Handle(eventArgs); } } } }
We keep the members static so that all callers will work with the same instance. The instance is built using a singleton. Then you’ll see the methods for registering the event handlers and raising an event. The registration function simply adds a new IDomainEventHandler to its internal static list of handlers. The RaiseEvent method accepts an EventArgs object and calls upon each subscribers to handle the event as they wish.
Next let’s implement IDomainEventHandler. The following implementation can be inserted into the Implementations folder of the WebSuiteDemo.Loadtesting.Application layer. The code will be familiar to you from the email decorator we saw in a previous post of this extension series:
public class TimetableChangedEmailEventHandler : IDomainEventHandler { private readonly IEmailService _emailService; public TimetableChangedEmailEventHandler(IEmailService emailService) { if (emailService == null) throw new ArgumentNullException("Email service"); _emailService = emailService; } public void Handle(EventArgs eventArgs) { TimetableChangedEventArgs e = eventArgs as TimetableChangedEventArgs; if (e != null) { AddOrUpdateLoadtestsValidationResult addOrUpdateValidationResult = e.AddOrUpdateLoadtestsValidationResult; if ((addOrUpdateValidationResult.ToBeInserted.Any() || addOrUpdateValidationResult.ToBeUpdated.Any()) && !addOrUpdateValidationResult.Failed.Any()) { EmailArguments args = new EmailArguments("Load tests added or updated", "Load tests added or updated", "The Boss", "The developer", "123.456.678"); _emailService.SendEmail(args); } } } }
…where TimetableChangedEventArgs is an object in the Domain layer. Add a folder called DomainEvents to the Domain layer with the following class:
public class TimetableChangedEventArgs : EventArgs { private AddOrUpdateLoadtestsValidationResult _addOrUpdateLoadtestsValidationResult; public TimetableChangedEventArgs(AddOrUpdateLoadtestsValidationResult addOrUpdateLoadtestsValidationResult) { if (addOrUpdateLoadtestsValidationResult == null) throw new ArgumentNullException("AddOrUpdateLoadtestsValidationResult"); _addOrUpdateLoadtestsValidationResult = addOrUpdateLoadtestsValidationResult; } public AddOrUpdateLoadtestsValidationResult AddOrUpdateLoadtestsValidationResult { get { return _addOrUpdateLoadtestsValidationResult; } } }
The TimetableChangedEventArgs object needs an AddOrUpdateLoadtestsValidationResult which if you recall will be available from the Timetable domain class.
Here comes the updated AddOrUpdateLoadtests method of Timetable.cs. Take note of how we call the mediator class towards the end of the function:
public AddOrUpdateLoadtestsValidationResult AddOrUpdateLoadtests(IList<Loadtest> loadtestsAddedOrUpdated) { List<Loadtest> toBeInserted = new List<Loadtest>(); List<Loadtest> toBeUpdated = new List<Loadtest>(); List<Loadtest> failed = new List<Loadtest>(); StringBuilder resultSummaryBuilder = new StringBuilder(); string NL = Environment.NewLine; foreach (Loadtest loadtest in loadtestsAddedOrUpdated) { Loadtest existing = (from l in Loadtests where l.Id == loadtest.Id select l).FirstOrDefault(); if (existing != null) //update { LoadtestValidationSummary validationSummary = OkToAddOrModify(loadtest); if (validationSummary.OkToAddOrModify) { existing.Update (loadtest.Parameters, loadtest.AgentId, loadtest.CustomerId, loadtest.EngineerId, loadtest.LoadtestTypeId, loadtest.ProjectId, loadtest.ScenarioId); toBeUpdated.Add(existing); resultSummaryBuilder.Append(string.Format("Load test ID {0} (update) successfully validated.{1}", existing.Id, NL)); } else { failed.Add(loadtest); resultSummaryBuilder.Append(string.Format("Load test ID {0} (update) validation failed: {1}{2}.", existing.Id, validationSummary.ReasonForValidationFailure, NL)); } } else //insertion { LoadtestValidationSummary validationSummary = OkToAddOrModify(loadtest); if (validationSummary.OkToAddOrModify) { Loadtests.Add(loadtest); toBeInserted.Add(loadtest); resultSummaryBuilder.Append(string.Format("Load test ID {0} (insertion) successfully validated.{1}", loadtest.Id, NL)); } else { failed.Add(loadtest); resultSummaryBuilder.Append(string.Format("Load test ID {0} (insertion) validation failed: {1}{2}.", loadtest.Id, validationSummary.ReasonForValidationFailure, NL)); } } } AddOrUpdateLoadtestsValidationResult validationResult = new AddOrUpdateLoadtestsValidationResult(toBeInserted, toBeUpdated, failed, resultSummaryBuilder.ToString()); TimetableChangedEventArgs args = new TimetableChangedEventArgs(validationResult); DomainEventMediator.RaiseEvent(args); return validationResult; }
The last bit if code is the actual registration. We’ll do that in Global.asax of the Web API layer. Locate that file and add the following call just before GlobalConfiguration.Configure(WebApiConfig.Register):
DomainEventMediator.RegisterDomainEventHandler(new TimetableChangedEmailEventHandler(new FakeEmailService()));
That’s all actually, we can test our code. Start the demo application and try adding a couple of load tests like we saw in this post using a simple console application. It’s a good idea to place break points at various places in the demo project so that you can follow how events are raised and handled. If everything goes well then you’ll get the same fake email sending output in the Debug window as before with the decorator:
From: The developer, to: The Boss, message: Load tests added or updated, server: 123.456.678, subject: Load tests added or updated
We’ve now seen a possible solution to raising and handling domain events. If we want to register another listener then we implement the IDomainEventHandler interface and call the DomainEventMediator.RegisterDomainEventHandler like above. The Timetable domain won’t know about it and won’t care either. The mediator will have no knowledge of the concrete implementation of the IDomainEventHandler that’s added to its list of event handlers.
We’ll actually do that in the upcoming posts in this DDD extension series. We’ll keep looking into messaging. Our current solution demonstrates how elements within the same solution can communicate with each other. We’ll go a step further and see how independent applications can be publishers and subscribers. We’ll see a solution for how the domain event can be propagated to a simulated financial module that will recalculate the expected profits from the updated and inserted load tests.
Read the next part here.
View the list of posts on Architecture and Patterns here.
Pingback: Domain Driven Design with Web API extensions part 4: domain events in code | Dinesh Ram Kali.