Skip to content
CØDE N!NJΔ edited this page Mar 15, 2026 · 1 revision

DomainEvents Library - Comprehensive Wiki

Table of Contents

  1. Overview
  2. Architecture
  3. Core Concepts
  4. Getting Started
  5. Registration Methods
  6. Extension Points
  7. Auto-Registration
  8. API Reference
  9. Best Practices
  10. Troubleshooting

Overview

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.

Key Features

  • Automatic Event Dispatching: Domain aggregates automatically dispatch events when Raise() or RaiseAsync() 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

Architecture

┌─────────────────────────────────────────────────────────────────────────────┐
│ 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 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
└───────────────────────────────────────────────────────────────────────────────┘

Event Flow

  1. Aggregate.Raise() - Aggregate raises an event
  2. EventInterceptor - Intercepts the call, proceeds with Raise, then dispatches event
  3. EventDispatcher.DispatchAsync() - Runs dispatch middleware, enqueues event
  4. InMemoryEventQueue - Stores event, invokes subscribed delegate immediately
  5. EventListener - Receives callback, processes event through handle middleware
  6. Resolver - Resolves handlers for the event type (includes ISubscribes<T> implementations on aggregates)
  7. Handler - Processes the event (either standalone IHandler<T> or aggregate's ISubscribes<T>.HandleAsync())

Note: The dispatcher returns immediately after enqueueing (fire-and-forget). Event processing happens asynchronously via the queue subscription delegate.

Two-Phase Event Processing

  1. Synchronous Phase (Aggregate.Raise → Queue.Enqueue):

    • Aggregate raises event via Raise() or RaiseAsync()
    • 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)
  2. 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)

Core Concepts

Domain Events

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;}}

Event Handlers

Handlers process domain events:

publicclassCustomerCreatedHandler:IHandler<CustomerCreated>{publicTaskHandleAsync(CustomerCreated@event){// Process the eventConsole.WriteLine($"Customer created: {@event.Name}");returnTask.CompletedTask;}}

Domain Aggregates

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);}}

Getting Started

1. Install the Package

dotnet add package Dormito.DomainEvents

2. Define a Domain Event

publicclassOrderPlaced:IDomainEvent{publicstringOrderId{get;set;}publicdecimalAmount{get;set;}}

3. Create an Event Handler

publicclassOrderPlacedHandler:IHandler<OrderPlaced>{publicasyncTaskHandleAsync(OrderPlaced@event){// Send confirmation email, update inventory, etc.awaitSendConfirmationAsync(@event.OrderId);}privateTaskSendConfirmationAsync(stringorderId){// ImplementationreturnTask.CompletedTask;}}

4. Create an Aggregate

publicclassOrderAggregate:Aggregate{publicvoidPlaceOrder(decimalamount){// Business logic here...var@event=newOrderPlaced{OrderId=Guid.NewGuid().ToString(),Amount=amount};Raise(@event);}}

4a. Aggregate with ISubscribes (Self-Handling)

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).

5. Register Services

services.AddDomainEvents(typeof(OrderPlacedHandler).Assembly);

6. Use in Your Application

publicclassOrderService{privatereadonlyIAggregateFactory_aggregateFactory;publicOrderService(IAggregateFactoryaggregateFactory){_aggregateFactory=aggregateFactory;}publicasyncTaskPlaceOrder(decimalamount){varorder=await_aggregateFactory.CreateAsync<OrderAggregate>();order.PlaceOrder(amount);// Event is automatically dispatched to handlers}}

Registration Methods

Basic Registration

// Scan specific assemblyservices.AddDomainEvents(typeof(OrderPlacedHandler).Assembly);// Scan multiple assembliesservices.AddDomainEvents(typeof(OrderPlacedHandler).Assembly,typeof(CustomerCreatedHandler).Assembly);// Scan calling assemblyservices.AddDomainEvents();

With Custom Dispatcher

services.AddDomainEventsWithDispatcher<MyCustomDispatcher>(assembly);

With Custom Dispatcher Instance

varcustomDispatcher=newMyCustomDispatcher();services.AddDomainEventsWithDispatcher(customDispatcher,assembly);

With Telemetry

services.AddDomainEventsWithTelemetry(assembly);

Manual Registration (Advanced)

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>();

Extension Points

Custom Event Dispatcher

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);

Custom Event Queue

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 Subscribe method 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>();

Custom Event Interceptor

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>();

Custom Handler Resolver

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>();

Event Middleware

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-registered

Middleware with Dependencies:

If your middleware requires dependencies, register it manually (not auto-registered):

services.AddSingleton<IEventMiddleware>(sp =>newMyMiddleware(sp.GetRequiredService<ILogger<MyMiddleware>>()));

Event Listener

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 automatically

Custom Aggregate Factory

Implement 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>();

Auto-Registration

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.

What Gets Auto-Registered

ComponentRequirementBehavior
Event Handlers (IHandler<T>)Parameterless constructorSingleton
Event Middleware (IEventMiddleware)Parameterless constructorSingleton

Auto-Registration Behavior

  1. Handlers: All types implementing IHandler<T> with parameterless constructors are registered
  2. Middlewares: All types implementing IEventMiddleware with parameterless constructors are registered
  3. Manual Override: If you manually register a service before calling AddDomainEvents, the auto-registration skips that specific type

Types with Parameters - Must Register Explicitly

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>();

Example: Auto-Registration

// This will auto-register all handlers and middlewares with parameterless constructorsservices.AddDomainEvents(typeof(MyHandler).Assembly);

Example: Preventing Auto-Registration

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}

API Reference

Interfaces

InterfaceDescription
IDomainEventMarker 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
IHandlerMarker interface for handlers
IPublisherInterface for manually raising events
IResolverInterface for resolving handlers
IEventDispatcherInterface for dispatching events
IEventInterceptorInterceptor for aggregate Raise/RaiseAsync methods
IEventMiddlewareMiddleware for event pipeline
IEventQueueQueue for in-flight events with subscription support
IEventListenerListener for processing queued events via subscription
IAggregateFactoryFactory for creating proxied aggregates

Delegates

DelegateDescription
EventDequeuedHandlerDelegate for processing dequeued events (signature: Task Handler(EventContext context))

Classes

ClassDescription
AggregateBase class for domain aggregates
EventContextContext passed to middleware
PublisherDefault implementation of IPublisher
ResolverDefault implementation of IResolver
EventDispatcherDefault implementation of IEventDispatcher
EventListenerDefault implementation of IEventListener - subscribes to queue and processes events
EventInterceptorDefault interceptor with telemetry
AggregateFactoryDefault factory for proxied aggregates
InMemoryEventQueueDefault in-memory queue with subscription support
EventMiddlewareBaseBase class for middleware
LoggingMiddlewareBuilt-in logging middleware

ServiceCollectionExtensions

MethodDescription
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

IAggregateFactory Methods

The IAggregateFactory provides multiple methods to create proxied aggregates:

MethodDescription
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 automatically

Note: 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.


Best Practices

1. Keep Handlers Focused

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}}

2. Use Middleware for Cross-Cutting Concerns

publicclassAuditMiddleware:EventMiddlewareBase{privatereadonlyIAuditService_auditService;publicAuditMiddleware(IAuditServiceauditService){_auditService=auditService;}publicoverrideasyncTaskOnDispatchedAsync(EventContextcontext){await_auditService.LogAsync(context.Event,context.EventType.Name);}}

3. Handle Errors in Middleware

publicclassErrorHandlingMiddleware:EventMiddlewareBase{privatereadonlyILogger<ErrorHandlingMiddleware>_logger;publicErrorHandlingMiddleware(ILogger<ErrorHandlingMiddleware>logger){_logger=logger;}publicoverrideasyncTaskOnDispatchedAsync(EventContextcontext){if(context.IsDispatched){_logger.LogInformation("Successfully handled {EventType}",context.EventType.Name);}}}

4. Use EventContext.Items for State Sharing

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);}}

5. Don't Block in Middleware

// 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;}

Troubleshooting

Events Not Being Dispatched

  1. 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
  2. Check handler registration:

    varhandlers=serviceProvider.GetServices<IHandler>();// Should contain your handlers
  3. 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}

Middleware Not Called

  1. Check registration:

    // Make sure middleware is registeredservices.AddSingleton<IEventMiddleware,MyMiddleware>();
  2. Check constructor:

    // Middleware must have parameterless constructor OR be manually registeredpublicclassMyMiddleware:IEventMiddleware{// This requires manual registrationpublicMyMiddleware(ILogger<MyMiddleware>logger){}}

Handlers Not Found

  1. Check assembly scanning:

    // Make sure the assembly contains handlersservices.AddDomainEvents(typeof(MyHandler).Assembly);
  2. Check handler interface:

    // Must implement IHandler<T> where T : IDomainEventpublicclassMyHandler:IHandler<MyEvent>// Correct{publicTaskHandleAsync(MyEvente)=>Task.CompletedTask;}

Queue Not Processing

  1. Call ProcessQueueAsync:

    vardispatcher=serviceProvider.GetRequiredService<IEventDispatcher>();awaitdispatcher.ProcessQueueAsync();
  2. Check queue is registered:

    services.AddSingleton<IEventQueue,MyQueue>();

Migration Guide

From v4 to v5

v5 introduces breaking changes:

  1. 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>>()));
  2. Use AddDomainEvents for full setup:

    // Recommendedservices.AddDomainEvents(assembly);// Manual registration is still supported for advanced scenarios

Adding to Existing Project

  1. Install the package:

    dotnet add package Dormito.DomainEvents
  2. Update registration:

    services.AddDomainEvents(typeof(YourHandler).Assembly);
  3. Use IAggregateFactory:

    publicclassOrderService{privatereadonlyIAggregateFactory_factory;publicOrderService(IAggregateFactoryfactory){_factory=factory;}publicasyncTaskPlaceOrder(){varorder=await_factory.CreateAsync<OrderAggregate>();order.Place(100);}}

License

MIT License - see LICENSE for details.