From 5ecf1d6507761272d79b475f7872565b62eafd1b Mon Sep 17 00:00:00 2001 From: Thomas Alken Date: Mon, 8 Sep 2025 12:11:34 +0200 Subject: [PATCH 01/11] 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 02/11] 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) From 7364a960630abe16c6b2a4492311d660a6895483 Mon Sep 17 00:00:00 2001 From: Alex Zaytsev Date: Mon, 22 Sep 2025 13:31:47 +1000 Subject: [PATCH 03/11] Fix: ReadOnlySet is not really read-only It is possible to update content of read-only set by updating underlying collection. It is expected that some tests will fail because they depend on that behavior. --- src/NHibernate/Intercept/AbstractFieldInterceptor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NHibernate/Intercept/AbstractFieldInterceptor.cs b/src/NHibernate/Intercept/AbstractFieldInterceptor.cs index 573081db4ae..e6f70f68654 100644 --- a/src/NHibernate/Intercept/AbstractFieldInterceptor.cs +++ b/src/NHibernate/Intercept/AbstractFieldInterceptor.cs @@ -35,7 +35,7 @@ protected internal AbstractFieldInterceptor(ISessionImplementor session, ISet(); this.entityName = entityName; this.mappedClass = mappedClass; - this.uninitializedFieldsReadOnly = uninitializedFields != null ? new ReadOnlySet(uninitializedFields) : null; + this.uninitializedFieldsReadOnly = uninitializedFields != null ? new ReadOnlySet(new HashSet(uninitializedFields)) : null; this.originalUninitializedFields = uninitializedFields != null ? uninitializedFields.ToArray() : null; } From 307e60256d490ffc7722fcc5142c3c65878c3c6e Mon Sep 17 00:00:00 2001 From: Alex Zaytsev Date: Mon, 22 Sep 2025 13:53:16 +1000 Subject: [PATCH 04/11] Fix broken tests --- src/NHibernate/Intercept/AbstractFieldInterceptor.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NHibernate/Intercept/AbstractFieldInterceptor.cs b/src/NHibernate/Intercept/AbstractFieldInterceptor.cs index e6f70f68654..cb277c5e4bd 100644 --- a/src/NHibernate/Intercept/AbstractFieldInterceptor.cs +++ b/src/NHibernate/Intercept/AbstractFieldInterceptor.cs @@ -210,7 +210,7 @@ private object InitializeField(string fieldName, object target) public ISet GetUninitializedFields() { - return uninitializedFieldsReadOnly ?? CollectionHelper.EmptySet(); + return uninitializedFields ?? CollectionHelper.EmptySet(); } public void ClearInitializedLazyFields() From 73c33c63637dac06edee398a735d75f2ddb2b340 Mon Sep 17 00:00:00 2001 From: Alex Zaytsev Date: Mon, 22 Sep 2025 14:15:10 +1000 Subject: [PATCH 05/11] Fix Oracle --- .../FetchLazyProperties/FetchLazyPropertiesFixture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs b/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs index 01f4cfd1498..424115a63e3 100644 --- a/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs +++ b/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs @@ -1083,7 +1083,7 @@ public void TestRefreshRemovesLazyLoadedProperties() { const string query = "from Person fetch Image where Id = 1"; const string namePostFix = "_MODIFIED"; - const int imageLength = 4711; + const int imageLength = 1985; Person outerPerson = outerSession.CreateQuery(query).UniqueResult(); From 8cfda99021925425a40c09c2264310ddf20e0923 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 22 Sep 2025 04:18:45 +0000 Subject: [PATCH 06/11] Generate async files --- .../Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs b/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs index 3bf71503332..0955bbbd949 100644 --- a/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs +++ b/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs @@ -1094,7 +1094,7 @@ public async Task TestRefreshRemovesLazyLoadedPropertiesAsync() { const string query = "from Person fetch Image where Id = 1"; const string namePostFix = "_MODIFIED"; - const int imageLength = 4711; + const int imageLength = 1985; Person outerPerson = await (outerSession.CreateQuery(query).UniqueResultAsync()); From 4227bf059bfb74170f5e167fe390bfc85ac16809 Mon Sep 17 00:00:00 2001 From: Alex Zaytsev Date: Mon, 22 Sep 2025 14:17:52 +1000 Subject: [PATCH 07/11] Resolve TODO --- .../Event/Default/DefaultRefreshEventListener.cs | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/NHibernate/Event/Default/DefaultRefreshEventListener.cs b/src/NHibernate/Event/Default/DefaultRefreshEventListener.cs index 86a4290af5b..c78901d414d 100644 --- a/src/NHibernate/Event/Default/DefaultRefreshEventListener.cs +++ b/src/NHibernate/Event/Default/DefaultRefreshEventListener.cs @@ -144,19 +144,16 @@ 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) + + if (persister.IsInstrumented) { - 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(); + persister.EntityMetamodel.BytecodeEnhancementMetadata.ExtractInterceptor(obj)?.ClearInitializedLazyFields(); } } } From 21359b2c108e66219ce5be780ec5128ee7c19006 Mon Sep 17 00:00:00 2001 From: Alex Zaytsev Date: Mon, 22 Sep 2025 14:27:44 +1000 Subject: [PATCH 08/11] Remove breaking changes --- src/NHibernate/Intercept/IFieldInterceptor.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/NHibernate/Intercept/IFieldInterceptor.cs b/src/NHibernate/Intercept/IFieldInterceptor.cs index 2deee4470eb..4a2900b6f72 100644 --- a/src/NHibernate/Intercept/IFieldInterceptor.cs +++ b/src/NHibernate/Intercept/IFieldInterceptor.cs @@ -41,8 +41,6 @@ public interface IFieldInterceptor /// Get the MappedClass (field container). System.Type MappedClass { get; } - - void ClearInitializedLazyFields(); } public static class FieldInterceptorExtensions @@ -76,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(); + } + } } } From bf0ee3b75e56732b5071548b02c832b02f679735 Mon Sep 17 00:00:00 2001 From: Alex Zaytsev Date: Mon, 22 Sep 2025 14:29:37 +1000 Subject: [PATCH 09/11] Collapse uninitializedFieldsReadOnly and originalUninitializedFields --- .../Intercept/AbstractFieldInterceptor.cs | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/src/NHibernate/Intercept/AbstractFieldInterceptor.cs b/src/NHibernate/Intercept/AbstractFieldInterceptor.cs index cb277c5e4bd..2198dbd3668 100644 --- a/src/NHibernate/Intercept/AbstractFieldInterceptor.cs +++ b/src/NHibernate/Intercept/AbstractFieldInterceptor.cs @@ -1,9 +1,7 @@ using System; using System.Collections.Generic; -using System.Linq; using Iesi.Collections.Generic; using NHibernate.Engine; -using NHibernate.Persister.Entity; using NHibernate.Proxy; using NHibernate.Util; @@ -22,8 +20,7 @@ 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; @@ -36,7 +33,6 @@ protected internal AbstractFieldInterceptor(ISessionImplementor session, ISet(new HashSet(uninitializedFields)) : null; - this.originalUninitializedFields = uninitializedFields != null ? uninitializedFields.ToArray() : null; } #region IFieldInterceptor Members @@ -215,12 +211,9 @@ public ISet GetUninitializedFields() public void ClearInitializedLazyFields() { - if (this.originalUninitializedFields == null) - return; - - foreach (var originalUninitializedField in this.originalUninitializedFields) + if (uninitializedFieldsReadOnly != null) { - this.uninitializedFields.Add(originalUninitializedField); + uninitializedFields.UnionWith(uninitializedFieldsReadOnly); } } } From 677a63c9b017cc9ba9110e9fad53c67b4db9edaf Mon Sep 17 00:00:00 2001 From: Alex Zaytsev Date: Mon, 22 Sep 2025 15:31:35 +1000 Subject: [PATCH 10/11] fixup! Remove breaking changes --- src/NHibernate/Event/Default/DefaultRefreshEventListener.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NHibernate/Event/Default/DefaultRefreshEventListener.cs b/src/NHibernate/Event/Default/DefaultRefreshEventListener.cs index c78901d414d..ff77ae97072 100644 --- a/src/NHibernate/Event/Default/DefaultRefreshEventListener.cs +++ b/src/NHibernate/Event/Default/DefaultRefreshEventListener.cs @@ -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; From c4dbd298a7ed2e2bbc3e462cbc8845b1c252e54c Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" Date: Mon, 22 Sep 2025 05:36:16 +0000 Subject: [PATCH 11/11] Generate async files --- .../Async/Event/Default/DefaultRefreshEventListener.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NHibernate/Async/Event/Default/DefaultRefreshEventListener.cs b/src/NHibernate/Async/Event/Default/DefaultRefreshEventListener.cs index e3ab9929cd5..6047a82c2d1 100644 --- a/src/NHibernate/Async/Event/Default/DefaultRefreshEventListener.cs +++ b/src/NHibernate/Async/Event/Default/DefaultRefreshEventListener.cs @@ -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;