Search code examples
c#mongodbrepositorydomain-driven-designclean-architecture

Clean Architecture Repository wrapper for aggregates


I come here with a question about clean architecture, which is how to break down my code between Application and Infrastructure layers. My application uses the Event Store and Event Bus, so that by connecting these 2 modules, I used the AggregateRepository class to which Aggregate Root is passed, and then the Events from a given aggregate are downloaded and thrown into the EventRepository class, which is injected into the AggregateRepository class, then 2 classes are injected into EventRepository class, one is the implementation of Mongo and the other is EventBus implementation, using the repository design pattern, events that go to the EventReposiotry class are passed to the MongoEventStore classes (implements IEventStoreRepository) and PublishEvent class (implements PublishEvent). All the interfaces that these classes implement are stored in the application layer, while the classes themselves are stored in the infrastructure layer. The question is whether the AggregateRepository and EventRepository classes, which are only wrappers and do not implement any things like using a database or using an event bus, should be in the application layer or infrastructure layer, these classes do not use any external libraries, because it is the responsibility of the MongoEventStore and Publish event classes that are located in the Infrastructure layer. Here it shows more precisely the layout in my application with the discussed classes and interfaces marked.

My application setup picture

Also i send Code implementation

IAggregateRepository.cs inside Application Layer

using FluentResults;
using SharedKernel.Domain.Aggregate;
using SharedKernel.Domain.UniqueKey;

namespace SharedKernel.Application.Repositories.Aggregate
{
    public interface IAggregateRepository
    {
        Result Save(AggregateRoot aggregate, AggregateKey key);
        Task<Result<T>> GetAsync<T>(AggregateKey key) where T : AggregateRoot, new();
        Task<Result> CommitAsync();
    }
}

AggregateRepository.cs inside Infrastructure layer but not sure is good place

using FluentResults;
using SharedKernel.Application.Common.Errors.RepositoriesErrors;
using SharedKernel.Application.Repositories.Aggregate;
using SharedKernel.Domain.Aggregate;
using SharedKernel.Domain.UniqueKey;

namespace SharedKernel.Infrastructure.Repositories.Aggregate
{
    public class AggregateRepository : IAggregateRepository
    {
        private readonly IDictionary<AggregateKey, AggregateRoot> _aggregates;
        private readonly IEventRepository _eventRepository;

        public AggregateRepository(IEventRepository eventRepository)
        {
            _aggregates = new Dictionary<AggregateKey, AggregateRoot>();
            _eventRepository = eventRepository;
        }

        public async Task<Result> CommitAsync()
        {
            foreach (var item in _aggregates)
            {
                try
                {
                    var events = item.Value.GetUncommittedChanges();
                    await _eventRepository.SaveAsync(events);
                    _aggregates.Remove(item);
                }
                catch (Exception)
                {
                    return Result.Fail(new SaveEventError());
                }
            }

            return Result.Ok();
        }

        public async Task<Result<T>> GetAsync<T>(AggregateKey key) where T : AggregateRoot, new()
        {
            var aggregate = new T();
            if (_aggregates.ContainsKey(key))
            {
                var domainEvents = _aggregates[key].GetUncommittedChanges();
                aggregate.LoadFromHistory(domainEvents);
                return aggregate;
            }

            try
            {
                var events = await _eventRepository.GetAsync(key);
                if (!events.Any())
                    return Result.Fail(new AggregateNotFoundError(key));

                aggregate.LoadFromHistory(events);
                return aggregate;
            }
            catch (Exception)
            {
                return Result.Fail(new GetEventError());
            }
        }

        public Result Save(AggregateRoot aggregate, AggregateKey key)
        {
            if (!CheckVersionAggregate(aggregate, key))
                return Result.Fail(new AggregateVersionError(key));

            _aggregates[key] = aggregate;
            return Result.Ok();
        }

        private bool CheckVersionAggregate(AggregateRoot aggregate, AggregateKey key)
        {
            AggregateRoot rootCheckAggregate = null;
            foreach (var item in _aggregates)
            {
                if (item.Key == key)
                    rootCheckAggregate = item.Value;
            }

            return rootCheckAggregate == null || rootCheckAggregate.Version < aggregate.Version;
        }
    }
}

IEventRepository.cs inside Application Layer

using SharedKernel.Domain.Event;
using SharedKernel.Domain.UniqueKey;

namespace SharedKernel.Application.Repositories.Aggregate
{
    public interface IEventRepository
    {
        Task SaveAsync(IEnumerable<DomainEvent> events);
        Task<IEnumerable<DomainEvent>> GetAsync(AggregateKey key);
    }
}

EventRepository.cs inside Infrastructure layer but not sure is good place

using SharedKernel.Application.Repositories.Aggregate;
using SharedKernel.Application.Repositories.EventBus;
using SharedKernel.Application.Repositories.EventStore;
using SharedKernel.Domain.Event;
using SharedKernel.Domain.UniqueKey;

namespace SharedKernel.Infrastructure.Repositories.Aggregate
{
    public class EventRepository : IEventRepository
    {
        private readonly IEventStoreRepository _eventStore;
        private readonly IPublishEvent _eventPublisher;

        public EventRepository(IEventStoreRepository eventStore, IPublishEvent eventPublisher)
        {
            _eventPublisher = eventPublisher;
            _eventStore = eventStore;
        }

        public Task<IEnumerable<DomainEvent>> GetAsync(AggregateKey key)
        {
            return _eventStore.GetAsync(key);
        }

        public async Task SaveAsync(IEnumerable<DomainEvent> events)
        {
            foreach (var @event in events)
            {
                await _eventStore.SaveAsync(@event);
                _eventPublisher.Publish(@event);
            }
        }
    }
}


Solution

  • Generally speaking, in Clean Architecture we would try to have as less code as possible in the infrastructure layer, not only but also because this code is harder to test.

    Furthermore the purpose of repositories usually is to just convert data most convenient for the infrastructure to data which is most convenient for the business/application logic and vice versa. Actual logic and decision making should not happen in repositories. So repositories would usually not belong to the application/business logic layer.

    In Uncle Bobs Clean Architecture such data conversion would usually go into the "interface adapters" layer which is located between application layer and infrastructure.

    For more details check out my blog post which includes a discussion on repository implementations and this YouTube video on where to put which code using a concrete example.