Domain Event pattern with in-built .Net Core IoC Container, scrutor and .Net Core console application. Source code for this post can be found here
This post focuses on the abstract of Domain Event in Domain Driven Design (DDD) which is also a fundamental building block of microservices or eventing system.
An event is something that has happened in the past. A domain event is, something that happened in the domain that you want other parts of the same domain (in-process) to be aware of. The notified parts usually react somehow to the events.
Dependency Injection does not eliminate tight coupling
While this post is not specific to microservices, the domain event pattern can be implemented in any system to reduce tight coupling between your services. An example would be an MVC controller with a lot of dependencies, services or repositories injected via constructor injection.
Adapting Udi Dahan's Domain Events Salvation post to .Net Core implementation.
Raising Events with side effects within a system
Without injecting any dependencies at compile time, any part of a system can have task completed without having prior knowledge of handler that would handle the commands. Some of the example we would look at includes, completing an order by raising an event and having the proper handler located which will handle the event such as writing the order to a data store or sending an email to the customer.
In the interest of time we'll talk about a domain object and we'll see how that interacts with the system and you may refer to the source code further to see other examples or feel free to leave any questions.
The Domain Object
public class Order
{
public Guid OrderId { get; private set; }
public DateTime OrderDate { get; private set; }
public int NumberOfItems { get; set; }
public string OrderName { get; private set; }
public Order(int numberOfItems, string orderName)
{
OrderId = Guid.NewGuid();
OrderDate = DateTime.UtcNow;
NumberOfItems = numberOfItems;
OrderName = orderName;
}
public void OrderComplete()
{
DomainEvent.Raise(new OrderCompletedEvent(DateTime.UtcNow, this));
}
}
The order domain object has a behavior such that when an order is created, it broadcasts an OrderCompletedEvent event of itself to the DomainEvent or Dispatcher and any handler that knows how to handle the event will respond and handles it. These handlers can also be referred to as Event Listeners or Subscribers.
The event is raised via the DomainEvent which is a static class. DomainEvent static class knows about all of the event handler and able to locate and invoke the right event handler for a given event. More about the DomainEvent shortly.
The OrderCompletedEvent
public class OrderCompletedEvent : IDomainEvent
{
public DateTime OrderCreatedDate { get; private set; }
public Order Order { get; private set; }
public OrderCompletedEvent(DateTime orderCreatedDate, Order order)
{
OrderCreatedDate = orderCreatedDate;
Order = order;
}
}
The OrderCompletedEvent class implements an interface without any contract to implement merely decorating the class as a type called IDomainEvent.
public interface IDomainEvent{}
The OrderCompletedEvent has an instance of the Order object and it's duty is to transport the Order object along with any needed property or behavior to the handler.
The DomainEvent
The DomainEvent is responsible for some behaviors and they include
Registering handler manually via its Register method
Forwarding an event to the proper handler by iterating over all handlers for a particular type resolved from the IoC container
An IoC container is not required but imagine if you have to manually register tens of handlers in your system.
public static class DomainEvent
{
[ThreadStatic]
private static List<Delegate> _actions;
public static IServiceProvider _serviceProvider { get; set; }
public static void Register<T>(Action<T> callback) where T : IDomainEvent
{
_actions = _actions ?? new List<Delegate>();
_actions.Add(callback);
}
public static void ClearCallbacks()
{
_actions = null;
}
public static void Raise<T>(T args) where T : IDomainEvent
{
if (_serviceProvider != null)
{
//Fetch all handler of this type from the IoC container and invoke their handle method.
foreach (var handler in (IEnumerable<IDomainHandler<T>>)_serviceProvider
.GetService(typeof(IEnumerable<IDomainHandler<T>>)))
{
handler.Hanle(args);
}
}
if (_actions != null)
{
foreach (var action in _actions)
{
if (action is Action<T>)
{
((Action<T>) action)(args);
}
}
}
}
}
The generic Raise<T> method in the DomainEvent static class iterates through all the handler using the resolved types from the IoC container. The DomainEvent has to be bootstrapped on application startup taking in an instance of the IoC container or resolver. In the case of this application, takes an instance of ServiceProvider built from type registration on the instance of ServiceCollection. Don't pay attention to the ServiceProvider and ServiceCollection as they specific to .Net Core and you can choose to use any IoC container of your choice.
The Event Handlers
The EventHandlers go into a separate project that mimics the Onion architecture. According to the onion architecture, the handlers fits into the Infrastructure layer as they most likely will interact with other services and data stores or carry out some infrastructure related tasks. You get the gist.
OrderCompleteHandler.cs
public class OrderCompleteHandler : IDomainHandler<OrderCompletedEvent>
{
public void Hanle(OrderCompletedEvent @event)
{
@event.Order.NumberOfItems = 100;
Console.WriteLine($"Order Information: \n=======================" +
$"\nOrder completed on {@event.OrderCreatedDate.ToShortDateString()} " +
$"at {@event.OrderCreatedDate:HH:mm:ss tt}\nID: {@event.Order.OrderId}" +
$"\nNumber of order items: {@event.Order.NumberOfItems}" +
$"\n____________________________________");
}
}
The event handler in this case, OrderCompleteHandler knows to listens for and knows how to handle OrderCompletedEvent we talked about earlier above. All handlers implement a generic IDomainHandler<T> which describes the general signature of an event handler. It will make more sense when we talk about gluing the whole pieces together next.
public interface IDomainHandler<T> where T : IDomainEvent
{
void Hanle(T @event);
}
Putting it all together
Rather than registering each handler or subscriber on demand as they are created, an IoC container best suit this scenario by registering all types that implements IDomainHandler<T>. For this purpose, Scrutor has been used. Scrutor can tie itself into the .Net Core IoC container which provides extensibility to register decorators currently not possible with the in-built .Net Core IoC.
Since a .Net Core console application is used as our client, the bootstrap and type registration is setup in the main method of the console app's Program.cs class.
class Program
{
static void Main(string[] args)
{
//Set up the DI
//Scan the assembly and register types that implements IDomainHandler interface with .Net Core IoC
var serviceProvider = new ServiceCollection()
.Scan( scan => scan
.FromAssemblyOf<StudentRegisteredHandler>()
.AddClasses(classes =>
classes.AssignableTo(typeof(IDomainHandler<>)))
.AsImplementedInterfaces()
).BuildServiceProvider();
DomainEvent._serviceProvider = serviceProvider;
//Create an order
var newOrder = new Order(5, "Amazon");
//Raise an order completed event
newOrder.OrderComplete();
var studentReg = new Student(Title.Mr, "John", "Murphy");
studentReg.RegisterStudent();
var emailSent =
new Email("recipient@contact.com",
"sender@contact.com", "Event Message Subject",
"This is a short email to say thank you!");
emailSent.RaiseEmailSent();
Console.ReadKey();
}
}
Scrutor provides an extension method, Scan on the IServiceCollection interface. using Scrutor, the assembly of type that implements IDomainHandler<T> are scanned and registered with the .Net Core IoC container which becomes automatically available to the DomainEvent static class which its Raise<T> method uses to locate the proper handler for a particular event.
This post mainly focused on one example of raising an event from a Domain object using a DomainEvent static class which its Raise method taps into an IoC container to locate the handler. The output below shows three samples of events that are raised and handled. Check out the source code for reference.
Order Information:
=======================
Order completed on 09/14/2019 at 17:04:30 PM
ID: 07fa61ed-d5b4-4ed4-9ee1-30e4971972e8
Number of order items: 100
____________________________________
Mr. John Murphy registration is now complete
_____________________________
Email to the address recipient@contact.com has been sent!
_________________________
Relationship to other patterns
- Event Driven Architecture: Event driven architecture provides higher decoupling of your system
- CQRS: Command Query Responsibility Segregation (CQRS) uses eventing to boost decoupling as well and its a very popular pattern. A very good implementation of the CQRS is the .net MediatR library.
Hopefully, you have learned something today.
Happy coding..