Search code examples
c#.netormdomain-driven-designpersistence

DDD logic and Persistence Ignorance


Consider the following scenario:

public class Document 
{
  private ISet<User> sharedWith;

  public Document(string name) 
  {
    this.sharedWith = new HashSet<User>();
    this.Name = name;
  }

  public string Name { get; private set; }

  public IEnumerable<User> SharedWith 
  {
    get 
    {      
      return this.sharedWith;
    }
  }

  public void ShareWith(User user) 
  {
    if (this.SharedWith.Contains(user)) 
    {      
      throw new Exception("This document is already shared with that user.");
    }

    this.sharedWith.Add(user);
  }
}
  • Documents can be shared with User
  • When sharing a document with a user, if the document has already been shared with that user, throw an exception.
  • Documents can be shared with 10's of thousands of users.

Obviously this does not scale very well, because of the need to check the SharedWith for user existence, resulting in the ORM lazy-loading the entire collection into memory. I could do the existence check in the application service, but I consider this domain logic, and so it makes the most sense to me that I keep it in the Document class.

I can't seem to figure out how this should be done with DDD? And what if I am unable to use an ORM, how does one do this sort of stuff?

I suppose I should have a Document Aggregate and a User Aggregate?

I've looked at various DDD resources (I have not read the book though), but I can't seem to find an answer to this particular scenario.


Solution

  • this was quickly done up so it's not perfect but you get the gist of it:

    public class User { public Guid UserId { get; set; } }
    
    public class Document
    {
        public string Name { get; private set; }
    
        private ICollection<User> sharedWith = new List<User>();
    
        private DateTime? publishedOn;
    
        public Document(string name)
        {
            if (string.IsNullOrWhiteSpace(name))
            {
                throw new ArgumentException("Name is required");
            }
    
            this.Name = name;
        }
    
        public void Publish()
        {
            if (this.publishedOn.HasValue == false)
            {
                this.publishedOn = DateTime.UtcNow;
            }
        }
    
        public void SharedWith(User user)
        {
            if (this.publishedOn.HasValue == false)
            {
                throw new InvalidOperationException("Document must be published for sharing is allowed.");
            }
    
            sharedWith.Add(user);
        }
    }
    
    public interface IDocumentRepository
    {
        Document documentOfId(Guid id);
    
        bool IsAlreadySharedWith(Guid documentId, Guid userId);
    }
    
    public interface IUseRepository
    {
        User userOfId(Guid id);
    }
    
    public class ShareDocumentService
    {
        private readonly IUseRepository userRepository;
        private readonly IDocumentRepository documentRepository;
    
        public void ShareWith(Guid userId, Guid documentId)
        {
            if (documentRepository.IsAlreadySharedWith(documentId, userId))
                throw new InvalidOperationException("Document has already been shared with user.");
    
            User user = userRepository.userOfId(userId);
    
            Document doc = documentRepository.documentOfId(documentId);
    
            doc.SharedWith(user);
        }
    }