From 3bfbdb79596e9036d0150f7d48cddb9f14df55be Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericDelaporte@users.noreply.github.com> Date: Mon, 22 Sep 2025 06:02:47 +0200 Subject: [PATCH 1/3] Fix collection cache lookup failure with enum keys (#3693) Co-authored-by: Christoph Watzl (cherry picked from commit 66300c15ef625607aa21a9296bd396027eae83e9) --- .../NHSpecificTest/GH3643/FixtureByCode.cs | 124 ++++++++++++++++++ .../NHSpecificTest/GH3643/Entity.cs | 25 ++++ .../NHSpecificTest/GH3643/FixtureByCode.cs | 112 ++++++++++++++++ src/NHibernate/Async/Type/TypeHelper.cs | 1 + src/NHibernate/Type/TypeHelper.cs | 3 +- 5 files changed, 264 insertions(+), 1 deletion(-) create mode 100644 src/NHibernate.Test/Async/NHSpecificTest/GH3643/FixtureByCode.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH3643/Entity.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH3643/FixtureByCode.cs diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH3643/FixtureByCode.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH3643/FixtureByCode.cs new file mode 100644 index 00000000000..71b43faf92e --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH3643/FixtureByCode.cs @@ -0,0 +1,124 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using System.Linq; +using NHibernate.Cfg; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Linq; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH3643 +{ + using System.Threading.Tasks; + using System.Threading; + [TestFixture] + public class FixtureByCodeAsync : TestCaseMappingByCode + { + protected override void Configure(Configuration configuration) + { + configuration.SetProperty(Environment.UseQueryCache, "true"); + configuration.SetProperty(Environment.GenerateStatistics, "true"); + } + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + + mapper.Class( + rc => + { + rc.Id(x => x.Id); + rc.Bag( + x => x.Children, + m => + { + m.Access(Accessor.Field); + m.Key(k => k.Column("EntityId")); + m.Cascade(Mapping.ByCode.Cascade.All); + }, + r => r.OneToMany()); + + rc.Cache( + cm => + { + cm.Include(CacheInclude.All); + cm.Usage(CacheUsage.ReadWrite); + }); + }); + + mapper.Class( + rc => + { + rc.Id(x => x.Id); + rc.Cache( + cm => + { + cm.Include(CacheInclude.All); + cm.Usage(CacheUsage.ReadWrite); + }); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnSetUp() + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + + var entity = new Entity { Id = EntityId.Id1 }; + entity.Children.Add(new ChildEntity { Id = 0 }); + entity.Children.Add(new ChildEntity { Id = 1 }); + session.Save(entity); + + transaction.Commit(); + } + + protected override void OnTearDown() + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + + session.CreateQuery("delete from ChildEntity").ExecuteUpdate(); + session.CreateQuery("delete from System.Object").ExecuteUpdate(); + + transaction.Commit(); + } + + [Test] + public async Task LoadsEntityWithEnumIdAndChildrenUsingQueryCacheAsync() + { + await (LoadEntityWithQueryCacheAsync()); // warm up cache + + var entity = await (LoadEntityWithQueryCacheAsync()); + + Assert.That(entity.Children.Count(), Is.EqualTo(2)); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + } + + private async Task LoadEntityWithQueryCacheAsync(CancellationToken cancellationToken = default(CancellationToken)) + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + var entity = (await (session + .Query() + .FetchMany(x => x.Children) + .WithOptions(opt => opt.SetCacheable(true)) + .ToListAsync(cancellationToken)))[0]; + + await (transaction.CommitAsync(cancellationToken)); + return entity; + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH3643/Entity.cs b/src/NHibernate.Test/NHSpecificTest/GH3643/Entity.cs new file mode 100644 index 00000000000..09f08d8dbad --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH3643/Entity.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +// ReSharper disable CollectionNeverUpdated.Local +// ReSharper disable UnassignedGetOnlyAutoProperty + +namespace NHibernate.Test.NHSpecificTest.GH3643 +{ + class Entity + { + private readonly ICollection _children = []; + public virtual EntityId Id { get; set; } + public virtual ICollection Children => _children; + } + + class ChildEntity + { + public virtual int Id { get; set; } + } + + enum EntityId + { + Id1, + Id2 + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH3643/FixtureByCode.cs b/src/NHibernate.Test/NHSpecificTest/GH3643/FixtureByCode.cs new file mode 100644 index 00000000000..2fc0741b484 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH3643/FixtureByCode.cs @@ -0,0 +1,112 @@ +using System.Linq; +using NHibernate.Cfg; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Linq; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH3643 +{ + [TestFixture] + public class FixtureByCode : TestCaseMappingByCode + { + protected override void Configure(Configuration configuration) + { + configuration.SetProperty(Environment.UseQueryCache, "true"); + configuration.SetProperty(Environment.GenerateStatistics, "true"); + } + + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + + mapper.Class( + rc => + { + rc.Id(x => x.Id); + rc.Bag( + x => x.Children, + m => + { + m.Access(Accessor.Field); + m.Key(k => k.Column("EntityId")); + m.Cascade(Mapping.ByCode.Cascade.All); + }, + r => r.OneToMany()); + + rc.Cache( + cm => + { + cm.Include(CacheInclude.All); + cm.Usage(CacheUsage.ReadWrite); + }); + }); + + mapper.Class( + rc => + { + rc.Id(x => x.Id); + rc.Cache( + cm => + { + cm.Include(CacheInclude.All); + cm.Usage(CacheUsage.ReadWrite); + }); + }); + + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnSetUp() + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + + var entity = new Entity { Id = EntityId.Id1 }; + entity.Children.Add(new ChildEntity { Id = 0 }); + entity.Children.Add(new ChildEntity { Id = 1 }); + session.Save(entity); + + transaction.Commit(); + } + + protected override void OnTearDown() + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + + session.CreateQuery("delete from ChildEntity").ExecuteUpdate(); + session.CreateQuery("delete from System.Object").ExecuteUpdate(); + + transaction.Commit(); + } + + [Test] + public void LoadsEntityWithEnumIdAndChildrenUsingQueryCache() + { + LoadEntityWithQueryCache(); // warm up cache + + var entity = LoadEntityWithQueryCache(); + + Assert.That(entity.Children.Count(), Is.EqualTo(2)); + + Assert.That(Sfi.Statistics.QueryExecutionCount, Is.EqualTo(1), "Unexpected execution count"); + Assert.That(Sfi.Statistics.QueryCachePutCount, Is.EqualTo(1), "Unexpected cache put count"); + Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(1), "Unexpected cache hit count"); + } + + private Entity LoadEntityWithQueryCache() + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + var entity = session + .Query() + .FetchMany(x => x.Children) + .WithOptions(opt => opt.SetCacheable(true)) + .ToList()[0]; + + transaction.Commit(); + return entity; + } + } +} diff --git a/src/NHibernate/Async/Type/TypeHelper.cs b/src/NHibernate/Async/Type/TypeHelper.cs index 3446d115249..0f04ceebb7e 100644 --- a/src/NHibernate/Async/Type/TypeHelper.cs +++ b/src/NHibernate/Async/Type/TypeHelper.cs @@ -125,6 +125,7 @@ internal static async Task InitializeCollectionsAsync( continue; } + value = await (pair.Value.KeyType.AssembleAsync(value, session, null, cancellationToken)).ConfigureAwait(false); var collection = session.PersistenceContext.GetCollection(new CollectionKey(pair.Value, value)); await (collection.ForceInitializationAsync(cancellationToken)).ConfigureAwait(false); assembleRow[pair.Key] = collection; diff --git a/src/NHibernate/Type/TypeHelper.cs b/src/NHibernate/Type/TypeHelper.cs index 1e28e73c8a5..d7278d4a386 100644 --- a/src/NHibernate/Type/TypeHelper.cs +++ b/src/NHibernate/Type/TypeHelper.cs @@ -1,4 +1,4 @@ -using System; +using System; using System.Collections; using System.Collections.Generic; using NHibernate.Collection; @@ -133,6 +133,7 @@ internal static void InitializeCollections( continue; } + value = pair.Value.KeyType.Assemble(value, session, null); var collection = session.PersistenceContext.GetCollection(new CollectionKey(pair.Value, value)); collection.ForceInitialization(); assembleRow[pair.Key] = collection; From 4b1a3d1ba1971544ba880b75ca9b59f5d6d8d713 Mon Sep 17 00:00:00 2001 From: Alex Zaytsev Date: Fri, 26 Sep 2025 15:59:54 +1000 Subject: [PATCH 2/3] Fix build --- src/NHibernate.Test/NHSpecificTest/GH3643/Entity.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/NHibernate.Test/NHSpecificTest/GH3643/Entity.cs b/src/NHibernate.Test/NHSpecificTest/GH3643/Entity.cs index 09f08d8dbad..e70f49a2335 100644 --- a/src/NHibernate.Test/NHSpecificTest/GH3643/Entity.cs +++ b/src/NHibernate.Test/NHSpecificTest/GH3643/Entity.cs @@ -7,7 +7,7 @@ namespace NHibernate.Test.NHSpecificTest.GH3643 { class Entity { - private readonly ICollection _children = []; + private readonly ICollection _children = new List(); public virtual EntityId Id { get; set; } public virtual ICollection Children => _children; } From 2b9009b2263b7b6a08c8d9e7196e1695dec0fa7e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericdelaporte@users.noreply.github.com> Date: Fri, 26 Sep 2025 20:22:02 +0200 Subject: [PATCH 3/3] Enable dev builds for 5.5.4 --- build-common/NHibernate.props | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/build-common/NHibernate.props b/build-common/NHibernate.props index d165e516a51..af99f8fc753 100644 --- a/build-common/NHibernate.props +++ b/build-common/NHibernate.props @@ -3,9 +3,9 @@ 5.5 - 3 + 4 - + dev 9.0 $(NhVersion).$(VersionPatch)