An exercise in domain driven design. [Collaboration]

As I have had several issues with fully understanding, and applying, domain driven design, I would like to take this opportunity to invite anybody who has any experience with it at all, to participate in an real world exercise. The results will be implemented in my forum application, and each participant will each get credit in the readme and about page.

To start with, I will assume the role of domain expert (lol) and begin by listing the user stories. We can proceed from there.

Caveat: this system (currently) will be using the ADONETDB user management system.

Forum Entity User Stories

Forum may be created, and operations may only be performed, by a user with the Administrator role.

There must be a way determine display order, or sequence, of forums.

A forum must have a title, unique when compared to others with the same parentid.

A forum may optionally have a description.

A forum is considered a category if it’s parentid is 0.

A forum may be marked as enabled or disabled, effectively turning on or off the ability to post new threads and replies.

A forum may be marked as visible or hidden. Navigation to a hidden forum is still allowed if the user knows the correct url.

A forum should maintain a collection of subforums.

A subforum must be unique, and added to only one parent at a time.

A subforum may be removed.

A subforum may be moved to another parent, or set to no parent (making it a category).

A forum should maintain a collection of threads (posts where forumid == id and post is not a reply to another post).

Before I go on to additional entity user stories, let’s try to define what we have so far in code.

Please post your takes on this. I’d like to see several suggestions if possible to compare and see where the strengths and weaknesses in each lay.

Ok, here’s my initial design. I realize that things may change as I develop the Post entity. But for now, take a look and tell me if anything needs to be changed (other than eliminating repeated logic checks).

using System;
using System.Collections.Generic;
using System.Security.Principal;
namespace Venue.Domain
{
    public class Forum
    {
        private Forum _parent;
        private IList<Forum> _children = new List<Forum>();
        private IList<Post> _posts = new List<Post>();
        public virtual int Sequence { get; protected set; }
        public virtual string Title { get; protected set; }
        public virtual string Description { get; protected set; }
        public virtual bool Enabled { get; protected set; }
        public virtual bool Visible { get; protected set; }
        public virtual IEnumerable<Forum> Children { get { return _children; } }
        public virtual IEnumerable<Post> Posts { get { return _posts; } }
        protected Forum() { }
        public Forum(IPrincipal user, Forum parent, string title)
        {
            ReparentTo(user, parent);
            ChangeTitle(user, title);
        }
        public virtual void ReparentTo(IPrincipal user, Forum parent)
        {
            if (user == null) throw new ArgumentNullException("user");
            if (parent == null) throw new ArgumentNullException("parent");
            if (!user.IsInRole("Administrator")) throw new ArgumentException("User not in role 'Administrator'!", "IsInRole");
            if (_parent != null) _parent.RemoveChild(user, this);
            if (parent != null) parent.AddChild(user, this);
        }
        public virtual void IncrementSequence(IPrincipal user)
        {
            if (user == null) throw new ArgumentNullException("user");
            if (!user.IsInRole("Administrator")) throw new ArgumentException("User not in role 'Administrator'!", "IsInRole");
            Sequence = Sequence == int.MaxValue ? int.MaxValue : Sequence++;
        }
        public virtual void DecrementSequence(IPrincipal user)
        {
            if (user == null) throw new ArgumentNullException("user");
            if (!user.IsInRole("Administrator")) throw new ArgumentException("User not in role 'Administrator'!", "IsInRole");
            Sequence = Sequence == int.MinValue ? int.MinValue : Sequence--;
        }
        public virtual void ChangeTitle(IPrincipal user, string title)
        {
            if (user == null) throw new ArgumentNullException("user");
            if (title == null) throw new ArgumentNullException("title");
            if (!user.IsInRole("Administrator")) throw new ArgumentException("User not in role 'Administrator'!", "IsInRole");
            if (string.IsNullOrEmpty(title) || string.IsNullOrWhiteSpace(title)) throw new ArgumentException("Title is not valid!", "title");
            Title = title.Trim();
        }
        public virtual void ChangeDescription(IPrincipal user, string description)
        {
            if (user == null) throw new ArgumentNullException("user");
            if (description == null) throw new ArgumentNullException("description");
            if (!user.IsInRole("Administrator")) throw new ArgumentException("User not in role 'Administrator'!", "IsInRole");
            if (string.IsNullOrEmpty(description) || string.IsNullOrWhiteSpace(description)) description = string.Empty;
            Description = description.Trim();
        }
        public virtual void AllowNewContent(IPrincipal user, bool enabled)
        {
            if (user == null) throw new ArgumentNullException("user");
            if (!user.IsInRole("Administrator")) throw new ArgumentException("User not in role 'Administrator'!", "IsInRole");
            Enabled = enabled;
        }
        public virtual void DisplayToPublic(IPrincipal user, bool visible)
        {
            if (user == null) throw new ArgumentNullException("user");
            if (!user.IsInRole("Administrator")) throw new ArgumentException("User not in role 'Administrator'!", "IsInRole");
            Visible = visible;
        }
        public virtual bool ContainsChild(Forum child)
        {
            return _children.Contains(child);
        }
        public virtual void AddChild(IPrincipal user, Forum child)
        {
            if (user == null) throw new ArgumentNullException("user");
            if (child == null) throw new ArgumentNullException("child");
            if (!user.IsInRole("Administrator")) throw new ArgumentException("User not in role 'Administrator'!", "IsInRole");
            if (!ContainsChild(child)) _children.Add(child);
        }
        public virtual void RemoveChild(IPrincipal user, Forum child)
        {
            if (user == null) throw new ArgumentNullException("user");
            if (child == null) throw new ArgumentNullException("child");
            if (!user.IsInRole("Administrator")) throw new ArgumentException("User not in role 'Administrator'!", "IsInRole");
            if (ContainsChild(child)) _children.Remove(child);
        }
        public virtual bool ContainsPost(Post post)
        {
            return _posts.Contains(post);
        }
        public virtual void AddPost(IPrincipal user, Post post)
        {
            if (user == null) throw new ArgumentNullException("user");
            if (post == null) throw new ArgumentNullException("post");
            if (!user.IsInRole("Administrator")) throw new ArgumentException("User not in role 'Administrator'!", "IsInRole");
            if (!ContainsPost(post)) _posts.Add(post);
        }
        public virtual void RemovePost(IPrincipal user, Post post)
        {
            if (user == null) throw new ArgumentNullException("user");
            if (post == null) throw new ArgumentNullException("post");
            if (!user.IsInRole("Administrator")) throw new ArgumentException("User not in role 'Administrator'!", "IsInRole");
            if (ContainsPost(post)) _posts.Remove(post);
        }
    }
}

Ok, so tell me, those of you who are silently tracking this…

There’s something I don’t quite get, and it may simply be this particular model, but if we are asked to give a protected default ctor, and a public one with signature values in them like so:

protected Foo() { }
public Foo(string name) { Name = name; }

With the understanding that not even the name is immutable, and can be changed via something like ChangeName(string name), then why do we pass it in at all like that? Why don’t we just have a normal get set routine that checks value before assignment, and keep the public parameterless ctor?

What if none of your models end up having any immutable properties at all? You can’t just chalk it up to bad design, just a lack of domain.

Input?

My first thought looking at it is that I don’t think it’s a good idea to be checking users rights within your domain object. The domain object shouldn’t care about that and the decision to allow that method to be called should be made before it is called - in a different layer.

That’s the question then it seems. Are domain entities merely a validity wrappers around the data, or does it include who can do what? After all, a store clerk is an entity, and they can’t do some of the things a manager can.

Some might think that those kinds of decisions are entirely a part of the domain definition, especially in a layered app, where an alternative client is written for it. Wouldn’t you want to simply write the client ui, and just start using the domain layer?

On the other hand, if HOW you decide who does what changes, then you have a problem if it’s in the domain model.

Interesting issue this is.

Personally I don’t fully understand why so many samples I see make the UI aware of the domain. I’m not sure it’s the right way but I assume there are many arguments out there for and against. The one consistency in DDD seems to be the different opinions about the correct way to do things.

An entity to me is simply representing a real thing in your business be it a person or a box. Don’t confuse it with data at all. In the case of your forum it can be moved from one parent to another so I would agree with putting that method in the object but not the logic to determine if the user of the application is allowed to.

I prefer to have an application services layer and that is the only layer the UI needs to be aware of. If the UI calls the app services to move a forum to another parent then the app services checks the user and proceeds if allowed to move the forum.

You asked your question in terms of DDD but in other terms these may not be the best answers to your specific problems. It may very well be that it suits your needs best to put the logic to check a users’ rights in the entity. Once you do it may not necessarily be considered an application developed using DDD but one developed using some patterns and practices associated with DDD where you as the developer made the choices on what was best for your application.

The only problem with any of this is the “you knowing best” issue. Do you know best because you want to get it done quick and easily or do you know best because you know with absolute certainty that coupling user authentication to an entity will never be a problem?

Me know best? LOL! Anyway, there are times when I could see DDD being a real benefit to a system. For example, a census type application. In it, a Person is an entity with things like SSN, and gender, which will (presumably) never change. Such a Person will also have children, a spouse, material belongings of worth. The question might arise: when something transfers ownership, or a child changes residence, who actually performs this move? In a typical case, there are many different things that happen. One simply does not move. They must move selected goods, assigning new owners to things they leave behind, they must register a new mailing address, and so forth. Wrapping these kinds of transactions within the entity scope seems like a valid way to do things. However, there are also times when I am not sure I really see a benefit. As in my forum application. There simply aren’t any complex transactions that occur. When all that is removed, any domain related methods seem to nothing more than something to aid in moving one child to another. The effort in setting up this kind of solution in a DDD way seems counter productive.

I think the key is in being able to determine if a solution is domain based in the first place, or simply a pretty wrapper around a simple data-centric design. I think my forum app falls into the latter category, but I could be wrong.

In the end though, it isn’t about what’s best really. It’s about what i can grasp, and right now, I am fighting to apply DDD to it. That’s a big clue in itself.

An after thought:

When I put together a puzzle, it’s me doing the arranging, not the pieces. Regardless of how intricate the pieces, or how well the fit together, it’s up to me to assemble them. This is sort of how my mind sees development, and as a result, I feel more comfortable with the service layer handling the logic, manipulating the pieces as needed.

Another thing to consider. Take the following actual code from my app, as it stands right now:

[HttpGet]
public ActionResult Edit(int id)
{
    return View(codeRepository.SelectById(id));
}
[HttpPost, ValidateInput(false)]
public ActionResult Edit(int id, FormCollection form)
{
    Code entity = codeRepository.SelectById(id);
    UpdateModel(entity);
    ConsumeErrors(validationService.Validate(entity));
    if (ModelState.IsValid)
        try
        {
            codeRepository.Commit();
            return RedirectToAction("Manager");
        }
        catch (Exception exception)
        {
            ModelState.AddModelError("_FORM", exception.Message);
        }
    return View(entity);
}

If I am moving the post, the view would have been provided a list of destinations. UpdateModel would’ve set the right forumId. Then it get’s saved. Why should I have to fetch the old and new parent forums, move the item, then save the affected forums? That’s something that never really made sense in the context of this application.