The solution I propose involves quite a bit of code, but you can just copy it all and past it in a VS test solution assuming you have SqLite installed, and you should be able to
It took me quite a while to get it, but I think the answer to my problem is actually deceptively simple. The best approach, as has been long advocated by the Hibernate team, is just to not override equals and gethashcode. What I did not get was that when I call Contains on a set of business objects, obviously I want to know whether that set contains an object with a specific business value. But that was something I did not get from the Nhibernate persistence set. But Stefan Steinegger put it right in a comment on a different question on this subject I was asking: 'the persistence set is not a business collection'! I completely failed to understand his remark the first time.
The key issue was that I should not try to make the persistence set to behave as a business collection. Instead I should make use of a persistence set wrapped in a business collection. Then things get much easier. So, in my code I created a wrapper:
internal abstract class EntityCollection : IEnumerable
{
private readonly Iesi.Collections.Generic.ISet _set;
private readonly TParent _parent;
private readonly IEqualityComparer _comparer;
protected EntityCollection(Iesi.Collections.Generic.ISet set, TParent parent, IEqualityComparer comparer)
{
_set = set;
_parent = parent;
_comparer = comparer;
}
public IEnumerator GetEnumerator()
{
return _set.GetEnumerator();
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
public bool Contains(TEnt entity)
{
return _set.Any(x => _comparer.Equals(x, entity));
}
internal Iesi.Collections.Generic.ISet GetEntitySet()
{
return _set;
}
internal protected virtual void Add(TEnt entity, Action addParent)
{
if (_set.Contains(entity)) return;
if (Contains(entity)) throw new CannotAddItemException(entity);
_set.Add(entity);
addParent.Invoke(_parent);
}
internal protected virtual void Remove(TEnt entity, Action removeParent)
{
if (_set.Contains(entity)) return;
_set.Remove(entity);
removeParent.Invoke(_parent);
}
}
This is a generic wrapper that implements the business meaning of a set. It knows when two business objects are equal by value through a IEqualityComparer, it presents itself as a true business collection exposing the entity as an enumerable of entity interfaces (much cleaner than exposing the persistence set) and it even knows how to handle bidirectional relations with the parent.
The parent entity that owns this business collection has the following code:
public virtual IEnumerable Products
{
get { return _products; }
}
public virtual Iesi.Collections.Generic.ISet ProductSet
{
get { return _products.GetEntitySet(); }
protected set { _products = new ProductCollection(value, this); }
}
public virtual void AddProduct(IProduct product)
{
_products.Add((Product)product, ((Product)product).SetBrand);
}
public virtual void RemoveProduct(IProduct product)
{
_products.Remove((Product)product, ((Product)product).RemoveFromBrand);
}
So, the entity has in fact two interfaces, a business interface exposing the business collection and a entity interface that is exposed to Nhibernate to deal with persistency of the collection. Note that the same persistence set is returned to Nhibernate as is passed in using the ProductSet property.
It basically all boils down to separation of concerns:
Only, when I want to intermix entities between sessions I would have to resort to other solutions as mentioned above. But I think that if you can avoid that situation, you should.