Mastering Domain Events Made Simple with MediatR in C# and .NET 8

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 called DomainEvents to capture messages we want to publish.

  • A primary key property called Id for entity identification.

  • A method QueueDomainEvent to add events to the DomainEvents 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.