Uh oh!
There was an error while loading. Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork 1
Home
- Overview
- Architecture
- Core Concepts
- Getting Started
- Registration Methods
- Extension Points
- Auto-Registration
- API Reference
- Best Practices
- Troubleshooting
DomainEvents is a library for implementing transactional domain events in domain-driven design bounded contexts. It provides a robust infrastructure for raising, dispatching, and handling domain events within your application.
- Automatic Event Dispatching: Domain aggregates automatically dispatch events when
Raise()orRaiseAsync()is called - Middleware Pipeline: Hook into the event lifecycle with custom middleware
- Event Queue: Support for in-flight event queuing
- OpenTelemetry Integration: Built-in telemetry support
- Flexible Registration: Auto-discovery of handlers and middlewares
- Multiple Extension Points: Customize behavior at every layer
┌─────────────────────────────────────────────────────────────────────────────┐
│ Application Layer │
│ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Aggregate │ │ Publisher │ │
│ │ (Raise Event) │ │ (Manual Raise) │ │
│ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌─────────────────────────────────────────────┐ │
│ │ EventInterceptor (Proxy) │ │
│ │ - Castle DynamicProxy interception │ │
│ │ - OpenTelemetry tracking │ │
│ └─────────────────────┬───────────────────────┘ │
│ │ │
└────────────────────────┼──────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────────────────┐
│ Middleware Pipeline (Dispatch) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Middleware 1 │ │ Middleware 2 │ │ Middleware N │ │
│ │ OnDispatching │ │ OnDispatching │ │ OnDispatching │ │
│ │ OnDispatched │ │ OnDispatched │ │ OnDispatched │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ └────────────────────┼────────────────────┘ │
│ │ │
└────────────────────────────────┼──────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────────┐
│ Event Queue │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ InMemoryEventQueue │ │
│ │ - Enqueue events │ │
│ │ - Invoke subscription delegate on enqueue │ │
│ └─────────────────────────┬───────────────────────────┘ │
│ │ │
│ (delegate callback) │
└────────────────────────────┼──────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────────┐
│ EventListener │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ EventListener.ProcessEventAsync │ │
│ │ - Subscribes to queue via delegate │ │
│ │ - Processes events from queue │ │
│ └─────────────────────────┬───────────────────────────┘ │
│ │ │
└────────────────────────────┼──────────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────────┐
│ Middleware Pipeline (Handle) │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ Middleware 1 │ │ Middleware 2 │ │ Middleware N │ │
│ │ OnHandling │ │ OnHandling │ │ OnHandling │ │
│ │ OnHandled │ │ OnHandled │ │ OnHandled │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ └────────────────────┼────────────────────┘ │
│ │ │
└────────────────────────────────┼──────────────────────────────────────────────┘
│
▼
┌───────────────────────────────────────────────────────────────────────────────┐
│ Handler Layer │
│ ┌─────────────────────────────────────────────────────┐ │
│ │ Resolver │ │
│ │ - Resolves handlers for event type │ │
│ └─────────────────────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │ Handler 1 │ │ Handler 2 │ │ Handler N │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└───────────────────────────────────────────────────────────────────────────────┘
- Aggregate.Raise() - Aggregate raises an event
- EventInterceptor - Intercepts the call, proceeds with Raise, then dispatches event
- EventDispatcher.DispatchAsync() - Runs dispatch middleware, enqueues event
- InMemoryEventQueue - Stores event, invokes subscribed delegate immediately
- EventListener - Receives callback, processes event through handle middleware
- Resolver - Resolves handlers for the event type (includes
ISubscribes<T>implementations on aggregates) - Handler - Processes the event (either standalone
IHandler<T>or aggregate'sISubscribes<T>.HandleAsync())
Note: The dispatcher returns immediately after enqueueing (fire-and-forget). Event processing happens asynchronously via the queue subscription delegate.
Synchronous Phase (Aggregate.Raise → Queue.Enqueue):
- Aggregate raises event via
Raise()orRaiseAsync() - EventInterceptor intercepts and calls EventDispatcher
- Dispatch middleware runs (
OnDispatchingAsync) - Event is enqueued to queue
- Dispatched middleware runs (
OnDispatchedAsync) - Returns to caller (aggregate business logic completes)
- Aggregate raises event via
Asynchronous Phase (Queue → Handler):
- Queue notifies subscribed listener
- Listener processes through handle middleware (
OnHandlingAsync) - Resolver finds all handlers (including
ISubscribes<T>implementations) - Each handler's
HandleAsync()is called - Handle middleware runs (
OnHandledAsync)
Domain events represent something that happened in the domain that other parts need to be aware of:
publicclassCustomerCreated:IDomainEvent{publicstringCustomerId{get;set;}publicstringName{get;set;}publicDateTimeCreatedAt{get;set;}}Handlers process domain events:
publicclassCustomerCreatedHandler:IHandler<CustomerCreated>{publicTaskHandleAsync(CustomerCreated@event){// Process the eventConsole.WriteLine($"Customer created: {@event.Name}");returnTask.CompletedTask;}}Aggregates are domain objects that can raise events:
publicclassCustomerAggregate:Aggregate{publicvoidCreateCustomer(stringname){// Business logicvar@event=newCustomerCreated{CustomerId=Guid.NewGuid().ToString(),Name=name,CreatedAt=DateTime.UtcNow};Raise(@event);}}dotnet add package Dormito.DomainEventspublicclassOrderPlaced:IDomainEvent{publicstringOrderId{get;set;}publicdecimalAmount{get;set;}}publicclassOrderPlacedHandler:IHandler<OrderPlaced>{publicasyncTaskHandleAsync(OrderPlaced@event){// Send confirmation email, update inventory, etc.awaitSendConfirmationAsync(@event.OrderId);}privateTaskSendConfirmationAsync(stringorderId){// ImplementationreturnTask.CompletedTask;}}publicclassOrderAggregate:Aggregate{publicvoidPlaceOrder(decimalamount){// Business logic here...var@event=newOrderPlaced{OrderId=Guid.NewGuid().ToString(),Amount=amount};Raise(@event);}}Aggregates can implement ISubscribes<TEvent> to handle events they raise themselves:
publicclassOrderAggregate:Aggregate,ISubscribes<OrderPlaced>{publicTaskHandleAsync(OrderPlaced@event){// Handle the event within the same aggregateConsole.WriteLine($"Order placed: {@event.OrderId}");returnTask.CompletedTask;}publicvoidPlaceOrder(decimalamount){var@event=newOrderPlaced{OrderId=Guid.NewGuid().ToString(),Amount=amount};Raise(@event);}}Note: When an aggregate implements ISubscribes<TEvent>, the handler is called via the Resolver during the asynchronous event processing phase. This happens after the Raise() call completes (fire-and-forget pattern).
services.AddDomainEvents(typeof(OrderPlacedHandler).Assembly);publicclassOrderService{privatereadonlyIAggregateFactory_aggregateFactory;publicOrderService(IAggregateFactoryaggregateFactory){_aggregateFactory=aggregateFactory;}publicasyncTaskPlaceOrder(decimalamount){varorder=await_aggregateFactory.CreateAsync<OrderAggregate>();order.PlaceOrder(amount);// Event is automatically dispatched to handlers}}// Scan specific assemblyservices.AddDomainEvents(typeof(OrderPlacedHandler).Assembly);// Scan multiple assembliesservices.AddDomainEvents(typeof(OrderPlacedHandler).Assembly,typeof(CustomerCreatedHandler).Assembly);// Scan calling assemblyservices.AddDomainEvents();services.AddDomainEventsWithDispatcher<MyCustomDispatcher>(assembly);varcustomDispatcher=newMyCustomDispatcher();services.AddDomainEventsWithDispatcher(customDispatcher,assembly);services.AddDomainEventsWithTelemetry(assembly);varservices=newServiceCollection();// Register publisherservices.AddSingleton<IPublisher,Publisher>();// Register resolverservices.AddSingleton<IResolver>(sp =>newResolver(sp.GetServices<IHandler>()));// Register dispatcherservices.AddSingleton<IEventDispatcher,EventDispatcher>();// Register interceptorservices.AddSingleton<IEventInterceptor>(sp =>newEventInterceptor(sp.GetRequiredService<IEventDispatcher>()));// Register aggregate factoryservices.AddSingleton<IAggregateFactory,AggregateFactory>();// Register handlersservices.AddSingleton<IHandler,OrderPlacedHandler>();services.AddSingleton<IHandler,CustomerCreatedHandler>();// Register middlewaresservices.AddSingleton<IEventMiddleware,MyMiddleware>();Implement IEventDispatcher to customize how events are dispatched. The dispatcher runs dispatch middleware and enqueues events. Event processing is handled by the EventListener via queue subscription.
publicclassMyCustomDispatcher:IEventDispatcher{privatereadonlyIResolver_resolver;privatereadonlyIEventQueue_queue;privatereadonlyIEnumerable<IEventMiddleware>_middlewares;privatereadonlyILogger<MyCustomDispatcher>_logger;publicMyCustomDispatcher(IResolverresolver,IEventQueuequeue=null,IEnumerable<IEventMiddleware>middlewares=null,ILogger<MyCustomDispatcher>logger=null){_resolver=resolver;_queue=queue??newInMemoryEventQueue();_middlewares=middlewares??Enumerable.Empty<IEventMiddleware>();_logger=logger;}publicvoidDispatch(object@event){// Custom synchronous dispatch logicvarcontext=newEventContext(@event);DispatchWithMiddlewareAsync(context).GetAwaiter().GetResult();}publicasyncTaskDispatchAsync(object@event){varcontext=newEventContext(@event);awaitDispatchWithMiddlewareAsync(context);}privateasyncTaskDispatchWithMiddlewareAsync(EventContextcontext){// Run dispatch middleware (before)foreach(varmiddlewarein_middlewares){if(!awaitmiddleware.OnDispatchingAsync(context)){_logger?.LogDebug("Middleware skipped dispatching");return;}}// Enqueue event - EventListener will process via subscriptionawait_queue.EnqueueAsync(context);context.IsDispatched=true;// Run dispatch middleware (after)foreach(varmiddlewarein_middlewares){awaitmiddleware.OnDispatchedAsync(context);}}publicIEventQueueQueue=>_queue;}---
### Custom Event Queue
{awaitmiddleware.OnHandledAsync(context);}}}
public IEventQueue Queue => _queue;publicasyncTaskProcessQueueAsync(){while(_queue.Count>0){varcontext=await_queue.DequeueAsync();if(context!=null){awaitProcessEventAsync(context);}}}}Registration:
services.AddDomainEventsWithDispatcher<MyCustomDispatcher>(assembly);// Or with instancevardispatcher=newMyCustomDispatcher(resolver);services.AddDomainEventsWithDispatcher(dispatcher,assembly);Implement IEventQueue to create a custom queue (e.g., persistent queue, distributed queue):
publicclassMyCustomQueue:IEventQueue{privatereadonlyQueue<EventContext>_queue=newQueue<EventContext>();privateEventDequeuedHandler_handler;privatereadonlyobject_lock=newobject();publicTaskEnqueueAsync(EventContextcontext){lock(_lock){_queue.Enqueue(context);}// Immediately invoke the subscribed handler (fire-and-forget)_handler?.Invoke(context);returnTask.CompletedTask;}
#if NET8_0_OR_GREATERpublicTask<EventContext?>DequeueAsync()
#else
publicTask<EventContext>DequeueAsync()
#endif
{lock(_lock){if(_queue.Count>0){
#if NET8_0_OR_GREATERreturnTask.FromResult<EventContext?>(_queue.Dequeue());
#else
returnTask.FromResult(_queue.Dequeue());
#endif
}}
#if NET8_0_OR_GREATERreturnTask.FromResult<EventContext?>(null);
#else
thrownewInvalidOperationException("Queue is empty");
#endif
}publicIReadOnlyList<EventContext>PeekAll(){lock(_lock){return_queue.ToArray();}}publicvoidClear(){lock(_lock){_queue.Clear();}}publicintCount{get{lock(_lock){return_queue.Count;}}}publicvoidSubscribe(EventDequeuedHandlerhandler){_handler=handler;}}Key Points:
- The
Subscribemethod registers a delegate that gets called when events are enqueued - The delegate is invoked immediately in
EnqueueAsync(synchronous callback) - This enables fire-and-forget event processing
Registration:
services.AddDomainEvents(assembly);services.AddSingleton<IEventQueue,MyCustomQueue>();Implement IEventInterceptor to customize how aggregate methods are intercepted:
publicclassMyCustomInterceptor:IEventInterceptor{privatereadonlyIEventDispatcher_dispatcher;privatereadonlyILogger<MyCustomInterceptor>_logger;publicMyCustomInterceptor(IEventDispatcherdispatcher,ILogger<MyCustomInterceptor>logger=null){_dispatcher=dispatcher;_logger=logger;}publicvoidIntercept(IInvocationinvocation){varmethod=invocation.Method;// Check if it's a Raise or RaiseAsync methodif(!IsRaiseMethod(method)){invocation.Proceed();return;}var@event=invocation.Arguments[0];vareventType=@event.GetType();varmethodName=method.Name;varisAsync=methodName=="RaiseAsync";_logger?.LogDebug("Intercepted {MethodName} for {EventType}",methodName,eventType.Name);try{// Proceed with the original method (executes Raise body)invocation.Proceed();// Dispatch the eventif(isAsync){_dispatcher.DispatchAsync(@event).GetAwaiter().GetResult();}else{_dispatcher.Dispatch(@event);}}catch(Exceptionex){_logger?.LogError(ex,"Error dispatching event {EventType}",eventType.Name);throw;}}privatestaticboolIsRaiseMethod(MethodInfomethod){returnmethod.Name=="Raise"||method.Name=="RaiseAsync";}}Registration:
services.AddDomainEvents(assembly);services.AddSingleton<IEventInterceptor,MyCustomInterceptor>();Implement IResolver to customize how handlers are resolved:
publicclassMyCustomResolver:IResolver{privatereadonlyIEnumerable<IHandler>_handlers;privatereadonlyDictionary<Type,List<IHandler>>_handlerCache;publicMyCustomResolver(IEnumerable<IHandler>handlers){_handlers=handlers;_handlerCache=newDictionary<Type,List<IHandler>>();// Build handler cacheforeach(varhandlerin_handlers){varhandlerType=handler.GetType();varinterfaces=handlerType.GetInterfaces().Where(i =>i.IsGenericType&&i.GetGenericTypeDefinition()==typeof(IHandler<>));foreach(varifaceininterfaces){vareventType=iface.GetGenericArguments()[0];if(!_handlerCache.ContainsKey(eventType)){_handlerCache[eventType]=newList<IHandler>();}_handlerCache[eventType].Add(handler);}}}publicTask<IEnumerable<IHandler<T>>>ResolveAsync<T>()whereT:IDomainEvent{vareventType=typeof(T);if(_handlerCache.TryGetValue(eventType,outvarhandlers)){vartypedHandlers=handlers.Cast<IHandler<T>>();returnTask.FromResult<IEnumerable<IHandler<T>>>(typedHandlers);}returnTask.FromResult<IEnumerable<IHandler<T>>>(Enumerable.Empty<IHandler<T>>());}}Registration:
services.AddSingleton<IResolver,MyCustomResolver>();Middleware allows you to hook into the event pipeline at various points:
publicclassMyMiddleware:IEventMiddleware{privatereadonlyILogger<MyMiddleware>_logger;publicMyMiddleware(ILogger<MyMiddleware>logger){_logger=logger;}// Called before event is dispatched to handlerspublicTask<bool>OnDispatchingAsync(EventContextcontext){_logger.LogInformation("About to dispatch event: {EventType}",context.EventType.Name);// Return false to skip dispatching// Return true to continuereturnTask.FromResult(true);}// Called after event has been dispatched to all handlerspublicTaskOnDispatchedAsync(EventContextcontext){_logger.LogInformation("Event dispatched: {EventType}",context.EventType.Name);returnTask.CompletedTask;}// Called before each handler processes the eventpublicTask<bool>OnHandlingAsync(EventContextcontext){_logger.LogDebug("About to handle event: {EventType}",context.EventType.Name);returnTask.FromResult(true);}// Called after each handler processes the eventpublicTaskOnHandledAsync(EventContextcontext){_logger.LogDebug("Event handled: {EventType}",context.EventType.Name);returnTask.CompletedTask;}}Using the Base Class:
publicclassLoggingMiddleware:EventMiddlewareBase{privatereadonlyILogger<LoggingMiddleware>_logger;publicLoggingMiddleware(ILogger<LoggingMiddleware>logger){_logger=logger;}publicoverrideTask<bool>OnDispatchingAsync(EventContextcontext){_logger.LogInformation("Event dispatching: {EventType}",context.EventType.Name);returnbase.OnDispatchingAsync(context);}publicoverrideTaskOnDispatchedAsync(EventContextcontext){_logger.LogInformation("Event dispatched: {EventType}",context.EventType.Name);returnbase.OnDispatchedAsync(context);}}Registration:
// Manual registrationservices.AddDomainEvents(assembly);services.AddSingleton<IEventMiddleware,MyMiddleware>();// Or auto-registration (requires parameterless constructor)services.AddDomainEvents(assembly);// Middlewares with parameterless constructors are auto-registeredMiddleware with Dependencies:
If your middleware requires dependencies, register it manually (not auto-registered):
services.AddSingleton<IEventMiddleware>(sp =>newMyMiddleware(sp.GetRequiredService<ILogger<MyMiddleware>>()));Implement IEventListener to customize how events are processed from the queue:
publicclassMyEventListener:IEventListener{privatereadonlyIEventQueue_queue;privatereadonlyIResolver_resolver;privatereadonlyIEnumerable<IEventMiddleware>_middlewares;privatereadonlyILogger<MyEventListener>_logger;publicMyEventListener(IEventQueuequeue,IResolverresolver,IEnumerable<IEventMiddleware>middlewares=null,ILogger<MyEventListener>logger=null){_queue=queue;_resolver=resolver;_middlewares=middlewares??Enumerable.Empty<IEventMiddleware>();_logger=logger;// Subscribe to queue - this is called when events are enqueued_queue.Subscribe(OnEventEnqueued);}privateTaskOnEventEnqueued(EventContextcontext){returnProcessEventAsync(context);}publicTaskStartAsync(CancellationTokencancellationToken=default){_logger?.LogInformation("Event listener started");returnTask.CompletedTask;}publicasyncTaskStopAsync(){_logger?.LogInformation("Event listener stopped");}publicasyncTaskProcessEventAsync(EventContextcontext){// Process event through middleware and handlersvarhandlers=await_resolver.ResolveAsync(context.EventType);foreach(varhandlerinhandlers){// Run handling middleware (before)foreach(varmiddlewarein_middlewares){if(!awaitmiddleware.OnHandlingAsync(context))continue;}// Invoke handlervarhandlerInterfaceType=typeof(IHandler<>).MakeGenericType(context.EventType);varhandleMethod=handlerInterfaceType.GetMethod("HandleAsync");handleMethod?.Invoke(handler,new[]{context.Event});context.IsHandled=true;// Run handling middleware (after)foreach(varmiddlewarein_middlewares){awaitmiddleware.OnHandledAsync(context);}}}}Key Points:
- The listener subscribes to the queue via
_queue.Subscribe(OnEventEnqueued) - When an event is enqueued, the delegate is invoked immediately
- The listener handles the processing pipeline: middleware -> resolver -> handler
- The EventListener is automatically registered when using
AddDomainEvents
Registration:
services.AddDomainEvents(assembly);// EventListener is auto-registered and subscribes automaticallyImplement IAggregateFactory to customize how aggregates are created:
publicclassMyAggregateFactory:IAggregateFactory{privatereadonlyProxyGenerator_proxyGenerator;privatereadonlyIServiceProvider_serviceProvider;publicMyAggregateFactory(IServiceProviderserviceProvider){_serviceProvider=serviceProvider;_proxyGenerator=newProxyGenerator();}publicTask<T>CreateAsync<T>(paramsobject[]constructorArguments)whereT:Aggregate{varinterceptor=_serviceProvider.GetService<IEventInterceptor>();if(interceptor==null){thrownewInvalidOperationException("IEventInterceptor not registered");}varproxy=_proxyGenerator.CreateClassProxy<T>(interceptor);returnTask.FromResult(proxy);}publicTask<IDomainAggregate>CreateAsync(TypeaggregateType,paramsobject[]constructorArguments){varinterceptor=_serviceProvider.GetService<IEventInterceptor>();if(interceptor==null){thrownewInvalidOperationException("IEventInterceptor not registered");}varproxy=(IDomainAggregate)_proxyGenerator.CreateClassProxy(aggregateType,interceptor);returnTask.FromResult(proxy);}}Registration:
services.AddSingleton<IAggregateFactory,MyAggregateFactory>();The library automatically discovers and registers components from specified assemblies. Only types with parameterless constructors are auto-registered. Types with constructor parameters must be registered explicitly.
| Component | Requirement | Behavior |
|---|---|---|
Event Handlers (IHandler<T>) | Parameterless constructor | Singleton |
Event Middleware (IEventMiddleware) | Parameterless constructor | Singleton |
- Handlers: All types implementing
IHandler<T>with parameterless constructors are registered - Middlewares: All types implementing
IEventMiddlewarewith parameterless constructors are registered - Manual Override: If you manually register a service before calling
AddDomainEvents, the auto-registration skips that specific type
If a handler or middleware has constructor parameters, it will not be auto-registered. You must register it explicitly:
Handler with dependencies (must register manually):
publicclassOrderHandler:IHandler<OrderPlaced>{privatereadonlyIOrderService_orderService;// Has constructor parameter - won't be auto-registeredpublicOrderHandler(IOrderServiceorderService){_orderService=orderService;}publicTaskHandleAsync(OrderPlaced@event){return_orderService.ProcessAsync(@event);}}// Must register explicitly:services.AddSingleton<IHandler,OrderHandler>();services.AddSingleton<IOrderService,OrderService>();Middleware with dependencies (must register manually):
publicclassAuditMiddleware:IEventMiddleware{privatereadonlyIAuditService_auditService;// Has constructor parameter - won't be auto-registeredpublicAuditMiddleware(IAuditServiceauditService){_auditService=auditService;}publicTask<bool>OnDispatchingAsync(EventContextcontext){return_auditService.LogAsync(context.Event);}// ... other interface implementations}// Must register explicitly:services.AddSingleton<IEventMiddleware,AuditMiddleware>();services.AddSingleton<IAuditService,AuditService>();// This will auto-register all handlers and middlewares with parameterless constructorsservices.AddDomainEvents(typeof(MyHandler).Assembly);To prevent auto-registration, add a constructor with parameters:
// Won't be auto-registered (has constructor parameter)publicclassMyMiddleware:IEventMiddleware{publicMyMiddleware(stringname){}// Requires parameter// ... interface implementations}// Will be auto-registered (parameterless constructor)publicclassAnotherMiddleware:IEventMiddleware{publicAnotherMiddleware(){}// Parameterless// ... interface implementations}| Interface | Description |
|---|---|
IDomainEvent | Marker interface for domain events |
IHandler<TEvent> | Async handler interface for specific event type |
ISubscribes<TEvent> | Aggregate handler interface - implemented by aggregates to handle their own events |
IHandler | Marker interface for handlers |
IPublisher | Interface for manually raising events |
IResolver | Interface for resolving handlers |
IEventDispatcher | Interface for dispatching events |
IEventInterceptor | Interceptor for aggregate Raise/RaiseAsync methods |
IEventMiddleware | Middleware for event pipeline |
IEventQueue | Queue for in-flight events with subscription support |
IEventListener | Listener for processing queued events via subscription |
IAggregateFactory | Factory for creating proxied aggregates |
| Delegate | Description |
|---|---|
EventDequeuedHandler | Delegate for processing dequeued events (signature: Task Handler(EventContext context)) |
| Class | Description |
|---|---|
Aggregate | Base class for domain aggregates |
EventContext | Context passed to middleware |
Publisher | Default implementation of IPublisher |
Resolver | Default implementation of IResolver |
EventDispatcher | Default implementation of IEventDispatcher |
EventListener | Default implementation of IEventListener - subscribes to queue and processes events |
EventInterceptor | Default interceptor with telemetry |
AggregateFactory | Default factory for proxied aggregates |
InMemoryEventQueue | Default in-memory queue with subscription support |
EventMiddlewareBase | Base class for middleware |
LoggingMiddleware | Built-in logging middleware |
| Method | Description |
|---|---|
AddDomainEvents(assemblies) | Register with default configuration |
AddDomainEvents() | Register for calling assembly |
AddDomainEventsWithDispatcher<TDispatcher>(assemblies) | Register with custom dispatcher type |
AddDomainEventsWithDispatcher(dispatcher, assemblies) | Register with custom dispatcher instance |
AddDomainEventsWithTelemetry(assemblies) | Register with OpenTelemetry support |
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 deps) |
CreateFromServiceProviderAsync(Type) | Non-generic version resolving from DI |
Example - Using CreateFromServiceProviderAsync:
// Register aggregate with DI (constructor dependencies auto-resolved)services.AddTransient<OrderAggregate>();services.AddTransient<IOrderService,OrderService>();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.
Each handler should do one thing:
// GoodpublicclassOrderConfirmationHandler:IHandler<OrderPlaced>{publicTaskHandleAsync(OrderPlaced@event)=>SendEmailAsync(@event.CustomerId,"Order confirmed");}publicclassInventoryHandler:IHandler<OrderPlaced>{publicTaskHandleAsync(OrderPlaced@event)=>ReserveInventoryAsync(@event.Items);}// Avoid - handlers doing too muchpublicclassOrderPlacedHandler:IHandler<OrderPlaced>{publicTaskHandleAsync(OrderPlaced@event){// Don't do email, inventory, analytics, etc. all here}}publicclassAuditMiddleware:EventMiddlewareBase{privatereadonlyIAuditService_auditService;publicAuditMiddleware(IAuditServiceauditService){_auditService=auditService;}publicoverrideasyncTaskOnDispatchedAsync(EventContextcontext){await_auditService.LogAsync(context.Event,context.EventType.Name);}}publicclassErrorHandlingMiddleware:EventMiddlewareBase{privatereadonlyILogger<ErrorHandlingMiddleware>_logger;publicErrorHandlingMiddleware(ILogger<ErrorHandlingMiddleware>logger){_logger=logger;}publicoverrideasyncTaskOnDispatchedAsync(EventContextcontext){if(context.IsDispatched){_logger.LogInformation("Successfully handled {EventType}",context.EventType.Name);}}}publicclassTrackingMiddleware:EventMiddlewareBase{publicoverrideTask<bool>OnDispatchingAsync(EventContextcontext){context.Items["CorrelationId"]=Guid.NewGuid();returnbase.OnDispatchingAsync(context);}}publicclassAnotherMiddleware:EventMiddlewareBase{publicoverrideTaskOnHandledAsync(EventContextcontext){varcorrelationId=context.Items["CorrelationId"];// Use correlation ID for logging/tracingreturnbase.OnHandledAsync(context);}}// Bad - blocks the threadpublicTask<bool>OnDispatchingAsync(EventContextcontext){Thread.Sleep(1000);// Don't do thisreturnTask.FromResult(true);}// Good - async/awaitpublicasyncTask<bool>OnDispatchingAsync(EventContextcontext){awaitTask.Delay(1000);// Non-blockingreturntrue;}Check if aggregate is proxied:
// Use IAggregateFactory to create aggregatesvarorder=awaitaggregateFactory.CreateAsync<OrderAggregate>();order.PlaceOrder(100);// This will dispatch events// Direct instantiation won't dispatchvarorder2=newOrderAggregate();order2.PlaceOrder(100);// Events won't be dispatched
Check handler registration:
varhandlers=serviceProvider.GetServices<IHandler>();// Should contain your handlers
Check middleware returning false:
// If any middleware returns false in OnDispatchingAsync, events won't be dispatchedpublicTask<bool>OnDispatchingAsync(EventContextcontext){returnTask.FromResult(false);// This blocks dispatch}
Check registration:
// Make sure middleware is registeredservices.AddSingleton<IEventMiddleware,MyMiddleware>();
Check constructor:
// Middleware must have parameterless constructor OR be manually registeredpublicclassMyMiddleware:IEventMiddleware{// This requires manual registrationpublicMyMiddleware(ILogger<MyMiddleware>logger){}}
Check assembly scanning:
// Make sure the assembly contains handlersservices.AddDomainEvents(typeof(MyHandler).Assembly);
Check handler interface:
// Must implement IHandler<T> where T : IDomainEventpublicclassMyHandler:IHandler<MyEvent>// Correct{publicTaskHandleAsync(MyEvente)=>Task.CompletedTask;}
Call ProcessQueueAsync:
vardispatcher=serviceProvider.GetRequiredService<IEventDispatcher>();awaitdispatcher.ProcessQueueAsync();
Check queue is registered:
services.AddSingleton<IEventQueue,MyQueue>();
v5 introduces breaking changes:
Event Dispatcher now receives middlewares:
// v4services.AddSingleton<IEventDispatcher>(sp =>newEventDispatcher(sp.GetRequiredService<IResolver>()));// v5services.AddSingleton<IEventDispatcher>(sp =>newEventDispatcher(sp.GetRequiredService<IResolver>(),sp.GetService<IEventQueue>(),sp.GetServices<IEventMiddleware>(),sp.GetService<ILogger<EventDispatcher>>()));
Use AddDomainEvents for full setup:
// Recommendedservices.AddDomainEvents(assembly);// Manual registration is still supported for advanced scenarios
Install the package:
dotnet add package Dormito.DomainEvents
Update registration:
services.AddDomainEvents(typeof(YourHandler).Assembly);
Use IAggregateFactory:
publicclassOrderService{privatereadonlyIAggregateFactory_factory;publicOrderService(IAggregateFactoryfactory){_factory=factory;}publicasyncTaskPlaceOrder(){varorder=await_factory.CreateAsync<OrderAggregate>();order.Place(100);}}
MIT License - see LICENSE for details.