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 = 4711;

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 = 4711;

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 @@ -115,7 +115,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
19 changes: 18 additions & 1 deletion src/NHibernate/Event/Default/DefaultRefreshEventListener.cs
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,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 +144,20 @@ private void EvictCachedCollections(IType[] types, object id, ISessionFactoryImp
}
}
}

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

// TODO: InstrumentationMetadata needs to be in IPersister
var castedPersister = persister as AbstractEntityPersister;
if (castedPersister?.InstrumentationMetadata?.EnhancedForLazyLoading == true)
{
var interceptor = castedPersister.InstrumentationMetadata.ExtractInterceptor(obj);
// The list of initialized lazy fields have to be cleared in order to refresh them from the database.
interceptor?.ClearInitializedLazyFields();
}
}
}
}
16 changes: 15 additions & 1 deletion src/NHibernate/Intercept/AbstractFieldInterceptor.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Iesi.Collections.Generic;
using NHibernate.Engine;
using NHibernate.Persister.Entity;
Expand All @@ -21,7 +22,8 @@ public abstract class AbstractFieldInterceptor : IFieldInterceptor
private readonly HashSet<string> loadedUnwrapProxyFieldNames = new HashSet<string>();
private readonly string entityName;
private readonly System.Type mappedClass;

private readonly string[] originalUninitializedFields;
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.

This seems to be same as uninitializedFieldsReadOnly


[NonSerialized]
private bool initializing;
private bool isDirty;
Expand All @@ -34,6 +36,7 @@ protected internal AbstractFieldInterceptor(ISessionImplementor session, ISet<st
this.entityName = entityName;
this.mappedClass = mappedClass;
this.uninitializedFieldsReadOnly = uninitializedFields != null ? new ReadOnlySet<string>(uninitializedFields) : null;
this.originalUninitializedFields = uninitializedFields != null ? uninitializedFields.ToArray() : null;
}

#region IFieldInterceptor Members
Expand Down Expand Up @@ -209,5 +212,16 @@ public ISet<string> GetUninitializedFields()
{
return uninitializedFieldsReadOnly ?? CollectionHelper.EmptySet<string>();
}

public void ClearInitializedLazyFields()
{
if (this.originalUninitializedFields == null)
return;

foreach (var originalUninitializedField in this.originalUninitializedFields)
{
this.uninitializedFields.Add(originalUninitializedField);
}
}
}
}
2 changes: 2 additions & 0 deletions src/NHibernate/Intercept/IFieldInterceptor.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,8 @@ public interface IFieldInterceptor

/// <summary> Get the MappedClass (field container).</summary>
System.Type MappedClass { get; }

void ClearInitializedLazyFields();
}

public static class FieldInterceptorExtensions
Expand Down