3

Struggling to handle aggregates and aggregate roots when it comes to persisting. For example, I currently have a repository for each aggregate/table, but this is causing some issues in my service class.

Current JournalService

public class JournalService
{
    public JournalService(IUnitOfWork uow, IJournalRepository journalRepo)
    {
        this.uow = uow;
        this.journalRepo = journalRepo;
    }

    public void AddEntry(string journalName, Entry entry)
    {
        journalEntryRepo.AddEntry(journalName, entry);

        // there is no need for me to even have a journal class or any other domain class since repositories handle it all

        uow.Save();
    }

    private readonly IUnitOfWork uow;
    private readonly IJournalRepository journalRepo;
}

In the above example (what I'm currently using), it seems that there is no point to even have domain classes such as Journal. I could just handle everything by accessing the repositories. This feels very weird to me.. I almost feel that there is no domain.

Upon further research it seems that a repository should handle an aggregate root; and return the entire root that you then operate on. The issue I'm having with this proposed solution is, do I just call update on the aggregate root even though I've added objects (e.g. to a list)? Please see below:

Proposed New JournalService

public class JournalService
{
    public JournalService(IUnitOfWork uow, IJournalRepository journalRepo)
    {
        this.uow = uow;
        this.journalRepo = journalRepo;
    }

    public void AddEntry(string journalName, Entry entry)
    {
        Journal journal = journalRepo.FindById(journalName);

        journal.AddEntry(entry);

        journalRepo.Update(journal);   ///// am I handling this correctly here?

        uow.Save();
    }

    private readonly IUnitOfWork uow;
    private readonly IJournalRepository journalRepo;
}

// simple CRUD for repo
public interface IJournalRepository
{
    Journal Create();
    Journal FindById(string name);
    void Update(Journal journal);
    void Remove(Journal journal);
}

With the new solution, am I handling this correctly? Then I assume the IJournalRepository needs to handle figuring out what entries were added and then add them to the correct table? Although, this seems like a lot of extra work...

  • What do you mean by "correctly?" – Robert Harvey Apr 12 '18 at 15:30
  • Correctly as in by DDD and am I missing something that I’ll have to refactor down the road? – keelerjr12 Apr 12 '18 at 15:35
  • I don't see how. It's a single class, which appears to consolidate three calls to a repository. Even if you do have to refactor it, how much work could that possibly be? – Robert Harvey Apr 12 '18 at 15:37
  • In my first example, there is no need for actual domain objects if I just have repositories for everything. This is what feels wrong to me. – keelerjr12 Apr 12 '18 at 15:39
  • 2
    If you need a principle to hang you're hat on, YAGNI. You aren't gonna need it (yet). Build it the simple way, and if you decide you need the additional horsepower of the more complex way later, take the five minutes to add it back in. – Robert Harvey Apr 12 '18 at 15:46
2

I almost feel that there is no domain.

Some domains are inherently anemic. If a given write doesn't depend on the current state, then there's no much to do beyond look at the incoming representation and ensure that it has the correct shape.

A big big hint here is that everything you need to know to perform the write to the database is contained within the request parameters themselves. All you are doing is taking a representation that was passed to you by the client, and transforming it into a message that you send to your persistence store.

So models and repositories and dancing bears aren't actually adding any business value.

Now - suppose you were to add another requirement: that each entry in the journal was supposed to be unique. That might be important to you in the case where you were receiving messages on an unreliable network, and needed to avoid adding any duplicate messages.

With some data stores, you might be able to just send it a WriteIfAbsent(entry) command, in which case this is still just a transformation.

But if you need to implement that logic yourself, now you need to load the history for this journal, compare this entry to those already present, add the entry if it is missing, and maybe store the result. In other words, the message you send to the data store now depends on the data that is already present in the store. In that case, you might want to separate the domain logic, which cares about the uniqueness of the list entries but doesn't care where the list is stored, from the persistence components that care where the list is stored but don't care at all about the uniqueness of the entries.

As the rules for what should be written get more complex, the value of (a) having all of those rules in a well defined boundary and (b) having that boundary be quickly testable without dragging in a bunch of overhead unrelated to the rules, goes up.

0

I think it seems weird to you because there isn't a repository (and service, for that matter) for Entry while it should, IMHO. Also, I believe you need a more "concrete" answer, so let me give you an example of how I would set it up:

(1) Do use domail model classes (Journal, Entry) and go with a generic repository design:

public interface IRepository<T, TId>  where T : class
{
    IEnumerable<T> FindAll();
    IEnumerable<T> FindBy(Expression<Func<T, bool>> predicate);
    void Save(T entity);
    void Add(T entity);
    void Remove(T entity);
}

(2) Provide one implementation for common repository functionality:

public abstract class Repository<T, TEntityKey> where T : class
{
    private readonly JournalDbContext _dbContext = new JournalDbContext();

    public void Save(T entity)
    {
        _dbContext.Entry(entity).State = EntityState.Modified;
        _dbContext.SaveChanges();
    }

    public void Add(T entity)
    {
        _dbContext.Set<T>().Add(entity);
        _dbContext.SaveChanges();
    }

    public void Remove(T entity)
    {
        _dbContext.Set<T>().Remove(entity);
        _dbContext.SaveChanges();
    }

    public IEnumerable<T> FindAll()
    {
        return _dbContext.Set<T>();
    }

    public IEnumerable<T> FindBy(Expression<Func<T, bool>> predicate)
    {
        return _dbContext.Set<T>().Where(predicate);
    }
}

(3) Interfaces for entity repository can be defined as follows. Any specific repository functionality should be here, too:

public interface IJournalRepository : IRepository<Journal, int>
{
    // anything besides the common repository functionality
    Journal FindByName(string name);
}

public interface IEntryRepository : IRepository<Entry, int>
{
    // ...
}

(4) Concrete repository implementations if any specific repository functionality is defined in (3) above or just the class declarations otherwise:

public class JournalRepository : Repository<Journal, int>, IJournalRepository
{
    public Journal FindByName(string name)
    {
        return FindAll().SingleOrDefault(j => j.Name == name);
    }        
}

public class EntryRepository : Repository<Entry, int>, IEntryRepository
{
    // ...
}

(5) Treat Entry as an aggregate root and have a service for it too so you can write (example based on your code):

public EntryService(IUnitOfWork uow, IEntryRepository entryRepo, IJournalRepository journalRepo)
{
    this.uow = uow;
    this.entryRepo = entryRepo;
    this.journalRepo = journalRepo;
}

public void Add(string journalName, Entry entry)
{
    Journal journal = journalRepo.FindByName(journalName);

    entry.JournalId = journal.Id;  // associate entry with the journal  

    entryRepo.Add(entry);   

    uow.Save();
}     
  • Per DDD/Eric Evans, aren’t I supposed to use 1 repository per Aggregrate Root? – keelerjr12 Apr 13 '18 at 10:49
  • @keelerjr12 Typically yes. But that doesn't mean you can't or won't have multiple aggregate roots in your design. My proposal is based on the fact that Entry is treated as an aggregate root. I don't know the very specifics of your application but in your scenario I think an Entry needs to be treated as such because (logically) there will be more than one cases when you want to access it outside the Journal aggregate. – dpant Apr 13 '18 at 11:39

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.