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.