diff --git a/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs b/src/NHibernate.Test/Async/FetchLazyProperties/FetchLazyPropertiesFixture.cs index aeb9eb71a9d..0955bbbd949 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 = 1985; + + 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.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs b/src/NHibernate.Test/FetchLazyProperties/FetchLazyPropertiesFixture.cs index 1fd3b8ed063..424115a63e3 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 = 1985; + + 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/Async/Event/Default/DefaultRefreshEventListener.cs b/src/NHibernate/Async/Event/Default/DefaultRefreshEventListener.cs index 080c96a70b6..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; @@ -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) diff --git a/src/NHibernate/Event/Default/DefaultRefreshEventListener.cs b/src/NHibernate/Event/Default/DefaultRefreshEventListener.cs index 351fae84db9..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; @@ -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) @@ -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(); + } + } } } diff --git a/src/NHibernate/Intercept/AbstractFieldInterceptor.cs b/src/NHibernate/Intercept/AbstractFieldInterceptor.cs index 898805360d9..2198dbd3668 100644 --- a/src/NHibernate/Intercept/AbstractFieldInterceptor.cs +++ b/src/NHibernate/Intercept/AbstractFieldInterceptor.cs @@ -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; @@ -33,7 +32,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; } #region IFieldInterceptor Members @@ -207,7 +206,15 @@ private object InitializeField(string fieldName, object target) public ISet GetUninitializedFields() { - return uninitializedFieldsReadOnly ?? CollectionHelper.EmptySet(); + return uninitializedFields ?? CollectionHelper.EmptySet(); + } + + public void ClearInitializedLazyFields() + { + if (uninitializedFieldsReadOnly != null) + { + uninitializedFields.UnionWith(uninitializedFieldsReadOnly); + } } } } diff --git a/src/NHibernate/Intercept/IFieldInterceptor.cs b/src/NHibernate/Intercept/IFieldInterceptor.cs index 1242ce27c95..4a2900b6f72 100644 --- a/src/NHibernate/Intercept/IFieldInterceptor.cs +++ b/src/NHibernate/Intercept/IFieldInterceptor.cs @@ -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(); + } + } } }