-
Notifications
You must be signed in to change notification settings - Fork 6
Domain events
CoreDdd supports domain events to announce that some domain activity has happened. A domain event is raised from a domain method on a domain entity. Domain event handlers can execute arbitrary code to handle the domain event. Domain event handlers which publish event messages over a message bus allows to defer some application processing to a later time, away from the current database transaction. Some processing does not have to be necessarily part of the current database transaction, and can be done later (e.g. sending email, subsequent domain processing, web service notification, etc). This allows the database transaction to be smaller and faster. Smaller and faster database transactions allow better application performance. In a case of a web server, it allows higher throughput and handling more web requests per second.
CoreDdd domain event needs to implement IDomainEvent
interface. Example of a domain event:
public class ShipCargoPolicyItemAddedDomainEvent : IDomainEvent
{
public int PolicyId { get; set; }
public int ShipId { get; set; }
}
Example of raising a domain event from a domain code:
public class Policy : Entity, IAggregateRoot
{
...
public virtual void AddShipCargoPolicyItem(ShipCargoPolicyItemArgs args)
{
var shipCargoPolicyItem = new ShipCargoPolicyItem(args);
_items.Add(shipCargoPolicyItem);
DomainEvents.RaiseEvent(new ShipCargoPolicyItemAddedDomainEvent
{
PolicyId = Id,
ShipId = args.Ship.Id
});
}
...
}
The last bit is to implement a domain event handler(s) (there can be multiple domain event handlers for one domain event):
public class ShipCargoPolicyItemAddedDomainEventHandler : IDomainEventHandler<ShipCargoPolicyItemAddedDomainEvent>
{
public void Handle(ShipCargoPolicyItemAddedDomainEvent domainEvent)
{
// handle the domain event here
}
}
The code sample above is available here.
CoreDdd supports two ways of domain event handling:
- immediate handling of domain events when raised
- delayed handling of domain events
The domain event handlers are executed immediately when the domain event is raised from the domain code running in a database transaction. Again, a domain event handler should not do much - for instance it should not do any database activity or any long processing, as it is executed as a part of the database transaction.
The following code initializes the domain events for an immediate handling when raised:
var domainEventHandlerFactory = ioCContainer.Resolve<IDomainEventHandlerFactory>();
DomainEvents.Initialize(domainEventHandlerFactory);
When immediate domain event handling when raised is used in a web application, is it expected that transaction scope wrappers TransactionScopeUnitOfWorkDependencyInjectionMiddleware
for ASP.NET Core, and TransactionScopeUnitOfWorkHttpModule
for ASP.NET are used.
The domain event handlers are not executed immediately when the domain event is raised from the domain code running in a database transaction, but are raised after the database transaction commits, by explicitly calling DomainEvents.RaiseDelayedEvents();
. A domain event handler should not do much - for instance it should not do any database activity or any long processing. Even though it is executed after the database transaction, a long running event handler would cause an overall slow web request handling.
The following code initializes the domain events for delayed handling:
var domainEventHandlerFactory = ioCContainer.Resolve<IDomainEventHandlerFactory>();
DomainEvents.Initialize(domainEventHandlerFactory, isDelayedDomainEventHandlingEnabled: true);
When delayed handling of domain events is used in a web application, it is expected that database transaction wrappers UnitOfWorkDependencyInjectionMiddleware
for ASP.NET Core, and UnitOfWorkHttpModule
for ASP.NET are used, which automatically calls DomainEvents.RaiseDelayedEvents();
after the main database transaction commits.
Domain event handler factory (a class implementing IDomainEventHandlerFactory
) is needed to initialize domain events. Either implement the factory manually, or register IDomainEventHandlerFactory
as a factory and resolve it from your favourite IoC container. When resolving it from an IoC container, you need to register the domain event handlers first.
Castle.Windsor example:
_castleWindsorIoCContainer.Register(
Classes
.FromAssemblyContaining<ShipCargoPolicyItemAddedDomainEvent>()
.BasedOn(typeof(IDomainEventHandler<>))
.WithService.FirstInterface()
.Configure(x => x.LifestyleTransient()));
Ninject example:
_ninjectIoCContainer.Bind(x => x
.FromAssemblyContaining<ShipUpdatedDomainEventHandler>()
.SelectAllClasses()
.InheritedFrom(typeof(IDomainEventHandler<>))
.BindAllInterfaces()
.Configure(y => y.InTransientScope()));
Dependency Injection example (needs Scrutor nuget package):
services.Scan(scan => scan
.FromAssemblyOf<ShipUpdatedDomainEventHandler>()
.AddClasses(classes => classes.AssignableTo(typeof(IDomainEventHandler<>)))
.AsImplementedInterfaces()
.WithTransientLifetime()
);
Ideally a domain event handler should just publish an event message over a message bus notifying that something has happened, and subscribers of the message will handle the message, possibly in a different process or machine. Some message bus libraries like Rebus or NServiceBus allow to enlist the message publishing into an ambient transaction (TransactionScope
) together with the database connection, making sure messages will not be published from domain event handlers in a case of an exception (e.g. database deadlock, SQL error, generic application exception, etc). This scenario is supported by CoreDdd when using immediate handling of domain events when raised.
Here are pros and cons of immediate and delayed domain event handling.
Immediate domain event handling
Pros:
- when using an ambient transaction (
TransactionScope
), and when a domain event handler just publishes a message over a message bus where both the database connection and the message publishing (e.g. Rebus, NServiceBus) are enlisted into the ambient transaction, they both succeed or both fail (the published messages will not be published when the main database transaction fails). On top of that, when strong reliability is needed which can withstand hardware crashes, NServiceBus can be used for the message publishing together with Outbox feature (cross platform) or with MSMQ and MSDTC (Windows only with full .NET framework - not .NET Core).
Cons:
- domain event handlers are executed as a part of the database transaction
- when using an ambient transaction (
TransactionScope
), and the machine crashes right after the database transaction commit, but before the message publishing commits (when not using NServiceBus Outbox or MSMQ/MSDTC, see above), the event messages will not be published and will be lost (more info about this here)
Delayed domain event handling
Pros:
- domain event handlers are executed after the database transaction is committed, making the database transaction shorter
Cons:
- if the machine crashes right after the database transaction commit, the domain event handlers will not be executed; if those domain event handlers publish event messages over a message bus, no event message would be published
- if the domain event handlers are publishing event messages, and the machine crashes in the middle of the domain event handlers execution, only some event messages (not all of them) would be published