Skip to content

Repository files navigation

events DomainEvents v5.0.0

NuGet versionLicense: MITBuildCodeQLGitHub Release.Net 10.0.Net 9.0.Net 8.0.Net Standard 2.1.Net Standard 2.0

Library to help implement transactional events in domain bounded context.

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.

What is a Domain Event?

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.

image

Two Approaches to Use DomainEvents

Approach 1: Using Publisher and Handler Directly

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

Approach 2: Using Interception (Aggregate + Factory)

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

Architecture Flow

Event Processing Flow

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

Flow Summary

  1. PUBLISHING PHASE - Aggregate.Raise() → Interceptor → Middleware.OnDispatching() → Dispatcher → Queue.Enqueue() → Middleware.OnDispatched()
  2. 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() or RaiseAsync(). Can also implement ISubscribes<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> or ISubscribes<T>.

Event Middleware

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

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


Interface Summary

InterfacePurpose
IDomainEventMarker interface for domain events
IHandler<TEvent>Async handler interface
ISubscribes<TEvent>Aggregate handler interface (implemented by aggregates to handle their own events)
IPublisherInterface for raising events
IAggregateFactoryFactory for creating proxied aggregates
IEventMiddlewarePlugin for event pipeline
IEventQueueIn-flight event queue

Package Information

  • Package ID: Dormito.DomainEvents
  • Target Frameworks: netstandard2.0, netstandard2.1, net8.0, net9.0, net10.0
  • License: MIT

About

DomainEvents is .Net library to implement transactional events in domain model.

Topics

Resources

Stars

3 stars

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages