Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -1086,6 +1086,41 @@ void AssertPersons(List<Person> results, bool fetched)
}
}
}

[Test]
public async Task TestRefreshRemovesLazyLoadedPropertiesAsync()
{
using (var outerSession = OpenSession())
{
const string query = "from Person fetch Image where Id = 1";
const string namePostFix = "_MODIFIED";
const int imageLength = 1985;

Person outerPerson = await (outerSession.CreateQuery(query).UniqueResultAsync<Person>());

Assert.That(outerPerson.Name.EndsWith(namePostFix), Is.False); // Normal property
Assert.That(outerPerson.Image.Length, Is.EqualTo(1)); // Lazy Property

// Changing the properties of the person in a different sessions
using (var innerSession = OpenSession())
{
var transaction = innerSession.BeginTransaction();

Person innerPerson = await (innerSession.CreateQuery(query).UniqueResultAsync<Person>());
innerPerson.Image = new byte[imageLength];
innerPerson.Name += namePostFix;
await (innerSession.UpdateAsync(innerPerson));

await (transaction.CommitAsync());
}

// Refreshing the person in the outer session
await (outerSession.RefreshAsync(outerPerson));

Assert.That(outerPerson.Name.EndsWith(namePostFix), Is.True); // Value has changed
Assert.That(outerPerson.Image.Length, Is.EqualTo(imageLength)); // This is still the old value
}
}

private static Person GeneratePerson(int i, Person bestFriend)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -1075,6 +1075,41 @@ void AssertPersons(List<Person> results, bool fetched)
}
}
}

[Test]
public void TestRefreshRemovesLazyLoadedProperties()
{
using (var outerSession = OpenSession())
{
const string query = "from Person fetch Image where Id = 1";
const string namePostFix = "_MODIFIED";
const int imageLength = 1985;

Person outerPerson = outerSession.CreateQuery(query).UniqueResult<Person>();

Assert.That(outerPerson.Name.EndsWith(namePostFix), Is.False); // Normal property
Assert.That(outerPerson.Image.Length, Is.EqualTo(1)); // Lazy Property

// Changing the properties of the person in a different sessions
using (var innerSession = OpenSession())
{
var transaction = innerSession.BeginTransaction();

Person innerPerson = innerSession.CreateQuery(query).UniqueResult<Person>();
innerPerson.Image = new byte[imageLength];
innerPerson.Name += namePostFix;
innerSession.Update(innerPerson);

transaction.Commit();
}

// Refreshing the person in the outer session
outerSession.Refresh(outerPerson);

Assert.That(outerPerson.Name.EndsWith(namePostFix), Is.True); // Value has changed
Assert.That(outerPerson.Image.Length, Is.EqualTo(imageLength)); // This is still the old value
}
}

private static Person GeneratePerson(int i, Person bestFriend)
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
using NHibernate.Cache;
using NHibernate.Engine;
using NHibernate.Impl;
using NHibernate.Intercept;
using NHibernate.Persister.Entity;
using NHibernate.Type;
using NHibernate.Util;
Expand Down Expand Up @@ -115,7 +116,9 @@ public virtual async Task OnRefreshAsync(RefreshEvent @event, IDictionary refres
}

await (EvictCachedCollectionsAsync(persister, id, source.Factory, cancellationToken)).ConfigureAwait(false);


RefreshLazyProperties(persister, obj);

// NH Different behavior : NH-1601
// At this point the entity need the real refresh, all elementes of collections are Refreshed,
// the collection state was evicted, but the PersistentCollection (in the entity state)
Expand Down
17 changes: 16 additions & 1 deletion src/NHibernate/Event/Default/DefaultRefreshEventListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using NHibernate.Cache;
using NHibernate.Engine;
using NHibernate.Impl;
using NHibernate.Intercept;
using NHibernate.Persister.Entity;
using NHibernate.Type;
using NHibernate.Util;
Expand Down Expand Up @@ -97,7 +98,9 @@ public virtual void OnRefresh(RefreshEvent @event, IDictionary refreshedAlready)
}

EvictCachedCollections(persister, id, source.Factory);


RefreshLazyProperties(persister, obj);

// NH Different behavior : NH-1601
// At this point the entity need the real refresh, all elementes of collections are Refreshed,
// the collection state was evicted, but the PersistentCollection (in the entity state)
Expand Down Expand Up @@ -142,5 +145,17 @@ private void EvictCachedCollections(IType[] types, object id, ISessionFactoryImp
}
}
}

private static void RefreshLazyProperties(IEntityPersister persister, object obj)
{
if (obj == null)
return;

if (persister.IsInstrumented)
{
// The list of initialized lazy fields have to be cleared in order to refresh them from the database.
persister.EntityMetamodel.BytecodeEnhancementMetadata.ExtractInterceptor(obj)?.ClearInitializedLazyFields();
}
}
}
}
13 changes: 10 additions & 3 deletions src/NHibernate/Intercept/AbstractFieldInterceptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@
using System.Collections.Generic;
using Iesi.Collections.Generic;
using NHibernate.Engine;
using NHibernate.Persister.Entity;
using NHibernate.Proxy;
using NHibernate.Util;

Expand Down Expand Up @@ -33,7 +32,7 @@ protected internal AbstractFieldInterceptor(ISessionImplementor session, ISet<st
this.unwrapProxyFieldNames = unwrapProxyFieldNames ?? new HashSet<string>();
this.entityName = entityName;
this.mappedClass = mappedClass;
this.uninitializedFieldsReadOnly = uninitializedFields != null ? new ReadOnlySet<string>(uninitializedFields) : null;
this.uninitializedFieldsReadOnly = uninitializedFields != null ? new ReadOnlySet<string>(new HashSet<string>(uninitializedFields)) : null;
Copy link
Member

@hazzik hazzik Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a bug that ReadOnlySet is not really read-only

It was possible to update content of read-only set by updating underlying collection.

Some tests were depend on that behavior.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is definitely an improvement, but I'd like to know more before I change it. So I added an extra variable.

}

#region IFieldInterceptor Members
Expand Down Expand Up @@ -207,7 +206,15 @@ private object InitializeField(string fieldName, object target)

public ISet<string> GetUninitializedFields()
{
return uninitializedFieldsReadOnly ?? CollectionHelper.EmptySet<string>();
return uninitializedFields ?? CollectionHelper.EmptySet<string>();
Copy link
Member

@hazzik hazzik Sep 22, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needed to return uninitializedFields, because some tests were depending on the of ReadOnlySet.

}

public void ClearInitializedLazyFields()
{
if (uninitializedFieldsReadOnly != null)
{
uninitializedFields.UnionWith(uninitializedFieldsReadOnly);
}
}
}
}
9 changes: 9 additions & 0 deletions src/NHibernate/Intercept/IFieldInterceptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -74,5 +74,14 @@ public static object Intercept(this IFieldInterceptor interceptor, object target
return interceptor.Intercept(target, fieldName, value);
#pragma warning restore 618
}

// 6.0 TODO: merge into IFieldInterceptor
public static void ClearInitializedLazyFields(this IFieldInterceptor interceptor)
{
if (interceptor is AbstractFieldInterceptor fieldInterceptor)
{
fieldInterceptor.ClearInitializedLazyFields();
}
}
}
}
Loading