Skip to content

Undocumented breaking change in 5.5.0, Session.Save now calls GetHashCode of related entities? #3468

@cremor

Description

@cremor

After updating from NHibernate 5.4.7 to 5.5.0 some of the tests in my application started failing. It looks like this is because of an undocumented breaking change.

NHibernate 5.5.0 now calls the GetHashCode method of entities which are implicitly saved by having a cascaded save from an entity that is saved by calling session.Save. Simplified model:

// An object of this class is passed to 'session.Save'
public class ParentEntity
{
    // Mapped with a sequence generator
    public virtual long Id { get; set; }

    // Mapped as a set with 'all-delete-orphan'
    public virtual ICollection<SubEntity> SubEntities { get; set; }
}

public class SubEntity
{
    // Mapped with a sequence generator
    public virtual long Id { get; set; }
}

The GetHashCode method of SubEntity is called in this case.

The call stack looks like this:

 	MyNamespace.Infrastructure.Entity<long>.GetHashCode()
 	System.Collections.Generic.ObjectEqualityComparer<System.__Canon>.GetHashCode(System.__Canon obj)
 	System.Collections.Generic.HashSet<MyEntityType>.InternalGetHashCode(MyEntityType item)
 	System.Collections.Generic.HashSet<MyEntityType>.AddIfNotPresent(MyEntityType value)
 	System.Collections.Generic.HashSet<MyEntityType>.UnionWith(System.Collections.Generic.IEnumerable<MyEntityType> other)
 	System.Collections.Generic.HashSet<MyEntityType>.HashSet(System.Collections.Generic.IEnumerable<MyEntityType> collection, System.Collections.Generic.IEqualityComparer<MyEntityType> comparer)
 	NHibernate.Type.GenericSetType<MyEntityType>.Wrap(NHibernate.Engine.ISessionImplementor session, object collection)
 	NHibernate.Event.Default.WrapVisitor.ProcessArrayOrNewCollection(object collection, NHibernate.Type.CollectionType collectionType)
 	NHibernate.Event.Default.WrapVisitor.ProcessComponent(object component, NHibernate.Type.IAbstractComponentType componentType)
 	NHibernate.Event.Default.WrapVisitor.ProcessValue(int i, object[] values, NHibernate.Type.IType[] types)
 	NHibernate.Event.Default.AbstractVisitor.ProcessEntityPropertyValues(object[] values, NHibernate.Type.IType[] types)
 	NHibernate.Event.Default.AbstractSaveEventListener.VisitCollectionsBeforeSave(object entity, object id, object[] values, NHibernate.Type.IType[] types, NHibernate.Event.IEventSource source)
 	NHibernate.Event.Default.AbstractSaveEventListener.PerformSaveOrReplicate(object entity, NHibernate.Engine.EntityKey key, NHibernate.Persister.Entity.IEntityPersister persister, bool useIdentityColumn, object anything, NHibernate.Event.IEventSource source, bool requiresImmediateIdAccess)
 	NHibernate.Event.Default.AbstractSaveEventListener.PerformSave(object entity, object id, NHibernate.Persister.Entity.IEntityPersister persister, bool useIdentityColumn, object anything, NHibernate.Event.IEventSource source, bool requiresImmediateIdAccess)
 	NHibernate.Event.Default.AbstractSaveEventListener.SaveWithGeneratedId(object entity, string entityName, object anything, NHibernate.Event.IEventSource source, bool requiresImmediateIdAccess)
 	NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.SaveWithGeneratedOrRequestedId(NHibernate.Event.SaveOrUpdateEvent event)
 	NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.EntityIsTransient(NHibernate.Event.SaveOrUpdateEvent event)
 	NHibernate.Event.Default.DefaultSaveOrUpdateEventListener.OnSaveOrUpdate(NHibernate.Event.SaveOrUpdateEvent event)
 	NHibernate.Impl.SessionImpl.FireSave(NHibernate.Event.SaveOrUpdateEvent event)
 	NHibernate.Impl.SessionImpl.Save(object obj)

With NHibernate 5.4.7 this call to GetHashCode via session.Save didn't happen at all! Instead GetHashCode is only called by session.Flush with this call stack:

 	MyNamespace.Infrastructure.Entity<long>.GetHashCode()
 	System.Collections.Generic.ObjectEqualityComparer<System.__Canon>.GetHashCode(System.__Canon obj)
 	System.Collections.Generic.HashSet<MyEntityType>.InternalGetHashCode(MyEntityType item)
 	System.Collections.Generic.HashSet<MyEntityType>.AddIfNotPresent(MyEntityType value)
 	System.Collections.Generic.HashSet<MyEntityType>.UnionWith(System.Collections.Generic.IEnumerable<MyEntityType> other)
 	System.Collections.Generic.HashSet<MyEntityType>.HashSet(System.Collections.Generic.IEnumerable<MyEntityType> collection, System.Collections.Generic.IEqualityComparer<MyEntityType> comparer)
 	NHibernate.Type.GenericSetType<MyEntityType>.Wrap(NHibernate.Engine.ISessionImplementor session, object collection)
 	NHibernate.Event.Default.WrapVisitor.ProcessArrayOrNewCollection(object collection, NHibernate.Type.CollectionType collectionType)
 	NHibernate.Event.Default.WrapVisitor.ProcessComponent(object component, NHibernate.Type.IAbstractComponentType componentType)
 	NHibernate.Event.Default.WrapVisitor.ProcessValue(int i, object[] values, NHibernate.Type.IType[] types)
 	NHibernate.Event.Default.AbstractVisitor.ProcessEntityPropertyValues(object[] values, NHibernate.Type.IType[] types)
 	NHibernate.Event.Default.DefaultFlushEntityEventListener.WrapCollections(NHibernate.Event.IEventSource session, NHibernate.Persister.Entity.IEntityPersister persister, NHibernate.Type.IType[] types, object[] values)
 	NHibernate.Event.Default.DefaultFlushEntityEventListener.OnFlushEntity(NHibernate.Event.FlushEntityEvent event)
 	NHibernate.Event.Default.AbstractFlushingEventListener.FlushEntities(NHibernate.Event.FlushEvent event)
 	NHibernate.Event.Default.AbstractFlushingEventListener.FlushEverythingToExecutions(NHibernate.Event.FlushEvent event)
 	NHibernate.Event.Default.DefaultFlushEventListener.OnFlush(NHibernate.Event.FlushEvent event)
 	NHibernate.Impl.SessionImpl.Flush()

The difference is that in the NHibernate 5.4.7 case the generated Id is already set on the entity when GetHashCode is called. But in the NHibernate 5.5.0 case the Id is not generated yet. (It is mapped with a sequence generator in my case.)

Was this changed on purpose? If yes, it should be documented (and I have to find a fix for my application 😢)
If not, then please revert the change.

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions