From 5ecf1d6507761272d79b475f7872565b62eafd1b Mon Sep 17 00:00:00 2001 From: Thomas Alken Date: Mon, 8 Sep 2025 12:11:34 +0200 Subject: [PATCH 1/2] Fixes issue 3622: ISession.Refresh updates initialized lazy properties --- .../FetchLazyPropertiesFixture.cs | 35 +++++++++++++++++++ .../Default/DefaultRefreshEventListener.cs | 19 +++++++++- .../Intercept/AbstractFieldInterceptor.cs | 16 ++++++++- src/NHibernate/Intercept/IFieldInterceptor.cs | 2 ++ 4 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs b/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs index 1fd3b8ed063..01f4cfd1498 100644 --- a/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs +++ b/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs @@ -1075,6 +1075,41 @@ void AssertPersons(List 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(); + + 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(); + 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) { diff --git a/src/NHibernate/Event/Default/DefaultRefreshEventListener.cs b/src/NHibernate/Event/Default/DefaultRefreshEventListener.cs index 351fae84db9..86a4290af5b 100644 --- a/src/NHibernate/Event/Default/DefaultRefreshEventListener.cs +++ b/src/NHibernate/Event/Default/DefaultRefreshEventListener.cs @@ -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) @@ -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(); + } + } } } diff --git a/src/NHibernate/Intercept/AbstractFieldInterceptor.cs b/src/NHibernate/Intercept/AbstractFieldInterceptor.cs index 898805360d9..573081db4ae 100644 --- a/src/NHibernate/Intercept/AbstractFieldInterceptor.cs +++ b/src/NHibernate/Intercept/AbstractFieldInterceptor.cs @@ -1,5 +1,6 @@ using System; using System.Collections.Generic; +using System.Linq; using Iesi.Collections.Generic; using NHibernate.Engine; using NHibernate.Persister.Entity; @@ -21,7 +22,8 @@ public abstract class AbstractFieldInterceptor : IFieldInterceptor private readonly HashSet loadedUnwrapProxyFieldNames = new HashSet(); private readonly string entityName; private readonly System.Type mappedClass; - + private readonly string[] originalUninitializedFields; + [NonSerialized] private bool initializing; private bool isDirty; @@ -34,6 +36,7 @@ protected internal AbstractFieldInterceptor(ISessionImplementor session, ISet(uninitializedFields) : null; + this.originalUninitializedFields = uninitializedFields != null ? uninitializedFields.ToArray() : null; } #region IFieldInterceptor Members @@ -209,5 +212,16 @@ public ISet GetUninitializedFields() { return uninitializedFieldsReadOnly ?? CollectionHelper.EmptySet(); } + + public void ClearInitializedLazyFields() + { + if (this.originalUninitializedFields == null) + return; + + foreach (var originalUninitializedField in this.originalUninitializedFields) + { + this.uninitializedFields.Add(originalUninitializedField); + } + } } } diff --git a/src/NHibernate/Intercept/IFieldInterceptor.cs b/src/NHibernate/Intercept/IFieldInterceptor.cs index 1242ce27c95..2deee4470eb 100644 --- a/src/NHibernate/Intercept/IFieldInterceptor.cs +++ b/src/NHibernate/Intercept/IFieldInterceptor.cs @@ -41,6 +41,8 @@ public interface IFieldInterceptor /// Get the MappedClass (field container). System.Type MappedClass { get; } + + void ClearInitializedLazyFields(); } public static class FieldInterceptorExtensions From ffc8a795e6500d9b197319c92bc777777bcff439 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 8 Sep 2025 10:14:33 +0000 Subject: [PATCH 2/2] Generate async files --- .../FetchLazyPropertiesFixture.cs | 35 +++++++++++++++++++ .../Default/DefaultRefreshEventListener.cs | 4 ++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs b/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs index aeb9eb71a9d..3bf71503332 100644 --- a/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs +++ b/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs @@ -1086,6 +1086,41 @@ void AssertPersons(List 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()); + + 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()); + 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) { diff --git a/src/NHibernate/Async/Event/Default/DefaultRefreshEventListener.cs b/src/NHibernate/Async/Event/Default/DefaultRefreshEventListener.cs index 080c96a70b6..e3ab9929cd5 100644 --- a/src/NHibernate/Async/Event/Default/DefaultRefreshEventListener.cs +++ b/src/NHibernate/Async/Event/Default/DefaultRefreshEventListener.cs @@ -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)