Domain Events¶
Domain events represent that something happened in the domain model. Domain events capture the changes in domain model state. Following are the characteristics of domain events:
- Immutable: Once created, they cannot change the properties.
- Represents domain model state: They indicate modifications in the state of a domain entity.
- Event name in past tense: The use of past tense in naming domain events is essential, as it signifies that a particular event has already occurred.
The Crystal Sharp framework provides a seamless way for creating and triggering domain events. Refer to the code snippet below for an illustration of how to create a domain event:
public class OrderPlacedDomainEvent : DomainEvent
{
public string OrderCode { get; set; }
public decimal TotalPrice { get; set; }
public OrderPlacedDomainEvent(Guid streamId, string orderCode, decimal totalPrice)
{
StreamId = streamId;
OrderCode = orderCode;
TotalPrice = totalPrice;
}
[JsonConstructor]
public OrderPlacedDomainEvent(Guid streamId,
string orderCode,
decimal totalPrice,
int entityStatus,
DateTime createdOn,
DateTime? modifiedOn,
long version)
{
StreamId = streamId;
OrderCode = orderCode;
TotalPrice = totalPrice;
EntityStatus = entityStatus;
CreatedOn = createdOn;
ModifiedOn = modifiedOn;
Version = version;
}
}
In the above code snippet, the OrderPlacedDomainEvent
class is inherited from the DomainEvent
base class. In the Crystal Sharp framework, it is mandatory that every domain event inherit from the DomainEvent
base class.
In the above OrderPlacedDomainEvent
class, there are two constructors. The first constructor will be called when a domain event triggers. In the first constructor, the streamId
parameter must be the GlobalUId
of an aggregate.
The second constructor, with the JsonConstructor
attribute, is called during the deserialization of a domain event. An important point about the second constructor is that here are four additional parameters: entityStatus
, createdOn
, modifiedOn
, and version
are used internally during the deserialization when replaying events.
Every domain event class must have both constructors, one for triggering a domain event and a second constructor with the JsonConstructor
attribute with four additional parameters for deserialization.
To raise a domain event from an aggregate, a Raise
method is available from the AggregateRoot<TKey>
base class. The following code snippet presents an illustration of how to raise a domain event from an aggregate:
public class Order : AggregateRoot<int>
{
public string OrderCode { get; private set; }
public decimal TotalPrice { get; private set; }
public static Order PlaceOrder(string orderCode, decimal totalPrice)
{
Order order = new() { OrderCode = orderCode, TotalPrice = totalPrice };
order.Raise(new OrderPlacedDomainEvent(order.GlobalUId, order.orderCode, order.totalPrice));
return order;
}
public void ChangeTotalPrice(decimal totalPrice)
{
TotalPrice = totalPrice;
Raise(new TotalPriceChangedDomainEvent(GlobalUId, TotalPrice));
}
}
In the above code snippet, the Order
aggregate is depicted with two methods: PlaceOrder
and ChangeTotalPrice
. The utilization of the Raise
method from the base class is evident as it serves to trigger a domain event.
“Apply” Method - When, where and why?¶
When an aggregate participates in event sourcing and utilizes event stores provided by the Crystal Sharp framework, the presence of the Apply
method becomes mandatory. To fulfill this requirement, each aggregate must include an Apply
method for every domain event. The following code snippet illustrates the implementation of the Apply
method in aggregate:
public class Order : AggregateRoot<int>
{
public string OrderCode { get; private set; }
public decimal TotalPrice { get; private set; }
public static Order PlaceOrder(string orderCode, decimal totalPrice)
{
Order order = new() { OrderCode = orderCode, TotalPrice = totalPrice };
order.Raise(new OrderPlacedDomainEvent(order.GlobalUId, order.orderCode, order.totalPrice));
return order;
}
public void ChangeTotalPrice(decimal totalPrice)
{
TotalPrice = totalPrice;
Raise(new TotalPriceChangedDomainEvent(GlobalUId, TotalPrice));
}
public void Apply(OrderPlacedDomainEvent @event)
{
OrderCode = @event.OrderCode;
TotalPrice = @event.TotalPrice;
}
public void Apply(TotalPriceChangedDomainEvent @event)
{
TotalPrice = @event.TotalPrice;
}
}
In the above code snippet, the Order
aggregate is presented with two overloads of the Apply
method. These overloads are intended to handle different types of domain events, namely OrderPlacedDomainEvent
and TotalPriceChangedDomainEvent
. It is essential to note that the Apply
method should always include a parameter that matches the domain event type, as illustrated in the provided code.
Domain Event Handlers¶
Domain event handlers are used to handle domain events. A domain event can have one or multiple handlers. To implement the domain event handler class, a class must implement the Handle
method of the ISynchronousDomainEventHandler<TNotification>
interface; here, generic TNotification
is the domain event that is being handled.
IMPORTANT
Domain event handlers are designed to be triggered only after the data has been successfully stored in the database or event store.
The following code snippet illustrates how to create a domain event handler:
public class OrderPlacedDomainEventHandler : ISynchronousDomainEventHandler<OrderPlacedDomainEvent>
{
private readonly ILogger _logger;
public OrderPlacedDomainEventHandler(ILogger logger)
{
_logger = logger;
}
public async Task Handle(OrderPlacedDomainEvent notification, CancellationToken cancellationToken = default)
{
_logger.Log($"Order code: {notification.OrderCode}");
_logger.Log($"Total price: {notification.TotalPrice}");
}
}
In the above code snippet, the OrderPlacedDomainEventHandler
class implements ISynchronousDomainEventHandler<TNotification>
and its Handle(TNotification notification, CancellationToken cancellationToken = default)
method, where TNotification
is the domain event that is being handled. As in the above code snippet, TNotification
is replaced by OrderPlacedDomainEvent
.
It is possible that a domain event can have multiple handlers. The following code snippet illustrates two domain event handlers for OrderPlacedDomainEvent
, one for email and one for SMS:
public class OrderPlacedEmailDomainEventHandler : ISynchronousDomainEventHandler<OrderPlacedDomainEvent>
{
private readonly IMailService _mailService;
public OrderPlacedEmailDomainEventHandler(IMailService mailService)
{
_mailService = mailService;
}
public async Task Handle(OrderPlacedDomainEvent notification, CancellationToken cancellationToken = default)
{
string to = "some.email@some.server.com";
string subject = "New Order";
string message = $"New order received: {notification.OrderCode}";
await _mailService.Send(to, subject, message);
}
}
public class OrderPlacedSmsDomainEventHandler : ISynchronousDomainEventHandler<OrderPlacedDomainEvent>
{
private readonly ISmsService _smsService;
public OrderPlacedSmsDomainEventHandler(ISmsService smsService)
{
_smsService = smsService;
}
public async Task Handle(OrderPlacedDomainEvent notification, CancellationToken cancellationToken = default)
{
string to = "0123456789";
string message = $"New order received: {notification.OrderCode}";
await _smsService.Send(to, message);
}
}
In the above code snippets, both classes OrderPlacedEmailDomainEventHandler
and OrderPlacedSmsDomainEventHandler
handle the OrderPlacedDomain
domain event.