One of the things you usually end up wanting to configure on a case-to-case basis when using NHibernate is cascading – i.e. which relations should NHibernate take responsiblity for saving.
An example could be in DDD terms where you have an aggregate root, that contains some stuff. Let’s take an example with a Band which is the subject of the BandMember role, which in turn references another aggregate root, User, that is the object of the band member role.
It is OK for us that we need to save a User and a Band in a repository, that’s the idea when dealing with aggregate roots. But everything beneath an aggregate root should be automatically persisted, and the root should be capable of creating/deleting stuff beneath itself without any references to repositories and stuff like that.
So how do we do that with NHibernate? Well, it just so happens that NHibernate can be configured to cascade calls to save, delete, etc. through relations, thus allowing the aggregate root to logically “contain” its entities.
This is all fine and dandy, but when Fluent NHibernate is doing its automapping, we need to be able to give some hints when we want cascading to happen. I usually want to be pretty persistence ignorant, BUT sometimes I just want to be able to do stuff quickly and just get stuff done, so I usually end up “polluting” my domain model with a few attributes that give hints to a convention I use.
Consider this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 |
public class Band : EntityBase { public Band() { BandMembers = new List<BandMember>(); } public virtual string Name { get; set; } [Cascade] public virtual IList<BandMember> BandMembers { get; set; } public virtual void AddBandMember(User user, BandMemberType bandMemberType) { var bandMember = FindExistingBandMember(user) ?? new BandMember(user); bandMember.BandMemberType = bandMemberType; } public virtual void RemoveBandMember(User user) { BandMembers.Remove(FindExistingBandMember(user)); } BandMember FindExistingBandMember(User user) { return BandMembers.ToList().Find(b => b.User == user); } } public enum BandMemberType { Member, Administrator } public class BandMember : EntityBase { public BandMember(User user) { User = user; } public virtual BandMemberType BandMemberType { get; set; } public virtual User User { get; set; } } public class User : EntityBase { public virtual string Name { get; set; } } |
Notice that little [Cascade]-thingie in there? It’s implemented like this:
1 2 3 |
public class CascadeAttribute : Attribute { } |
Trivial – but I want that to be spotted by Fluent NHibernate to make the BandMembers collection into a cascade="all-delete-orphan", which in turn will cause the methods AddBandMember and RemoveBandMember to be able to update the DB.
I do this with a CascadeConvention, which is my implementation of the IHasManyConvention and IReferenceConvention interfaces. It looks like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 |
public class CascadeConvention : IHasManyConvention, IReferenceConvention { public void Apply(IManyToOneInstance instance) { var property = instance.Property; if (!HasAttribute(property)) return; Console.WriteLine("CascadeAll on {0}.{1}", property.DeclaringType.Name, property.Name); instance.Cascade.All(); } public void Apply(IOneToManyCollectionInstance instance) { var property = instance.Member; if (!HasAttribute(property)) return; Console.WriteLine("CascadeAllDeleteOrphan on {0}.{1}", property.DeclaringType.Name, property.Name); instance.Cascade.AllDeleteOrphan(); } bool HasAttribute(ICustomAttributeProvider provider) { return provider.GetCustomAttributes(typeof(CascadeAttribute), false).Length == 1 } } |
What is left now is to make sure Fluent NHibernate picks up my convention and uses it. I usually do this by throwing them into the same assembly as my entities and do something like this when configuring FNH:
1 2 3 4 |
new AutoPersistenceModel() // (...) .Conventions.Setup(s => s.AddFromAssemblyOf<EntityBase>()) .Configure(configuration); |
– which will cause all conventions residing in the same assembly as EntityBase to be used.
This works really good for me, because it makes it really easy and quick to configure cascading where it’s needed – I don’t have to look deep into an .hbm.xml file somewhere or try to figure out how cascading might be configured somewhere else – the configuration is right where it’s relevant and needed.
Extremist PI-kinds-of-guys might not want to pollute their domain models with attributes, so they might want to use another way of specifying where cascading should be applied. Another approach I have tinkered with, is to let all my entities be subclasses of an appropariate base class – like e.g. AggregateRoot (implies no cascading), Aggregate (implies cascading), and Component (implies that the class is embedded in the entity).
The great thing about Fluent NHibernate is that it’s entirely up to you to decide what kind of intrusion offends you the least 🙂
Great article. I’ve now implemented a EntityBase type and the CascadeConvention and it works just fine.
It has simplified my test-setups quite a bit:
from:
var session = sessionProvider.CurrentSession;
var inventory = new ActivityInventory();
var activity1 = new Activity() {Title = “a1”, State = ActivityStateType.Ready};
//session.SaveOrUpdate(activity1);
inventory.Activities.Add(activity1);
//activity1.ActivityInventory = inventory;
var activity2 = new Activity() {Title = “a2”, State = ActivityStateType.Completed, CompletedAt = DateTime.Now.AddDays(-1)};
//session.SaveOrUpdate(activity2);
inventory.Activities.Add(activity2);
//activity2.ActivityInventory = inventory;
var activity3 = new Activity() {Title = “a3”, State = ActivityStateType.Completed, CompletedAt = DateTime.Now};
//session.SaveOrUpdate(activity3);
inventory.Activities.Add(activity3);
//activity3.ActivityInventory = inventory;
session.SaveOrUpdate(inventory);
to:
var inventory = new ActivityInventory()
{
Activities =
{
new Activity() {Title = “a1”, State = ActivityStateType.Ready},
new Activity() { Title = “a2”, State = ActivityStateType.Completed, CompletedAt = DateTime.Now.AddDays(-1) },
new Activity() { Title = “a3”, State = ActivityStateType.Completed, CompletedAt = DateTime.Now }
}
};
session.SaveOrUpdate(inventory);
thanks!
Fantastic! 🙂