Mastering Domain Events Made Simple with MediatR in C# and .NET 8
Readers of this article will gain insights into mastering domain events with Meditr in C# and .NET 8. They'll learn how to effectively manage domain events, from creation to handling, using this powerful library. Through step-by-step guidance, they'll discover practical techniques for optimizing event-driven architectures, enhancing scalability and maintainability in their software projects. To structure our discussion, we will use the What? Why? How? framework.
What?
Domain events are a design pattern in Domain-Driven Design (DDD) used to capture and broadcast significant state changes in your domain model. In the context of C# with Entity Framework Core 8 and MediatR, domain events enable you to decouple the core logic of your application from side effects, such as notifications or external system updates.
MediatR is a popular library in the .NET ecosystem that facilitates CQRS (Command Query Responsibility Segregation) and mediator patterns. It helps to manage interactions between objects, reducing dependencies and promoting a clean, maintainable codebase. MediatR uses handlers to process requests, such as commands, queries, and notifications, allowing for better separation of concerns.
Why?
Separation of Concerns: By using domain events, you ensure that your core domain logic remains clean and focused on business rules, while other concerns (like notifications or logging) are handled separately.
Decoupling: Domain events help to decouple different parts of your application, promoting a more modular and maintainable architecture.
Testability: With domain events, you can more easily test your core domain logic without worrying about side effects, improving the overall testability of your application.
How?
To implement domain events using C# with Entity Framework Core 8 and MediatR, follow these steps:
Capturing Domain Events
1. Define a Basic Interface for Domain Events:
Create an interface IDomainEvent
that inherits from MediatR's INotification
. This interface will represent the domain events.
namespace RecipeManagement.Domain
{
using MediatR;
public interface IDomainEvent : INotification { }
}
2. Create a Base Entity Class:
Develop a base entity class that all your entities can inherit from. This class will include:
A list of
IDomainEvent
calledDomainEvents
to capture messages we want to publish.A primary key property called
Id
for entity identification.A method
QueueDomainEvent
to add events to theDomainEvents
list.
namespace RecipeManagement.Domain
{
using System;
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
using System.ComponentModel.DataAnnotations.Schema;
public abstract class BaseEntity
{
[Key]
public Guid Id { get; private set; } = Guid.NewGuid();
[NotMapped]
public List<IDomainEvent> DomainEvents { get; } = new List<IDomainEvent>();
public void QueueDomainEvent(IDomainEvent @event)
{
DomainEvents.Add(@event);
}
}
}
3. Define and Publish the Domain Event:
Create a domain event class that implements IDomainEvent
. Raise this event in your domain model.
public class OrderPlacedEvent : IDomainEvent
{
public int OrderId { get; }
public DateTime OrderDate { get; }
public OrderPlacedEvent(int orderId, DateTime orderDate)
{
OrderId = orderId;
OrderDate = orderDate;
}
}
public class Order : BaseEntity
{
public DateTime OrderDate { get; private set; }
public void PlaceOrder(DateTime orderDate)
{
OrderDate = orderDate;
QueueDomainEvent(new OrderPlacedEvent(Id, orderDate));
}
}
4. Save and Dispatch Domain Events:
Customize your DbContext
to intercept the SaveChanges
and SaveChangesAsync
methods and dispatch the domain events after the changes are committed by encapsulating the dispatch logic in a method called DispatchDomainEvents
.
public class AppDbContext : DbContext
{
private readonly IMediator _mediator;
public AppDbContext(DbContextOptions<AppDbContext> options, IMediator mediator)
: base(options)
{
_mediator = mediator;
}
public override int SaveChanges()
{
OnBeforeSaving().GetAwaiter().GetResult();
return base.SaveChanges();
}
public override async Task<int> SaveChangesAsync(CancellationToken cancellationToken = default)
{
await OnBeforeSaving();
return await base.SaveChangesAsync(cancellationToken);
}
private async Task OnBeforeSaving()
{
await DispatchDomainEvents();
}
private async Task DispatchDomainEvents()
{
var domainEventEntities = ChangeTracker.Entries<BaseEntity>()
.Select(po => po.Entity)
.Where(po => po.DomainEvents.Any())
.ToArray();
foreach (var entity in domainEventEntities)
{
var events = entity.DomainEvents.ToArray();
entity.DomainEvents.Clear();
foreach (var entityDomainEvent in events)
{
await _mediator.Publish(entityDomainEvent);
}
}
}
}
5. Handle the Domain Event:
Implement a handler for your domain event. The handler should implement the INotificationHandler
interface from MediatR.
public class OrderPlacedEventHandler : INotificationHandler<OrderPlacedEvent>
{
public Task Handle(OrderPlacedEvent notification, CancellationToken cancellationToken)
{
// Handle the event (e.g., send an email notification)
Console.WriteLine($"Order placed with ID: {notification.OrderId} on {notification.OrderDate}");
return Task.CompletedTask;
}
}
6. Register Dependencies:
Finally, register MediatR and your handlers in the dependency injection container.
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddDbContext<AppDbContext>(options =>
options.UseSqlServer(Configuration.GetConnectionString("DefaultConnection")));
services.AddMediatR(typeof(Startup).Assembly);
// Register other services and handlers
}
}
By following these steps, you can effectively implement domain events in your C# application using Entity Framework Core 8 and MediatR. This approach helps you maintain a clean separation of concerns, decouple your application's components, and improve testability.
Subscribe to my newsletter
Read articles from Pablo Rivas Sánchez directly inside your inbox. Subscribe to the newsletter, and don't miss out.
Written by
Pablo Rivas Sánchez
Pablo Rivas Sánchez
Seasoned Software Engineer | Microsoft Technology Specialist | Over a Decade of Expertise in Web Applications | Proficient in Angular & React | Dedicated to .NET Development & Promoting Unit Testing Practices