DEV Community

Cover image for Designing the domain model to support multiple persistence methods
thecodewrapper
thecodewrapper

Posted on

Designing the domain model to support multiple persistence methods

Introduction

This is a continuation of my earlier post Implementing a Clean Architecture in ASP.NET Core 6, so if you haven’t read that yet, I suggest you do as it will provide a little more context, although it is not necessary to follow along.

As mentioned in the previous post, I wanted to allow the use of a traditional state-oriented CRUD model for persistence, and at the same time have the ability to switch to an event sourcing method, or even use both side-by-side. So in this post, we’ll take a look at how to design our domain model so we can use both event-sourcing and a state persistence model together, or interchangeably.

One key assumption for this is that we’ll be using EF Core with SQL Server for our relational persistence method.

The goal

The main goal was to have domain entities within the project be able to be persisted either using a relational model or using event sourcing, or even both together without changing the underlying implementation of the domain entity itself. In that, I wanted to keep the structure of the core aligned with Domain-Driven Design practices.

To make this work, it naturally had to be based on some form of event mechanism. In this case, domain events seems quite a good fit. To learn what domain events are and how they fit into the whole domain-driven design approach, take a look at some of the resources at the end of this post.

Through my research prior to starting this journey, I found that there are quite a few ways domain events can be implemented. I’ve come to realize that there isn’t a “better” way to do this, the “best” way is what fits in the overall architecture and the purpose for which you want to utilize domain events.

For this to be a viable solution, I’ve set out to achieve the following low-level goals:

  • Encapsulate the handling logic of the event within the aggregate itself
  • Handle the events in such a way that if event sourcing is used, the playback of events would allow the aggregate object to build itself into its latest state
  • Have compile-time safety such that the event handlers are guaranteed to be implemented

Why domain events?

I took the approach of having functions in the aggregates which contain some logic to only raise an event, and have that event handler internally, within the aggregate, perform any changes to the aggregate. This is because it serves the purpose of event-sourcing and non-event sourcing implementations at the same time; it caters to the reconstruction of the aggregate’s state from the event stream.

Once again, I’ve looked into what is out there in terms of domain event implementations. Below some articles for domain events:

Initial approach

My initial approach to handle and apply domain events was to have internal functions called Apply inside the aggregates. Each function would take the event object its supposed to handle and do whatever work they are supposed to do.

Whenever we would like an event to fire, we’ll call the RaiseEvent function which is inherited by the AggregateRootBase class. The RaiseEvent function does some versioning related work and subsequently calls into the ApplyEvent function (part of the IAggregateRoot interface).

In turn, this dynamically calls the appropriate Apply function on the aggregate, according to the type of the event passed in. The implementation of RaiseEvent and the initial implementation of the ApplyEvent function in the AggregateRootBase looked like this:

protected void RaiseEvent<TEvent>(TEvent @event)
            where TEvent : DomainEventBase<TId> {
            int version = Version + 1;
            IDomainEvent<TId> eventWithAggregate = @event.WithAggregate(
                Equals(Id, default(TId)) ? @event.AggregateId : Id,
                version);

            ((IAggregateRoot<TId>)this).ApplyEvent(eventWithAggregate, version);
            _uncommittedEvents.Add(eventWithAggregate);
        }
Enter fullscreen mode Exit fullscreen mode
void IAggregateRoot<TId>.ApplyEvent(IDomainEvent<TId> @event, int version) {
            if (!_uncommittedEvents.Any(x => Equals(x.EventId, @event.EventId))) {
                ((dynamic)this).Apply((dynamic)@event);
                Version = version;
            }
        }
Enter fullscreen mode Exit fullscreen mode

Below an example of how this would be raised by the aggregate:

public Order(string trackingNumber) {
            RaiseEvent(new OrderCreatedEvent(trackingNumber));
        }

internal void Apply(OrderCreatedEvent @event) {
            Id = @event.AggregateId;
            TrackingNumber = @event.TrackingNumber;
        }
Enter fullscreen mode Exit fullscreen mode

Now, this has the benefit of good encapsulation by keeping the handling of the event internal, not exposed to the outside world. However, it does not give the benefit of compile-time checking in terms of actually having the implementation to handle the event in place. This would show up at runtime if I accidentally forgot to write the Apply function for a given event. We could argue that this is not so much of an issue, however being the picky person that I am, I couldn’t allow this 🙂

Therefore, i tried a different approach.

Second attempt

With this second approach, aggregates implement the IDomainEventHandler generic interface, which has just one function to handle the event.

internal interface IDomainEventHandler<T> where T: IDomainEvent
    {
        internal void Apply(T @event);
    }
Enter fullscreen mode Exit fullscreen mode

The AggregateRootBase contains the code which applies the actual event. This uses reflection to invoke the aggregate’s Apply function for the particular domain event type, implemented via the interface internally.

void IAggregateRoot<TId>.ApplyEvent(IDomainEvent<TId> @event, int version) {
            if (!_uncommittedEvents.Any(x => Equals(x.EventId, @event.EventId))) {
                var handlerType = GetType()
                    .GetInterfaces()
                    .Single(i => i.GetGenericTypeDefinition() == typeof(IDomainEventHandler<>) && i.GetGenericArguments()[0] == @event.GetType());
                var handlerMethod = handlerType.GetTypeInfo().GetDeclaredMethod(nameof(IDomainEventHandler<IDomainEvent>.Apply));
                handlerMethod.Invoke(this, new object[] { @event });
            }
            Version = version;
        }
Enter fullscreen mode Exit fullscreen mode

So now, the way this would be handled by the aggregate turns into this:

public Order(string trackingNumber) {
            RaiseEvent(new OrderCreatedEvent(trackingNumber));
        }

void IDomainEventHandler<OrderCreatedEvent>.Apply(OrderCreatedEvent @event) {
            Id = @event.AggregateId;
            TrackingNumber = @event.TrackingNumber;
        }
Enter fullscreen mode Exit fullscreen mode

There are a couple of reasons I went in this direction.

First, I wanted to keep the implementation of the handler inside the aggregate itself, and also provide compile-time safety in having to implement the IDomainEventHandler interface.

Secondly, I wanted to have the handler implementations access modifier be internal, since these handler implementations are contained within the domain entities, which themselves are visible to all outer layers. I don’t like having the handling of an event be visible to the outside world.

I would say this approach provides good encapsulation in regards to raising and handling domain events, with the downside of been slower due to reflection, although the performance impact is fairly negligible.

Final result

Having said that, there is an issue with the above approach as well. The issue here is that any exception thrown within the IDomainEventHandler implementation, actually bubbles up as a TargetInvocationException, which is an exception thrown when methods are invoked using reflection. This type of exception wraps the original exception in the InnerException property. Obviously this reduces testability, and is therefore not acceptable.

To avoid this, I wrapped the invocation around a try/catch block which will re-throw the inner exception. So the final implementation looks like this:

void IAggregateRoot<TId>.ApplyEvent(IDomainEvent<TId> @event, int version) {
            if (!_uncommittedEvents.Any(x => Equals(x.EventId, @event.EventId))) {
                try {
                    var handlerType = GetType()
                    .GetInterfaces()
                    .Single(i => i.GetGenericTypeDefinition() == typeof(IDomainEventHandler<>) && i.GetGenericArguments()[0] == @event.GetType());
                    var handlerMethod = handlerType.GetTypeInfo().GetDeclaredMethod(nameof(IDomainEventHandler<IDomainEvent>.Apply));
                    handlerMethod.Invoke(this, new object[] { @event });
                }
                catch (TargetInvocationException ex) {
                    throw ex.InnerException;
                }
            }
            Version = version;
        }
Enter fullscreen mode Exit fullscreen mode

For now, I can make my peace with this try/catch block, at least until any significant performance issues arise. I may look into doing some performance testing to see how that goes in the future.

Below the entire AggregateRootBase class, as it stands now:

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace CH.CleanArchitecture.Core.Domain
{
    public abstract class AggregateRootBase<TId> : IAggregateRoot<TId>
    {
        public const int NewAggregateVersion = -1;

        private readonly ICollection<IDomainEvent<TId>> _uncommittedEvents = new LinkedList<IDomainEvent<TId>>();

        /// <summary>
        /// The aggregate root Id
        /// </summary>
        public TId Id { get; protected set; }

        /// <summary>
        /// The aggregate root current version
        /// </summary>
        public int Version { get; private set; } = NewAggregateVersion;

        /// <summary>
        /// Indicates whether this aggregate is logically deleted
        /// </summary>
        public bool IsDeleted { get; private set; }

        void IAggregateRoot<TId>.ApplyEvent(IDomainEvent<TId> @event, int version) {
            if (!_uncommittedEvents.Any(x => Equals(x.EventId, @event.EventId))) {
                try {
                    InvokeHandler(@event);
                    Version = version;
                }
                catch (TargetInvocationException ex) {
                    throw ex.InnerException;
                }
            }
            Version = version;
        }

        void IAggregateRoot<TId>.ClearUncommittedEvents() {
            _uncommittedEvents.Clear();
        }

        IEnumerable<IDomainEvent<TId>> IAggregateRoot<TId>.GetUncommittedEvents() {
            return _uncommittedEvents.AsEnumerable();
        }

        protected void MarkAsDeleted() {
            IsDeleted = true;
        }

        protected void RaiseEvent<TEvent>(TEvent @event)
            where TEvent : DomainEventBase<TId> {
            int version = Version + 1;
            IDomainEvent<TId> eventWithAggregate = @event.WithAggregate(
                Equals(Id, default(TId)) ? @event.AggregateId : Id,
                version);

            ((IAggregateRoot<TId>)this).ApplyEvent(eventWithAggregate, version);
            _uncommittedEvents.Add(eventWithAggregate);
        }

        private void InvokeHandler(IDomainEvent<TId> @event) {
            var handlerMethod = GetEventHandlerMethodInfo(@event);
            handlerMethod.Invoke(this, new object[] { @event });
        }

        private MethodInfo GetEventHandlerMethodInfo(IDomainEvent<TId> @event) {
            var handlerType = GetType()
                    .GetInterfaces()
                    .Single(i => i.GetGenericTypeDefinition() == typeof(IDomainEventHandler<>) && i.GetGenericArguments()[0] == @event.GetType());
            return handlerType.GetTypeInfo().GetDeclaredMethod(nameof(IDomainEventHandler<IDomainEvent>.Apply));
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

As always, any feedback on this is much appreciate so please feel free to let me know what you think of this approach.

Resources

To find out additional information about domain events, what they are, their benefits and how the fit into your design, follow one of the links below:

Top comments (0)