diff --git a/src/NHibernate.Test/Async/CacheTest/JsonSerializerCacheFixture.cs b/src/NHibernate.Test/Async/CacheTest/JsonSerializerCacheFixture.cs index 01403997a46..d473758ff70 100644 --- a/src/NHibernate.Test/Async/CacheTest/JsonSerializerCacheFixture.cs +++ b/src/NHibernate.Test/Async/CacheTest/JsonSerializerCacheFixture.cs @@ -30,7 +30,8 @@ public class JsonSerializerCacheFixtureAsync : TestCase protected override string[] Mappings => new[] { "CacheTest.ReadOnly.hbm.xml", - "CacheTest.ReadWrite.hbm.xml" + "CacheTest.ReadWrite.hbm.xml", + "CacheTest.NonStrictReadWrite.hbm.xml" }; protected override string MappingsAssembly => "NHibernate.Test"; @@ -46,7 +47,7 @@ protected override void Configure(Configuration configuration) serializer.RegisterType(typeof(Tuple), "tso"); CoreDistributedCacheProvider.DefaultSerializer = serializer; } - + protected override void OnSetUp() { using (var s = Sfi.OpenSession()) @@ -55,36 +56,27 @@ protected override void OnSetUp() var totalItems = 6; for (var i = 1; i <= totalItems; i++) { - var parent = new ReadOnly - { - Name = $"Name{i}" - }; + var parent = new ReadOnly { Name = $"Name{i}" }; for (var j = 1; j <= totalItems; j++) { - var child = new ReadOnlyItem - { - Parent = parent - }; - parent.Items.Add(child); + parent.Items.Add(new ReadOnlyItem { Parent = parent }); } s.Save(parent); } for (var i = 1; i <= totalItems; i++) { - var parent = new ReadWrite - { - Name = $"Name{i}" - }; + var parent = new ReadWrite { Name = $"Name{i}" }; for (var j = 1; j <= totalItems; j++) { - var child = new ReadWriteItem - { - Parent = parent - }; - parent.Items.Add(child); + parent.Items.Add(new ReadWriteItem { Parent = parent }); } s.Save(parent); } + for (var i = 1; i <= totalItems; i++) + { + var parent = new NonStrictReadWrite() { Name = $"Name{i}" }; + s.Save(parent); + } tx.Commit(); } } @@ -98,12 +90,13 @@ protected override void OnTearDown() s.CreateQuery("delete from ReadWriteItem").ExecuteUpdate(); s.CreateQuery("delete from ReadOnly").ExecuteUpdate(); s.CreateQuery("delete from ReadWrite").ExecuteUpdate(); + s.CreateQuery("delete from NonStrictReadWrite").ExecuteUpdate(); tx.Commit(); } // Must rebuild the session factory, CoreDistribted cache being not clearable. RebuildSessionFactory(); } - + [Test] public async Task CacheableScalarSqlQueryWithTransformerAsync() { @@ -532,5 +525,22 @@ public async Task QueryFetchEntityBatchCacheTestAsync(bool future) Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(future ? 2 : 1), "Unexpected cache hit count"); } + + [Test] + public async Task LazyFormulaTestAsync() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var l = await (s.Query().ToListAsync()); + foreach (var item in l) + { + // The lazy formula will puke if equality is not correct (due to comparison of deserialized + // UnfetchedLazyProperty vs LazyPropertyInitializer.UnfetchedProperty + Assert.AreEqual(1, item.Count); + } + await (t.CommitAsync()); + } + } } } diff --git a/src/NHibernate.Test/CacheTest/CacheEntity.cs b/src/NHibernate.Test/CacheTest/CacheEntity.cs new file mode 100644 index 00000000000..cf8669430cf --- /dev/null +++ b/src/NHibernate.Test/CacheTest/CacheEntity.cs @@ -0,0 +1,11 @@ +namespace NHibernate.Test.CacheTest; + +public abstract class CacheEntity +{ + public virtual int Id { get; protected set; } +} + +public abstract class NamedCacheEntity : CacheEntity +{ + public virtual string Name { get; set; } +} diff --git a/src/NHibernate.Test/CacheTest/JsonSerializerCacheFixture.cs b/src/NHibernate.Test/CacheTest/JsonSerializerCacheFixture.cs index bf1009c90d3..ea6e2f8dd30 100644 --- a/src/NHibernate.Test/CacheTest/JsonSerializerCacheFixture.cs +++ b/src/NHibernate.Test/CacheTest/JsonSerializerCacheFixture.cs @@ -19,7 +19,8 @@ public class JsonSerializerCacheFixture : TestCase protected override string[] Mappings => new[] { "CacheTest.ReadOnly.hbm.xml", - "CacheTest.ReadWrite.hbm.xml" + "CacheTest.ReadWrite.hbm.xml", + "CacheTest.NonStrictReadWrite.hbm.xml" }; protected override string MappingsAssembly => "NHibernate.Test"; @@ -35,7 +36,7 @@ protected override void Configure(Configuration configuration) serializer.RegisterType(typeof(Tuple), "tso"); CoreDistributedCacheProvider.DefaultSerializer = serializer; } - + protected override void OnSetUp() { using (var s = Sfi.OpenSession()) @@ -44,36 +45,27 @@ protected override void OnSetUp() var totalItems = 6; for (var i = 1; i <= totalItems; i++) { - var parent = new ReadOnly - { - Name = $"Name{i}" - }; + var parent = new ReadOnly { Name = $"Name{i}" }; for (var j = 1; j <= totalItems; j++) { - var child = new ReadOnlyItem - { - Parent = parent - }; - parent.Items.Add(child); + parent.Items.Add(new ReadOnlyItem { Parent = parent }); } s.Save(parent); } for (var i = 1; i <= totalItems; i++) { - var parent = new ReadWrite - { - Name = $"Name{i}" - }; + var parent = new ReadWrite { Name = $"Name{i}" }; for (var j = 1; j <= totalItems; j++) { - var child = new ReadWriteItem - { - Parent = parent - }; - parent.Items.Add(child); + parent.Items.Add(new ReadWriteItem { Parent = parent }); } s.Save(parent); } + for (var i = 1; i <= totalItems; i++) + { + var parent = new NonStrictReadWrite() { Name = $"Name{i}" }; + s.Save(parent); + } tx.Commit(); } } @@ -87,12 +79,13 @@ protected override void OnTearDown() s.CreateQuery("delete from ReadWriteItem").ExecuteUpdate(); s.CreateQuery("delete from ReadOnly").ExecuteUpdate(); s.CreateQuery("delete from ReadWrite").ExecuteUpdate(); + s.CreateQuery("delete from NonStrictReadWrite").ExecuteUpdate(); tx.Commit(); } // Must rebuild the session factory, CoreDistribted cache being not clearable. RebuildSessionFactory(); } - + [Test] public void CacheableScalarSqlQueryWithTransformer() { @@ -521,5 +514,22 @@ public void QueryFetchEntityBatchCacheTest(bool future) Assert.That(Sfi.Statistics.QueryCacheMissCount, Is.EqualTo(0), "Unexpected cache miss count"); Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(future ? 2 : 1), "Unexpected cache hit count"); } + + [Test] + public void LazyFormulaTest() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var l = s.Query().ToList(); + foreach (var item in l) + { + // The lazy formula will puke if equality is not correct (due to comparison of deserialized + // UnfetchedLazyProperty vs LazyPropertyInitializer.UnfetchedProperty + Assert.AreEqual(1, item.Count); + } + t.Commit(); + } + } } } diff --git a/src/NHibernate.Test/CacheTest/NonStrictReadWrite.cs b/src/NHibernate.Test/CacheTest/NonStrictReadWrite.cs new file mode 100644 index 00000000000..fa3f6f544fc --- /dev/null +++ b/src/NHibernate.Test/CacheTest/NonStrictReadWrite.cs @@ -0,0 +1,7 @@ +namespace NHibernate.Test.CacheTest +{ + public class NonStrictReadWrite : NamedCacheEntity + { + public virtual int Count { get; set; } + } +} diff --git a/src/NHibernate.Test/CacheTest/NonStrictReadWrite.hbm.xml b/src/NHibernate.Test/CacheTest/NonStrictReadWrite.hbm.xml new file mode 100644 index 00000000000..09fc23c3ed5 --- /dev/null +++ b/src/NHibernate.Test/CacheTest/NonStrictReadWrite.hbm.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/CacheTest/ReadOnly.cs b/src/NHibernate.Test/CacheTest/ReadOnly.cs index c509e0cc2ec..b1ac5ee0742 100644 --- a/src/NHibernate.Test/CacheTest/ReadOnly.cs +++ b/src/NHibernate.Test/CacheTest/ReadOnly.cs @@ -1,15 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; namespace NHibernate.Test.CacheTest { - public class ReadOnly : CacheEntity + public class ReadOnly : NamedCacheEntity { - public virtual string Name { get; set; } - public virtual ISet Items { get; set; } = new HashSet(); } @@ -17,9 +11,4 @@ public class ReadOnlyItem : CacheEntity { public virtual ReadOnly Parent { get; set; } } - - public abstract class CacheEntity - { - public virtual int Id { get; protected set; } - } } diff --git a/src/NHibernate.Test/CacheTest/ReadWrite.cs b/src/NHibernate.Test/CacheTest/ReadWrite.cs index f08added0e2..f9875e82038 100644 --- a/src/NHibernate.Test/CacheTest/ReadWrite.cs +++ b/src/NHibernate.Test/CacheTest/ReadWrite.cs @@ -1,15 +1,9 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using System.Text; -using System.Threading.Tasks; +using System.Collections.Generic; namespace NHibernate.Test.CacheTest { - public class ReadWrite : CacheEntity + public class ReadWrite : NamedCacheEntity { - public virtual string Name { get; set; } - public virtual ISet Items { get; set; } = new HashSet(); } diff --git a/src/NHibernate.Test/NHibernate.Test.csproj b/src/NHibernate.Test/NHibernate.Test.csproj index ff728f12045..542f7b68e36 100644 --- a/src/NHibernate.Test/NHibernate.Test.csproj +++ b/src/NHibernate.Test/NHibernate.Test.csproj @@ -52,6 +52,7 @@ + diff --git a/src/NHibernate/Engine/IPersistenceContext.cs b/src/NHibernate/Engine/IPersistenceContext.cs index f23f9731788..05a3070aebe 100644 --- a/src/NHibernate/Engine/IPersistenceContext.cs +++ b/src/NHibernate/Engine/IPersistenceContext.cs @@ -457,7 +457,7 @@ public static EntityEntry AddEntity( existsInDatabase, persister, disableVersionIncrement, - loadedState?.Any(o => o == LazyPropertyInitializer.UnfetchedProperty) == true); + loadedState?.Any(o => Equals(o, LazyPropertyInitializer.UnfetchedProperty)) == true); #pragma warning restore 618 } @@ -505,7 +505,7 @@ public static EntityEntry AddEntry( existsInDatabase, persister, disableVersionIncrement, - loadedState?.Any(o => o == LazyPropertyInitializer.UnfetchedProperty) == true); + loadedState?.Any(o => Equals(o, LazyPropertyInitializer.UnfetchedProperty)) == true); #pragma warning restore 618 } } diff --git a/src/NHibernate/Intercept/ILazyPropertyInitializer.cs b/src/NHibernate/Intercept/ILazyPropertyInitializer.cs index d167f5a56f5..0852d37ca1a 100644 --- a/src/NHibernate/Intercept/ILazyPropertyInitializer.cs +++ b/src/NHibernate/Intercept/ILazyPropertyInitializer.cs @@ -4,8 +4,14 @@ namespace NHibernate.Intercept { [Serializable] - public struct UnfetchedLazyProperty + public struct UnfetchedLazyProperty : IEquatable { + /// As long as the other instance is the same type, it's considered equal. This + /// avoids the issue where a deserialized value fails the base Object.Equals() check due to + /// both objects being different references. + public bool Equals(UnfetchedLazyProperty other) => true; + public override bool Equals(object obj) => obj is UnfetchedLazyProperty; + public override int GetHashCode() => typeof(UnfetchedLazyProperty).GetHashCode(); } public struct LazyPropertyInitializer @@ -20,4 +26,4 @@ public interface ILazyPropertyInitializer /// Initialize the property, and return its new value object InitializeLazyProperty(string fieldName, object entity, ISessionImplementor session); } -} \ No newline at end of file +} diff --git a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs index 77729249b62..f00daf1af31 100644 --- a/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs +++ b/src/NHibernate/Persister/Entity/AbstractEntityPersister.cs @@ -1396,7 +1396,7 @@ public virtual object InitializeLazyProperty(string fieldName, object entity, IS { CacheEntry cacheEntry = (CacheEntry)CacheEntryStructure.Destructure(ce, factory); var initializedValue = InitializeLazyPropertiesFromCache(fieldName, entity, session, entry, cacheEntry, uninitializedLazyProperties); - if (initializedValue != LazyPropertyInitializer.UnfetchedProperty) + if (!Equals(initializedValue, LazyPropertyInitializer.UnfetchedProperty)) { // NOTE EARLY EXIT!!! return initializedValue; @@ -1567,7 +1567,7 @@ private object InitializeLazyPropertiesFromCache( for (int j = 0; j < lazyPropertyNames.Length; j++) { var cachedValue = disassembledValues[lazyPropertyNumbers[j]]; - if (cachedValue == LazyPropertyInitializer.UnfetchedProperty) + if (Equals(cachedValue, LazyPropertyInitializer.UnfetchedProperty)) { if (fieldName == lazyPropertyNames[j]) { diff --git a/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataPocoImpl.cs b/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataPocoImpl.cs index e0adb041feb..5904bb0132f 100644 --- a/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataPocoImpl.cs +++ b/src/NHibernate/Tuple/Entity/BytecodeEnhancementMetadataPocoImpl.cs @@ -202,7 +202,7 @@ public ISet GetUninitializedLazyProperties(object[] entityState) var uninitializedProperties = new HashSet(); foreach (var propertyDescriptor in LazyPropertiesMetadata.LazyPropertyDescriptors) { - if (entityState[propertyDescriptor.PropertyIndex] == LazyPropertyInitializer.UnfetchedProperty) + if (Equals(entityState[propertyDescriptor.PropertyIndex], LazyPropertyInitializer.UnfetchedProperty)) { uninitializedProperties.Add(propertyDescriptor.Name); }