Use domain events to explicitly implement side effects of changes within your domain. In other words, and using DDD terminology, use domain events to explicitly implement side effects across multiple aggregates.
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.
The domain events and their side effects (the actions triggered afterwards that are managed by event handlers) should occur almost immediately, usually in-process, and within the same domain.
It's important to ensure that, just like a database transaction, either all the operations related to a domain event finish successfully or none of them do.
Figure below shows how consistency between aggregates is achieved by domain events. When the user initiates an order, the Order Aggregate sends an OrderStarted domain event. The OrderStarted domain event is handled by the Buyer Aggregate to create a Buyer object in the ordering microservice (bounded context). Please read Domain Events for more details.
Define, publish, and subscribe to events using IPublisher and IHandler.
1. Define an Event
publicclassCustomerCreated:IDomainEvent{publicstringName{get;set;}}2. Create a Handler
publicclassCustomerCreatedHandler:IHandler<CustomerCreated>{publicTaskHandleAsync(CustomerCreated@event){Console.WriteLine($"Customer created: {@event.Name}");returnTask.CompletedTask;}}3. Register Services
services.AddDomainEvents(typeof(CustomerCreatedHandler).Assembly);4. Publish Events
varpublisher=serviceProvider.GetRequiredService<IPublisher>();awaitpublisher.RaiseAsync(newCustomerCreated{Name="John Doe"});Raise events automatically from domain aggregates using Castle DynamicProxy interception.
1. Define an Event
publicclassOrderPlaced:IDomainEvent{publicstringOrderId{get;set;}publicdecimalAmount{get;set;}}2. Create an Aggregate (Publisher)
publicclassOrderAggregate:Aggregate{publicvoidPlaceOrder(decimalamount){// Business logic here...var@event=newOrderPlaced{OrderId=Guid.NewGuid().ToString(),Amount=amount};Raise(@event);}}publicclassWarehouseAggregate:Aggregate,ISubscribes<OrderPlaced>{publicTaskHandleAsync(OrderPlaced@event){Console.WriteLine($"Order created: {@event.OrderId}");returnTask.CompletedTask;}}3. Register Services
services.AddDomainEvents(typeof(OrderPlacedHandler).Assembly);4. Create Aggregate and Raise Event
varfactory=serviceProvider.GetRequiredService<IAggregateFactory>();varorder=awaitfactory.CreateAsync<OrderAggregate>();order.PlaceOrder(100.00m);// Event is automatically dispatched to handlers┌────────────────────────────────────────────────────────────────────────────────┐
│ PUBLISHING PHASE │
│ (Aggregate.Raise() → Queue.Enqueue) │
└────────────────────────────────────────────────────────────────────────────────┘
Aggregate.Raise()
│
▼
┌───────────┐ ┌───────────┐ ┌────────────┐ ┌────────────┐
│ Aggregate │────▶│Interceptor│────▶│ Middleware│────▶│ Dispatcher │
│ │ │ (Proxy) │ │(OnDispatch)│ │ │
└───────────┘ └───────────┘ └────────────┘ └─────┬──────┘
│
▼
┌───────────────┐
│ Queue │
│ (In-Memory) │
└───────────────┘
┌────────────────────────────────────────────────────────────────────────────────┐
│ SUBSCRIPTION PHASE │
│ (Queue → Listener → Handler) │
└────────────────────────────────────────────────────────────────────────────────┘
Queue notifies Listener
│
▼
┌───────────┐ ┌────────────┐ ┌───────────┐ ┌───────────┐
│ Listener │────▶│ Middleware │────▶│ Resolver │────▶│ Handler │
│ │ │(OnHandling)│ │ │ │ │
└───────────┘ └────────────┘ └─────┬─────┘ └───────────┘
│
▼
┌──────────────────┐
│ IHandler<T> │
│ ISubscribes<T> │
│ (includes │
│ aggregates) │
└──────────────────┘
- PUBLISHING PHASE - Aggregate.Raise() → Interceptor → Middleware.OnDispatching() → Dispatcher → Queue.Enqueue() → Middleware.OnDispatched()
- SUBSCRIPTION PHASE - Queue notifies Listener → Middleware.OnHandling() → Resolver (finds handlers) → Handler.HandleAsync() (includes ISubscribes) → Middleware.OnHandled()
Note:ISubscribes - Aggregates can implement ISubscribes to handle events they raise. The proxy ensures both business logic AND handler execute.
Components:
- Aggregate - Domain aggregate that raises events via
Raise()orRaiseAsync(). Can also implementISubscribes<TEvent>. - Interceptor - Castle DynamicProxy that intercepts
Raise()/RaiseAsync()and dispatches events. - Middleware - Custom plugins:
OnDispatching,OnDispatched,OnHandling,OnHandled. - Dispatcher - Enqueues events to the queue.
- Queue - In-memory queue (fire-and-forget).
- Listener - Processes events from queue asynchronously.
- Resolver - Resolves handlers for events.
- Handler - Handles events:
IHandler<T>orISubscribes<T>.
Custom plugins that run at various points in the event pipeline:
publicclassMyMiddleware:IEventMiddleware{publicTask<bool>OnDispatchingAsync(EventContextcontext){// Runs before event is dispatchedreturnTask.FromResult(true);}publicTaskOnDispatchedAsync(EventContextcontext){// Runs after event is dispatchedreturnTask.CompletedTask;}publicTask<bool>OnHandlingAsync(EventContextcontext){// Runs before each handler processes the eventreturnTask.FromResult(true);}publicTaskOnHandledAsync(EventContextcontext){// Runs after each handler processes the eventreturnTask.CompletedTask;}}Registration:
services.AddDomainEvents(assembly);// auto-registers handlers and middlewares which have parameter-less constructor. For types with parameterized constructor, you need to explicitly register as below. services.AddSingleton<IEventMiddleware,MyMiddleware>();The IAggregateFactory provides multiple methods to create proxied aggregates:
| Method | Description |
|---|---|
CreateAsync<T>() | Creates proxy using default constructor |
CreateAsync<T>(params object[]) | Creates proxy with specified constructor arguments |
CreateAsync(Type, params object[]) | Non-generic version with constructor arguments |
CreateFromInstanceAsync<T>(T aggregate) | Wraps existing aggregate instance in proxy |
CreateFromServiceProviderAsync<T>() | Resolves from DI and wraps in proxy (auto-resolves constructor dependencies) |
CreateFromServiceProviderAsync(Type) | Non-generic version resolving from DI |
Example - Using CreateFromServiceProviderAsync:
// Register aggregate with DI (constructor dependencies auto-resolved)services.AddTransient<OrderAggregate>();varfactory=serviceProvider.GetRequiredService<IAggregateFactory>();// Creates proxy, resolves OrderAggregate from DI, wraps in proxyvarorder=awaitfactory.CreateFromServiceProviderAsync<OrderAggregate>();order.PlaceOrder(100.00m);// Events dispatched automaticallyNote: When using CreateFromServiceProviderAsync, all constructor dependencies must be registered with the IoC container. The factory uses reflection to find the constructor with most parameters and resolves them from the service provider.
| Interface | Purpose |
|---|---|
IDomainEvent | Marker interface for domain events |
IHandler<TEvent> | Async handler interface |
ISubscribes<TEvent> | Aggregate handler interface (implemented by aggregates to handle their own events) |
IPublisher | Interface for raising events |
IAggregateFactory | Factory for creating proxied aggregates |
IEventMiddleware | Plugin for event pipeline |
IEventQueue | In-flight event queue |
- Package ID:
Dormito.DomainEvents - Target Frameworks: netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0
- License: MIT
