While searching the web, I came across a list of rules from Eric Evans\' book that should be enforced for aggregates:
One thing you could do is give a copy of the internal state to the outside world.
Technically I don't think there is a way to prevent an external object from holding on to the reference beyond a single method or block. I guess you just have to force this rule in your design.
How to enforce it (or even what's possible), I think, is largely dependent on how you're going to persiste it. For example, you're using NHibernate which I think means that everything in the domain object has to be accessible in order for it to be mapped vs. Event Sourcing where the only thing that matters to rebuild th object state is the events themselves, which makes it easier to rebuild internal objects that are not accessible through the public interface.
:: I use GUID for the identity. Never use a PK. Ever. I also happen to use GUIDs for any non-root entities, but you could just as well use a string.
:: This is where persistence implementation matters. I'm event sourced, I can use the events to rebuild objects within the root that are not accessible by the root's public interface. So in C#, I simply mark all non-root entities as internal, and all access to those are proxied through the root's public interface. Since my domain is in its own assembly, no clients can ever get a reference to non-root entities, nor will the compiler allow me to accidentally do it. If I need to expose properties, I simply ensure that they are readonly/get-only. If you're using an ORM, then this might be impossible, I'm not sure. If you can grant access to internal
to NHibernate, then that could open some doors, but it still limits you in many aspects. My solution in this case would be to create a pair of methods that mimic the Event Snapshot (what you'd have if you were event sourced), which essentially spit out a DTO containing state that NHibernate could use, and also accepts the same DTO to restore state to the object. If possible, make sure those are only accessible by the repository.
From within the domain (domain objects referencing other domain objects), it just becomes a discipline (code reviewed) that non-root entities should only ever be present within roots. If you set up your namespaces correctly, you can use Visual Studio's dependency validation to prevent the project from building when this rule is violated.
:: I mark my non-root entities with IEntity which simply has an ID as part of the interface. Then I create an AggregateRoot abstract class which implements IEntity. This obeys the characteristic "An aggregate root is an entity within the aggregate". Then my repositories only accept or return instances of AggregateRoot. This is enforced by the repository abstraction, using generics as constraints, so it basically cannot be violated without some obvious shennaniganry. See next comment for "traversal"
:: The key word is "references". This really just means an ID. So for example, when you "add" an instance of RootB to RootA, then RootA should only capture the ID of RootB, and it gets saved that way. So now if you need to get back that of RootB from RootA, then you need to ask RootA to give you the ID, then you use that to look up RootB in a subsequent query.
:: This is pretty straight forward, but it is also very dependent on the business case. For example, let's say that through the root, I created a configuration. As a result of the configuration, several resource files were created. If I delete the configuration through the root, then those resource files should be deleted as well. In most cases, if your root-persistence is setup correctly, this will take care of itself. However, in terms of invariants, you may run into something more complex. For example, if you had a manager entity that was a root, and that manager had many employees reporting to it, then by deleting the manager, many actions may be required to complete the process in business terms. For example, perhaps those employees need to have a "reports to" field nulled out. This is a more complicated topic because there are a lot of system design factors involved. For example, are you event sourced, is it an event driven system or synchronous, etc. There could be a hundred different ways to solve that problem. I think the main point here is that the Aggregate Root is responsible for making sure it happens, or at least that the process got started.
:: See the previous comment about managers and employees. This basically just means that before the root can be saved, all business rules must have been enforced. I enforce this by making sure than when you run ActionA(), if any business rule fails within the aggregate or its non-root entities, or value objects or ANYTHING along the line, then I throw an exception. This prevents the final commit from happening since the original Action() never completes. For this to work, you must make sure your handler (whatever starts this action) doesn't try to save prematurely. To emulate a transaction, I usually wait until the very end of the operation (or chain of operations) before I attempt to save anything. If your bounded context is nice an neat, you should really only have to save a single entity (the root) at the end of the operation, since it IS the root.
There are cases where you might have a few roots to save, but you'll have to figure out how to roll that transaction back. Those snapshots I mentioned could make that trivial. For example, you get root A and B and you save their snapshots (mementos), then you perform the operation. Then you try to save RootA and it suceeds. You try to save RootB but an exception is thrown (maybe the connection fails or something). After some failed retry logic, you use the snapshot to restore RootB and then resave it, then rethrow the exception so it shows up in the logs as a fatal exception. If for some reason, you can't restore and save RootA (database is down now -- crappy timing), then you simply log the memento out with your log so that it can be manually restored later (e.g. queue it for restoration). Some don't like the idea of throwing exceptions in the domain for a business rule violation and argue that you should use events for that (see: exceptions should be exceptional), and I don't disagree. I'm simply more comfortable with this approach at the moment. There's a million ways you can do this, but it's not really a DDD concern, I'm just offering some ideas on how you could leverage the construct to solve those inevitable questions/problems.
I know this is 8 years late, but I hope it helps someone out there.
My favourite way of enforcing DDD patterns and practices is constantly educating people about their value. There are however moments when I with I had a more rigorous tool.
I haven't done this myself yet, but is seems to me that FluentNHibernate could be a good tool for enforcing aggregate properties.
Your example could be implemented by marking all aggregate roots with 'IAggregateRoot' marker interface and non-root entities with 'IEntity' marker interface. Then, your custom FNH convention would check for entities marked as IEntity referencing entities IEntity and when found, would signal an error (throw an exception for example).
Does it make any sense?
I don't think you should let the aggregate give your external code access to it's entities.
You tell your aggregate what you want to happen and it deals with it.
If we have an aggregate:Car. We don't care about petrol, and wheels, we just drive. We ask the car about things and it answers without giving references to the internals.
We ask: Do we have petrol? Yes. Not: Give me the tank object so I can check if we have petrol.