From 2cd54e6a835d9179d7f4bdf38c94d6de846efd65 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Thu, 13 Jul 2023 10:57:22 +0300 Subject: [PATCH 1/8] Fix fetching properties from unmapped base class (#3357) Fixes #3352 --- .../FetchFromNotMappedBaseClassFixture.cs | 174 ++++++++++++++++++ .../NHSpecificTest/GH3352/Entity.cs | 31 ++++ .../FetchFromNotMappedBaseClassFixture.cs | 163 ++++++++++++++++ .../ResultOperatorProcessors/ProcessFetch.cs | 16 +- 4 files changed, 378 insertions(+), 6 deletions(-) create mode 100644 src/NHibernate.Test/Async/NHSpecificTest/GH3352/FetchFromNotMappedBaseClassFixture.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH3352/Entity.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH3352/FetchFromNotMappedBaseClassFixture.cs diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH3352/FetchFromNotMappedBaseClassFixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH3352/FetchFromNotMappedBaseClassFixture.cs new file mode 100644 index 00000000000..07b1a219f83 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH3352/FetchFromNotMappedBaseClassFixture.cs @@ -0,0 +1,174 @@ +//------------------------------------------------------------------------------ +// +// 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.MappingSchema; +using NHibernate.Linq; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH3352 +{ + using System.Threading.Tasks; + [TestFixture] + public class FetchFromNotMappedBaseClassFixtureAsync : TestCaseMappingByCode + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name, m => m.Lazy(true)); + }); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.ManyToOne(x => x.Parent, m => m.ForeignKey("none")); + }); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Component(x => x.Component); + }); + mapper.Component(rc => + { + rc.Property(x => x.Field); + rc.ManyToOne(x => x.Entity, m => m.ForeignKey("none")); + rc.Lazy(true); + }); + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnSetUp() + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + var np = new EntityComponentMapped { Component = new Component { Field = "x" } }; + session.Save(np); + var e = new EntityParentMapped { Parent = np }; + session.Save(e); + var nameMapped = new EntityNameMapped { Name = "lazy" }; + session.Save(nameMapped); + np.Component.Entity = nameMapped; + + transaction.Commit(); + } + + protected override void OnTearDown() + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + session.CreateQuery("delete from System.Object").ExecuteUpdate(); + + transaction.Commit(); + } + + [Test] + public async Task CanFetchLazyComponentFromNotMappedBaseClassAsync() + { + using var session = OpenSession(); + var list = await (session.Query().Fetch(x => x.Component).ToListAsync()); + + Assert.That(list, Has.Count.EqualTo(1)); + var result = list[0]; + Assert.That(NHibernateUtil.IsPropertyInitialized(result, nameof(result.Component))); + Assert.That(result.Component.Field, Is.EqualTo("x")); + } + + [Test] + public async Task CanFetchLazyComponentThenEntityFromNotMappedBaseClassAsync() + { + using var session = OpenSession(); + var list = await (session.Query() + .Fetch(x => x.Component) + .ThenFetch(x => x.Entity) + .ThenFetch(x => x.Name) + .ToListAsync()); + + Assert.That(list, Has.Count.EqualTo(1)); + var result = list[0]; + Assert.That(NHibernateUtil.IsPropertyInitialized(result, nameof(result.Component))); + Assert.That(result.Component.Field, Is.EqualTo("x")); + Assert.That(result.Component.Entity, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(result.Component.Entity), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(result.Component.Entity, nameof(result.Name)), Is.True); + Assert.That(result.Component.Entity.Name, Is.EqualTo("lazy")); + } + + [Test] + public async Task CanFetchLazyPropertyFromNotMappedBaseClassAsync() + { + using var session = OpenSession(); + var list = await (session.Query().Fetch(x => x.Name).ToListAsync()); + + Assert.That(list, Has.Count.EqualTo(1)); + var result = list[0]; + Assert.That(NHibernateUtil.IsPropertyInitialized(result, nameof(result.Name))); + Assert.That(result.Name, Is.EqualTo("lazy")); + } + + [Test] + public async Task CanThenFetchLazyComponentFromNotMappedBaseClassAsync() + { + using var session = OpenSession(); + var list = await (session.Query().Fetch(x => x.Parent).ThenFetch(x => x.Component).ToListAsync()); + + Assert.That(list, Has.Count.EqualTo(1)); + var result = list[0].Parent; + Assert.That(NHibernateUtil.IsInitialized(result), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(result, nameof(result.Component))); + Assert.That(result.Component.Field, Is.EqualTo("x")); + } + + [KnownBug("GH-3356")] + [Test(Description = "GH-3356" )] + public async Task FetchAfterSelectAsync() + { + using var log = new SqlLogSpy(); + + using var s = OpenSession(); + var list = await (s.Query() + .Select(x => x.Parent) + .Fetch(x => x.Component) + .ThenFetch(x => x.Entity) + .ThenFetch(x => x.Name) + .ToListAsync()); + Assert.That(list, Has.Count.EqualTo(1)); + var result = list[0]; + Assert.That(NHibernateUtil.IsPropertyInitialized(result, nameof(result.Component))); + Assert.That(result.Component.Field, Is.EqualTo("x")); + Assert.That(result.Component.Entity, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(result.Component.Entity), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(result.Component.Entity, nameof(result.Name)), Is.True); + Assert.That(result.Component.Entity.Name, Is.EqualTo("lazy")); + } + + [Test] + public async Task CanFetchEntityFromNotMappedBaseClassAsync() + { + using var session = OpenSession(); + var list = await (session.Query().Fetch(x => x.Parent).ToListAsync()); + + Assert.That(list, Has.Count.EqualTo(1)); + Assert.That(list[0].Parent, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(list[0].Parent)); + } + + [Test] + public void FetchNotMappedAssociationThrowsAsync() + { + using var session = OpenSession(); + var query = session.Query().Fetch(x => x.Parent); + + Assert.ThrowsAsync(() => query.ToListAsync()); + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH3352/Entity.cs b/src/NHibernate.Test/NHSpecificTest/GH3352/Entity.cs new file mode 100644 index 00000000000..dc7800cc977 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH3352/Entity.cs @@ -0,0 +1,31 @@ +using System; + +namespace NHibernate.Test.NHSpecificTest.GH3352 +{ + public class Entity + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + public virtual EntityComponentMapped Parent { get; set; } + public virtual Component Component { get; set; } + } + + public class EntityNameMapped : Entity + { + } + + public class EntityParentMapped : Entity + { + } + + public class EntityComponentMapped : Entity + { + } + + public class Component + { + public string Field { get; set; } + + public EntityNameMapped Entity { get; set; } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH3352/FetchFromNotMappedBaseClassFixture.cs b/src/NHibernate.Test/NHSpecificTest/GH3352/FetchFromNotMappedBaseClassFixture.cs new file mode 100644 index 00000000000..5b66027c963 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH3352/FetchFromNotMappedBaseClassFixture.cs @@ -0,0 +1,163 @@ +using System.Linq; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Linq; +using NHibernate.Mapping.ByCode; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH3352 +{ + [TestFixture] + public class FetchFromNotMappedBaseClassFixture : TestCaseMappingByCode + { + protected override HbmMapping GetMappings() + { + var mapper = new ModelMapper(); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Property(x => x.Name, m => m.Lazy(true)); + }); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.ManyToOne(x => x.Parent, m => m.ForeignKey("none")); + }); + mapper.Class(rc => + { + rc.Id(x => x.Id, m => m.Generator(Generators.GuidComb)); + rc.Component(x => x.Component); + }); + mapper.Component(rc => + { + rc.Property(x => x.Field); + rc.ManyToOne(x => x.Entity, m => m.ForeignKey("none")); + rc.Lazy(true); + }); + return mapper.CompileMappingForAllExplicitlyAddedEntities(); + } + + protected override void OnSetUp() + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + var np = new EntityComponentMapped { Component = new Component { Field = "x" } }; + session.Save(np); + var e = new EntityParentMapped { Parent = np }; + session.Save(e); + var nameMapped = new EntityNameMapped { Name = "lazy" }; + session.Save(nameMapped); + np.Component.Entity = nameMapped; + + transaction.Commit(); + } + + protected override void OnTearDown() + { + using var session = OpenSession(); + using var transaction = session.BeginTransaction(); + session.CreateQuery("delete from System.Object").ExecuteUpdate(); + + transaction.Commit(); + } + + [Test] + public void CanFetchLazyComponentFromNotMappedBaseClass() + { + using var session = OpenSession(); + var list = session.Query().Fetch(x => x.Component).ToList(); + + Assert.That(list, Has.Count.EqualTo(1)); + var result = list[0]; + Assert.That(NHibernateUtil.IsPropertyInitialized(result, nameof(result.Component))); + Assert.That(result.Component.Field, Is.EqualTo("x")); + } + + [Test] + public void CanFetchLazyComponentThenEntityFromNotMappedBaseClass() + { + using var session = OpenSession(); + var list = session.Query() + .Fetch(x => x.Component) + .ThenFetch(x => x.Entity) + .ThenFetch(x => x.Name) + .ToList(); + + Assert.That(list, Has.Count.EqualTo(1)); + var result = list[0]; + Assert.That(NHibernateUtil.IsPropertyInitialized(result, nameof(result.Component))); + Assert.That(result.Component.Field, Is.EqualTo("x")); + Assert.That(result.Component.Entity, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(result.Component.Entity), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(result.Component.Entity, nameof(result.Name)), Is.True); + Assert.That(result.Component.Entity.Name, Is.EqualTo("lazy")); + } + + [Test] + public void CanFetchLazyPropertyFromNotMappedBaseClass() + { + using var session = OpenSession(); + var list = session.Query().Fetch(x => x.Name).ToList(); + + Assert.That(list, Has.Count.EqualTo(1)); + var result = list[0]; + Assert.That(NHibernateUtil.IsPropertyInitialized(result, nameof(result.Name))); + Assert.That(result.Name, Is.EqualTo("lazy")); + } + + [Test] + public void CanThenFetchLazyComponentFromNotMappedBaseClass() + { + using var session = OpenSession(); + var list = session.Query().Fetch(x => x.Parent).ThenFetch(x => x.Component).ToList(); + + Assert.That(list, Has.Count.EqualTo(1)); + var result = list[0].Parent; + Assert.That(NHibernateUtil.IsInitialized(result), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(result, nameof(result.Component))); + Assert.That(result.Component.Field, Is.EqualTo("x")); + } + + [KnownBug("GH-3356")] + [Test(Description = "GH-3356" )] + public void FetchAfterSelect() + { + using var log = new SqlLogSpy(); + + using var s = OpenSession(); + var list = s.Query() + .Select(x => x.Parent) + .Fetch(x => x.Component) + .ThenFetch(x => x.Entity) + .ThenFetch(x => x.Name) + .ToList(); + Assert.That(list, Has.Count.EqualTo(1)); + var result = list[0]; + Assert.That(NHibernateUtil.IsPropertyInitialized(result, nameof(result.Component))); + Assert.That(result.Component.Field, Is.EqualTo("x")); + Assert.That(result.Component.Entity, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(result.Component.Entity), Is.True); + Assert.That(NHibernateUtil.IsPropertyInitialized(result.Component.Entity, nameof(result.Name)), Is.True); + Assert.That(result.Component.Entity.Name, Is.EqualTo("lazy")); + } + + [Test] + public void CanFetchEntityFromNotMappedBaseClass() + { + using var session = OpenSession(); + var list = session.Query().Fetch(x => x.Parent).ToList(); + + Assert.That(list, Has.Count.EqualTo(1)); + Assert.That(list[0].Parent, Is.Not.Null); + Assert.That(NHibernateUtil.IsInitialized(list[0].Parent)); + } + + [Test] + public void FetchNotMappedAssociationThrows() + { + using var session = OpenSession(); + var query = session.Query().Fetch(x => x.Parent); + + Assert.Throws(() => query.ToList()); + } + } +} diff --git a/src/NHibernate/Linq/Visitors/ResultOperatorProcessors/ProcessFetch.cs b/src/NHibernate/Linq/Visitors/ResultOperatorProcessors/ProcessFetch.cs index 31401f7df81..7c18f9476f4 100644 --- a/src/NHibernate/Linq/Visitors/ResultOperatorProcessors/ProcessFetch.cs +++ b/src/NHibernate/Linq/Visitors/ResultOperatorProcessors/ProcessFetch.cs @@ -1,6 +1,7 @@ using System; using System.Linq; using NHibernate.Hql.Ast; +using NHibernate.Persister.Entity; using NHibernate.Type; using Remotion.Linq.EagerFetching; @@ -54,15 +55,18 @@ private void Process( .GetClassMetadata(resultOperator.RelationMember.ReflectedType); if (metadata == null) { - var entityName = queryModelVisitor.VisitorParameters.SessionFactory.GetImplementors( - resultOperator.RelationMember.ReflectedType.FullName).FirstOrDefault(); - if (!string.IsNullOrEmpty(entityName)) + foreach (var entityName in queryModelVisitor.VisitorParameters.SessionFactory + .GetImplementors(resultOperator.RelationMember.ReflectedType.FullName)) { - metadata = queryModelVisitor.VisitorParameters.SessionFactory.GetClassMetadata(entityName); + if (queryModelVisitor.VisitorParameters.SessionFactory.GetClassMetadata(entityName) is IPropertyMapping propertyMapping + && propertyMapping.TryToType(resultOperator.RelationMember.Name, out propType)) + break; } } - - propType = metadata?.GetPropertyType(resultOperator.RelationMember.Name); + else + { + propType = metadata.GetPropertyType(resultOperator.RelationMember.Name); + } } if (propType != null && !propType.IsAssociationType) From 8c0a664ee5bde1de2e496aebdc3ec64abcc59f77 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Thu, 13 Jul 2023 16:42:14 +0300 Subject: [PATCH 2/8] Fix exception accessing indexer property for field interceptor proxy (#3358) Fixes #3354 --- .../Async/LazyProperty/LazyPropertyFixture.cs | 3 +++ src/NHibernate.Test/LazyProperty/Book.cs | 6 ++++++ src/NHibernate.Test/LazyProperty/LazyPropertyFixture.cs | 3 +++ src/NHibernate/Proxy/FieldInterceptorProxyBuilder.cs | 4 ++-- 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/NHibernate.Test/Async/LazyProperty/LazyPropertyFixture.cs b/src/NHibernate.Test/Async/LazyProperty/LazyPropertyFixture.cs index f99b79e5420..78ff4a50a16 100644 --- a/src/NHibernate.Test/Async/LazyProperty/LazyPropertyFixture.cs +++ b/src/NHibernate.Test/Async/LazyProperty/LazyPropertyFixture.cs @@ -230,6 +230,9 @@ public async Task CanGetValueForNonLazyPropertyAsync() Assert.That(book.Name, Is.EqualTo("some name")); Assert.That(book.FieldInterceptor, Is.EqualTo("Why not that name?")); Assert.That(NHibernateUtil.IsPropertyInitialized(book, "ALotOfText"), Is.False); + //GH-3354 Exception accessing indexer property + Assert.That(book[0], Is.EqualTo(0)); + Assert.DoesNotThrow(() => book[0] = 0); } } diff --git a/src/NHibernate.Test/LazyProperty/Book.cs b/src/NHibernate.Test/LazyProperty/Book.cs index 3dcfe73c567..10eb7d241f2 100644 --- a/src/NHibernate.Test/LazyProperty/Book.cs +++ b/src/NHibernate.Test/LazyProperty/Book.cs @@ -28,5 +28,11 @@ public virtual byte[] NoSetterImage public virtual string FieldInterceptor { get; set; } public virtual IList Words { get; set; } + + public virtual int this[int i] + { + get { return i;} + set { } + } } } diff --git a/src/NHibernate.Test/LazyProperty/LazyPropertyFixture.cs b/src/NHibernate.Test/LazyProperty/LazyPropertyFixture.cs index 302271f41a3..1b588dee440 100644 --- a/src/NHibernate.Test/LazyProperty/LazyPropertyFixture.cs +++ b/src/NHibernate.Test/LazyProperty/LazyPropertyFixture.cs @@ -225,6 +225,9 @@ public void CanGetValueForNonLazyProperty() Assert.That(book.Name, Is.EqualTo("some name")); Assert.That(book.FieldInterceptor, Is.EqualTo("Why not that name?")); Assert.That(NHibernateUtil.IsPropertyInitialized(book, "ALotOfText"), Is.False); + //GH-3354 Exception accessing indexer property + Assert.That(book[0], Is.EqualTo(0)); + Assert.DoesNotThrow(() => book[0] = 0); } } diff --git a/src/NHibernate/Proxy/FieldInterceptorProxyBuilder.cs b/src/NHibernate/Proxy/FieldInterceptorProxyBuilder.cs index 9e1326c3b2a..f3255a2aaab 100644 --- a/src/NHibernate/Proxy/FieldInterceptorProxyBuilder.cs +++ b/src/NHibernate/Proxy/FieldInterceptorProxyBuilder.cs @@ -94,11 +94,11 @@ public static TypeInfo CreateProxyType(System.Type baseType) private static void CreateProxiedMethod(TypeBuilder typeBuilder, MethodInfo method, FieldInfo fieldInterceptorField) { - if (ReflectHelper.IsPropertyGet(method)) + if (ReflectHelper.IsPropertyGet(method) && method.GetParameters().Length == 0) { ImplementGet(typeBuilder, method, fieldInterceptorField); } - else if (ReflectHelper.IsPropertySet(method)) + else if (ReflectHelper.IsPropertySet(method) && method.GetParameters().Length == 1) { ImplementSet(typeBuilder, method, fieldInterceptorField); } From 9302ad467bba443176f2bfb6b85855c1d1e1e504 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Thu, 13 Jul 2023 22:45:24 +0300 Subject: [PATCH 3/8] Update NHibernate.props Enable 5.4.4 dev builds --- 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 62b650cb650..87df1acdbd8 100644 --- a/build-common/NHibernate.props +++ b/build-common/NHibernate.props @@ -3,9 +3,9 @@ 5.4 - 3 + 4 - + dev 9.0 $(NhVersion).$(VersionPatch) From f3bd74ef8d90675a756e54ccd956c0ef24bf6a33 Mon Sep 17 00:00:00 2001 From: Alex Zaytsev Date: Tue, 18 Jul 2023 12:30:52 +0000 Subject: [PATCH 4/8] Call BeforeAssemble on persistent collection InitializeFromCache to allow batch fetching (#3365) Fixes #3359 --- .../Async/CacheTest/BatchableCacheFixture.cs | 25 ++++++++++++++++++- .../CacheTest/BatchableCacheFixture.cs | 25 ++++++++++++++++++- .../Generic/PersistentGenericBag.cs | 9 ++++++- .../Generic/PersistentGenericIdentifierBag.cs | 13 ++++++++-- .../Generic/PersistentGenericList.cs | 9 ++++++- .../Generic/PersistentGenericMap.cs | 13 ++++++++-- .../Generic/PersistentGenericSet.cs | 9 ++++++- .../Async/Collection/PersistentArrayHolder.cs | 8 +++++- .../Generic/PersistentGenericBag.cs | 9 ++++++- .../Generic/PersistentGenericIdentifierBag.cs | 13 ++++++++-- .../Generic/PersistentGenericList.cs | 9 ++++++- .../Generic/PersistentGenericMap.cs | 16 +++++++++--- .../Generic/PersistentGenericSet.cs | 9 ++++++- .../Collection/PersistentArrayHolder.cs | 8 +++++- 14 files changed, 156 insertions(+), 19 deletions(-) diff --git a/src/NHibernate.Test/Async/CacheTest/BatchableCacheFixture.cs b/src/NHibernate.Test/Async/CacheTest/BatchableCacheFixture.cs index c3c2195688a..3fe00c5e385 100644 --- a/src/NHibernate.Test/Async/CacheTest/BatchableCacheFixture.cs +++ b/src/NHibernate.Test/Async/CacheTest/BatchableCacheFixture.cs @@ -1565,8 +1565,31 @@ public async Task QueryFetchEntityBatchCacheTestAsync(bool clearEntityCacheAfter Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(future ? 2 : 1), "Unexpected cache hit count"); } + [Test] + public async Task CollectionLazyInitializationFromCacheIsBatchedAsync() + { + using (var s = OpenSession()) + { + var readOnly = await (s.GetAsync(await (s.Query().Select(x => x.Id).FirstAsync()))); + Assert.That(readOnly.Items.Count, Is.EqualTo(6)); + } + + var itemPersister = Sfi.GetEntityPersister(typeof(ReadOnlyItem).FullName); + var itemCache = (BatchableCache) itemPersister.Cache.Cache; + itemCache.ClearStatistics(); + + using (var s = OpenSession()) + { + var readOnly = await (s.GetAsync(await (s.Query().Select(x => x.Id).FirstAsync()))); + Assert.That(readOnly.Items.Count, Is.EqualTo(6)); + } + + // 6 items with batch-size = 4 so 2 GetMany calls are expected 1st call: 4 items + 2nd call: 2 items + Assert.That(itemCache.GetMultipleCalls.Count, Is.EqualTo(2)); + } + private async Task AssertMultipleCacheCallsAsync(IEnumerable loadIds, IReadOnlyList getIds, int idIndex, - int[][] fetchedIdIndexes, int[] putIdIndexes, Func cacheBeforeLoadFn = null, CancellationToken cancellationToken = default(CancellationToken)) + int[][] fetchedIdIndexes, int[] putIdIndexes, Func cacheBeforeLoadFn = null, CancellationToken cancellationToken = default(CancellationToken)) where TEntity : CacheEntity { var persister = Sfi.GetEntityPersister(typeof(TEntity).FullName); diff --git a/src/NHibernate.Test/CacheTest/BatchableCacheFixture.cs b/src/NHibernate.Test/CacheTest/BatchableCacheFixture.cs index 150369306b2..a18e7b616ca 100644 --- a/src/NHibernate.Test/CacheTest/BatchableCacheFixture.cs +++ b/src/NHibernate.Test/CacheTest/BatchableCacheFixture.cs @@ -1553,8 +1553,31 @@ public void QueryFetchEntityBatchCacheTest(bool clearEntityCacheAfterQuery, bool Assert.That(Sfi.Statistics.QueryCacheHitCount, Is.EqualTo(future ? 2 : 1), "Unexpected cache hit count"); } + [Test] + public void CollectionLazyInitializationFromCacheIsBatched() + { + using (var s = OpenSession()) + { + var readOnly = s.Get(s.Query().Select(x => x.Id).First()); + Assert.That(readOnly.Items.Count, Is.EqualTo(6)); + } + + var itemPersister = Sfi.GetEntityPersister(typeof(ReadOnlyItem).FullName); + var itemCache = (BatchableCache) itemPersister.Cache.Cache; + itemCache.ClearStatistics(); + + using (var s = OpenSession()) + { + var readOnly = s.Get(s.Query().Select(x => x.Id).First()); + Assert.That(readOnly.Items.Count, Is.EqualTo(6)); + } + + // 6 items with batch-size = 4 so 2 GetMany calls are expected 1st call: 4 items + 2nd call: 2 items + Assert.That(itemCache.GetMultipleCalls.Count, Is.EqualTo(2)); + } + private void AssertMultipleCacheCalls(IEnumerable loadIds, IReadOnlyList getIds, int idIndex, - int[][] fetchedIdIndexes, int[] putIdIndexes, Func cacheBeforeLoadFn = null) + int[][] fetchedIdIndexes, int[] putIdIndexes, Func cacheBeforeLoadFn = null) where TEntity : CacheEntity { var persister = Sfi.GetEntityPersister(typeof(TEntity).FullName); diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs index 3b229ac54b8..7ffa1b41fb9 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericBag.cs @@ -109,9 +109,16 @@ public override async Task InitializeFromCacheAsync(ICollectionPersister persist var array = (object[]) disassembled; var size = array.Length; BeforeInitialize(persister, size); + + var elementType = persister.ElementType; + for (int i = 0; i < size; i++) + { + await (elementType.BeforeAssembleAsync(array[i], Session, cancellationToken)).ConfigureAwait(false); + } + for (var i = 0; i < size; i++) { - var element = await (persister.ElementType.AssembleAsync(array[i], Session, owner, cancellationToken)).ConfigureAwait(false); + var element = await (elementType.AssembleAsync(array[i], Session, owner, cancellationToken)).ConfigureAwait(false); if (element != null) { _gbag.Add((T) element); diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericIdentifierBag.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericIdentifierBag.cs index 6e3ea111d01..af37fb8a1e0 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericIdentifierBag.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericIdentifierBag.cs @@ -44,10 +44,19 @@ public override async Task InitializeFromCacheAsync(ICollectionPersister persist object[] array = (object[])disassembled; int size = array.Length; BeforeInitialize(persister, size); + + var identifierType = persister.IdentifierType; + var elementType = persister.ElementType; + for (int i = 0; i < size; i++) + { + await (identifierType.BeforeAssembleAsync(array[i], Session, cancellationToken)).ConfigureAwait(false); + await (elementType.BeforeAssembleAsync(array[i + 1], Session, cancellationToken)).ConfigureAwait(false); + } + for (int i = 0; i < size; i += 2) { - _identifiers[i / 2] = await (persister.IdentifierType.AssembleAsync(array[i], Session, owner, cancellationToken)).ConfigureAwait(false); - _values.Add((T) await (persister.ElementType.AssembleAsync(array[i + 1], Session, owner, cancellationToken)).ConfigureAwait(false)); + _identifiers[i / 2] = await (identifierType.AssembleAsync(array[i], Session, owner, cancellationToken)).ConfigureAwait(false); + _values.Add((T) await (elementType.AssembleAsync(array[i + 1], Session, owner, cancellationToken)).ConfigureAwait(false)); } } diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericList.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericList.cs index 1417c98e361..6ac827ecfa0 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericList.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericList.cs @@ -98,9 +98,16 @@ public override async Task InitializeFromCacheAsync(ICollectionPersister persist object[] array = (object[])disassembled; int size = array.Length; BeforeInitialize(persister, size); + + var elementType = persister.ElementType; for (int i = 0; i < size; i++) { - var element = await (persister.ElementType.AssembleAsync(array[i], Session, owner, cancellationToken)).ConfigureAwait(false); + await (elementType.BeforeAssembleAsync(array[i], Session, cancellationToken)).ConfigureAwait(false); + } + + for (int i = 0; i < size; i++) + { + var element = await (elementType.AssembleAsync(array[i], Session, owner, cancellationToken)).ConfigureAwait(false); WrappedList.Add((T) (element ?? DefaultForType)); } } diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs index e574577a111..fbe6e30c490 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs @@ -94,10 +94,19 @@ public override async Task InitializeFromCacheAsync(ICollectionPersister persist object[] array = (object[])disassembled; int size = array.Length; BeforeInitialize(persister, size); + + var indexType = persister.IndexType; + var elementType = persister.ElementType; + for (int i = 0; i < size; i++) + { + await (indexType.BeforeAssembleAsync(array[i], Session, cancellationToken)).ConfigureAwait(false); + await (elementType.BeforeAssembleAsync(array[i + 1], Session, cancellationToken)).ConfigureAwait(false); + } + for (int i = 0; i < size; i += 2) { - WrappedMap[(TKey)await (persister.IndexType.AssembleAsync(array[i], Session, owner, cancellationToken)).ConfigureAwait(false)] = - (TValue)await (persister.ElementType.AssembleAsync(array[i + 1], Session, owner, cancellationToken)).ConfigureAwait(false); + WrappedMap[(TKey)await (indexType.AssembleAsync(array[i], Session, owner, cancellationToken)).ConfigureAwait(false)] = + (TValue)await (elementType.AssembleAsync(array[i + 1], Session, owner, cancellationToken)).ConfigureAwait(false); } } diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericSet.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericSet.cs index 91f2f86f626..c567ea44d86 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericSet.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericSet.cs @@ -84,9 +84,16 @@ public override async Task InitializeFromCacheAsync(ICollectionPersister persist var array = (object[])disassembled; int size = array.Length; BeforeInitialize(persister, size); + + var elementType = persister.ElementType; + for (int i = 0; i < size; i++) + { + await (elementType.BeforeAssembleAsync(array[i], Session, cancellationToken)).ConfigureAwait(false); + } + for (int i = 0; i < size; i++) { - var element = await (persister.ElementType.AssembleAsync(array[i], Session, owner, cancellationToken)).ConfigureAwait(false); + var element = await (elementType.AssembleAsync(array[i], Session, owner, cancellationToken)).ConfigureAwait(false); if (element != null) { WrappedSet.Add((T) element); diff --git a/src/NHibernate/Async/Collection/PersistentArrayHolder.cs b/src/NHibernate/Async/Collection/PersistentArrayHolder.cs index 448309f2f9e..a08d2e6e96e 100644 --- a/src/NHibernate/Async/Collection/PersistentArrayHolder.cs +++ b/src/NHibernate/Async/Collection/PersistentArrayHolder.cs @@ -94,9 +94,15 @@ public override async Task InitializeFromCacheAsync(ICollectionPersister persist array = System.Array.CreateInstance(persister.ElementClass, cached.Length); + var elementType = persister.ElementType; for (int i = 0; i < cached.Length; i++) { - array.SetValue(await (persister.ElementType.AssembleAsync(cached[i], Session, owner, cancellationToken)).ConfigureAwait(false), i); + await (elementType.BeforeAssembleAsync(cached[i], Session, cancellationToken)).ConfigureAwait(false); + } + + for (int i = 0; i < cached.Length; i++) + { + array.SetValue(await (elementType.AssembleAsync(cached[i], Session, owner, cancellationToken)).ConfigureAwait(false), i); } } diff --git a/src/NHibernate/Collection/Generic/PersistentGenericBag.cs b/src/NHibernate/Collection/Generic/PersistentGenericBag.cs index 98c916717c3..6530936f613 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericBag.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericBag.cs @@ -400,9 +400,16 @@ public override void InitializeFromCache(ICollectionPersister persister, object var array = (object[]) disassembled; var size = array.Length; BeforeInitialize(persister, size); + + var elementType = persister.ElementType; + for (int i = 0; i < size; i++) + { + elementType.BeforeAssemble(array[i], Session); + } + for (var i = 0; i < size; i++) { - var element = persister.ElementType.Assemble(array[i], Session, owner); + var element = elementType.Assemble(array[i], Session, owner); if (element != null) { _gbag.Add((T) element); diff --git a/src/NHibernate/Collection/Generic/PersistentGenericIdentifierBag.cs b/src/NHibernate/Collection/Generic/PersistentGenericIdentifierBag.cs index 884d454b76e..71005049ad8 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericIdentifierBag.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericIdentifierBag.cs @@ -75,10 +75,19 @@ public override void InitializeFromCache(ICollectionPersister persister, object object[] array = (object[])disassembled; int size = array.Length; BeforeInitialize(persister, size); + + var identifierType = persister.IdentifierType; + var elementType = persister.ElementType; + for (int i = 0; i < size; i++) + { + identifierType.BeforeAssemble(array[i], Session); + elementType.BeforeAssemble(array[i + 1], Session); + } + for (int i = 0; i < size; i += 2) { - _identifiers[i / 2] = persister.IdentifierType.Assemble(array[i], Session, owner); - _values.Add((T) persister.ElementType.Assemble(array[i + 1], Session, owner)); + _identifiers[i / 2] = identifierType.Assemble(array[i], Session, owner); + _values.Add((T) elementType.Assemble(array[i + 1], Session, owner)); } } diff --git a/src/NHibernate/Collection/Generic/PersistentGenericList.cs b/src/NHibernate/Collection/Generic/PersistentGenericList.cs index 6255616dd0e..2c08510c311 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericList.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericList.cs @@ -160,9 +160,16 @@ public override void InitializeFromCache(ICollectionPersister persister, object object[] array = (object[])disassembled; int size = array.Length; BeforeInitialize(persister, size); + + var elementType = persister.ElementType; for (int i = 0; i < size; i++) { - var element = persister.ElementType.Assemble(array[i], Session, owner); + elementType.BeforeAssemble(array[i], Session); + } + + for (int i = 0; i < size; i++) + { + var element = elementType.Assemble(array[i], Session, owner); WrappedList.Add((T) (element ?? DefaultForType)); } } diff --git a/src/NHibernate/Collection/Generic/PersistentGenericMap.cs b/src/NHibernate/Collection/Generic/PersistentGenericMap.cs index 5fab797d793..e81aee07578 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericMap.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericMap.cs @@ -163,10 +163,19 @@ public override void InitializeFromCache(ICollectionPersister persister, object object[] array = (object[])disassembled; int size = array.Length; BeforeInitialize(persister, size); + + var indexType = persister.IndexType; + var elementType = persister.ElementType; + for (int i = 0; i < size; i++) + { + indexType.BeforeAssemble(array[i], Session); + elementType.BeforeAssemble(array[i + 1], Session); + } + for (int i = 0; i < size; i += 2) { - WrappedMap[(TKey)persister.IndexType.Assemble(array[i], Session, owner)] = - (TValue)persister.ElementType.Assemble(array[i + 1], Session, owner); + WrappedMap[(TKey)indexType.Assemble(array[i], Session, owner)] = + (TValue)elementType.Assemble(array[i + 1], Session, owner); } } @@ -246,8 +255,9 @@ public void Add(TKey key, TValue value) { if (key == null) { - throw new ArgumentNullException("key"); + throw new ArgumentNullException(nameof(key)); } + if (PutQueueEnabled) { var found = TryReadElementByKey(key, out _, out _); diff --git a/src/NHibernate/Collection/Generic/PersistentGenericSet.cs b/src/NHibernate/Collection/Generic/PersistentGenericSet.cs index 4e138bb22fa..f2736a8d21c 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericSet.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericSet.cs @@ -154,9 +154,16 @@ public override void InitializeFromCache(ICollectionPersister persister, object var array = (object[])disassembled; int size = array.Length; BeforeInitialize(persister, size); + + var elementType = persister.ElementType; + for (int i = 0; i < size; i++) + { + elementType.BeforeAssemble(array[i], Session); + } + for (int i = 0; i < size; i++) { - var element = persister.ElementType.Assemble(array[i], Session, owner); + var element = elementType.Assemble(array[i], Session, owner); if (element != null) { WrappedSet.Add((T) element); diff --git a/src/NHibernate/Collection/PersistentArrayHolder.cs b/src/NHibernate/Collection/PersistentArrayHolder.cs index 242d7dac51a..1bd42dc42ce 100644 --- a/src/NHibernate/Collection/PersistentArrayHolder.cs +++ b/src/NHibernate/Collection/PersistentArrayHolder.cs @@ -196,9 +196,15 @@ public override void InitializeFromCache(ICollectionPersister persister, object array = System.Array.CreateInstance(persister.ElementClass, cached.Length); + var elementType = persister.ElementType; for (int i = 0; i < cached.Length; i++) { - array.SetValue(persister.ElementType.Assemble(cached[i], Session, owner), i); + elementType.BeforeAssemble(cached[i], Session); + } + + for (int i = 0; i < cached.Length; i++) + { + array.SetValue(elementType.Assemble(cached[i], Session, owner), i); } } From 3186197c1eec77aae46eb0f2ce8cf37bb4c2a03b Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Tue, 18 Jul 2023 15:32:43 +0300 Subject: [PATCH 5/8] Migrate dev NuGet packages to Cloudsmith (#3367) (cherry picked from commit 26b557fc38824e812527f18cd5cbe3f40fab5398) --- README.md | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 8da992df161..6290ebbaa94 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,7 @@ Nightly Development Builds -------------------------- The quickest way to get the latest development build of NHibernate is to add it to your project using -NuGet from MyGet feed (). +NuGet from Cloudsmith feed (). In order to make life a little bit easier you can register the package source in the NuGet.Config file in the top folder of your project, similar to the following. @@ -35,11 +35,18 @@ file in the top folder of your project, similar to the following. - + ``` +Package repository hosting is graciously provided by [Cloudsmith](https://cloudsmith.com). +Cloudsmith is the only fully hosted, cloud-native, universal package management solution, that +enables your organization to create, store and share packages in any format, to any place, with total +confidence. + +[![Hosted By: Cloudsmith](https://img.shields.io/badge/OSS%20hosting%20by-cloudsmith-blue?logo=cloudsmith&style=flat-square)](https://cloudsmith.com) + Community Forums ---------------- From 13565fb13db643b2f64d8f407ae19b71e3e9af00 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Wed, 19 Jul 2023 06:19:11 +0300 Subject: [PATCH 6/8] Allow internal entity classess/interfaces in .NET Standard 2.0 for field interceptor (#3368) --- src/NHibernate/Proxy/FieldInterceptorProxyBuilder.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/NHibernate/Proxy/FieldInterceptorProxyBuilder.cs b/src/NHibernate/Proxy/FieldInterceptorProxyBuilder.cs index f3255a2aaab..4fed7814108 100644 --- a/src/NHibernate/Proxy/FieldInterceptorProxyBuilder.cs +++ b/src/NHibernate/Proxy/FieldInterceptorProxyBuilder.cs @@ -57,10 +57,9 @@ public static TypeInfo CreateProxyType(System.Type baseType) var assemblyBuilder = ProxyBuilderHelper.DefineDynamicAssembly(AppDomain.CurrentDomain, name); -#if NETFX || NETCOREAPP2_0_OR_GREATER || NETSTANDARD2_1_OR_GREATER if (!baseType.IsVisible) ProxyBuilderHelper.GenerateInstanceOfIgnoresAccessChecksToAttribute(assemblyBuilder, baseType.Assembly.GetName().Name); -#endif + var moduleBuilder = ProxyBuilderHelper.DefineDynamicModule(assemblyBuilder, moduleName); const TypeAttributes typeAttributes = TypeAttributes.AutoClass | TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.BeforeFieldInit; From c9d0a951edaaaecfe591b806bc72bb0aa30fbb7c Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Tue, 25 Jul 2023 05:03:46 +0300 Subject: [PATCH 7/8] Fix BeforeAssemble call for Map and IdentifierBag (#3380) Fixup for #3365 --- .../Collection/Generic/PersistentGenericIdentifierBag.cs | 2 +- .../Async/Collection/Generic/PersistentGenericMap.cs | 4 ++-- .../Collection/Generic/PersistentGenericIdentifierBag.cs | 2 +- src/NHibernate/Collection/Generic/PersistentGenericMap.cs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericIdentifierBag.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericIdentifierBag.cs index af37fb8a1e0..1b8a8f57502 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericIdentifierBag.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericIdentifierBag.cs @@ -47,7 +47,7 @@ public override async Task InitializeFromCacheAsync(ICollectionPersister persist var identifierType = persister.IdentifierType; var elementType = persister.ElementType; - for (int i = 0; i < size; i++) + for (int i = 0; i < size; i += 2) { await (identifierType.BeforeAssembleAsync(array[i], Session, cancellationToken)).ConfigureAwait(false); await (elementType.BeforeAssembleAsync(array[i + 1], Session, cancellationToken)).ConfigureAwait(false); diff --git a/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs b/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs index fbe6e30c490..3348087744a 100644 --- a/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs +++ b/src/NHibernate/Async/Collection/Generic/PersistentGenericMap.cs @@ -94,10 +94,10 @@ public override async Task InitializeFromCacheAsync(ICollectionPersister persist object[] array = (object[])disassembled; int size = array.Length; BeforeInitialize(persister, size); - + var indexType = persister.IndexType; var elementType = persister.ElementType; - for (int i = 0; i < size; i++) + for (int i = 0; i < size; i += 2) { await (indexType.BeforeAssembleAsync(array[i], Session, cancellationToken)).ConfigureAwait(false); await (elementType.BeforeAssembleAsync(array[i + 1], Session, cancellationToken)).ConfigureAwait(false); diff --git a/src/NHibernate/Collection/Generic/PersistentGenericIdentifierBag.cs b/src/NHibernate/Collection/Generic/PersistentGenericIdentifierBag.cs index 71005049ad8..59769257121 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericIdentifierBag.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericIdentifierBag.cs @@ -78,7 +78,7 @@ public override void InitializeFromCache(ICollectionPersister persister, object var identifierType = persister.IdentifierType; var elementType = persister.ElementType; - for (int i = 0; i < size; i++) + for (int i = 0; i < size; i += 2) { identifierType.BeforeAssemble(array[i], Session); elementType.BeforeAssemble(array[i + 1], Session); diff --git a/src/NHibernate/Collection/Generic/PersistentGenericMap.cs b/src/NHibernate/Collection/Generic/PersistentGenericMap.cs index e81aee07578..24c4c30568e 100644 --- a/src/NHibernate/Collection/Generic/PersistentGenericMap.cs +++ b/src/NHibernate/Collection/Generic/PersistentGenericMap.cs @@ -163,10 +163,10 @@ public override void InitializeFromCache(ICollectionPersister persister, object object[] array = (object[])disassembled; int size = array.Length; BeforeInitialize(persister, size); - + var indexType = persister.IndexType; var elementType = persister.ElementType; - for (int i = 0; i < size; i++) + for (int i = 0; i < size; i += 2) { indexType.BeforeAssemble(array[i], Session); elementType.BeforeAssemble(array[i + 1], Session); From dd17017b658adc1e90bfba0d2bfea2fb5d9518f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericDelaporte@users.noreply.github.com> Date: Sun, 30 Jul 2023 20:52:55 +0200 Subject: [PATCH 8/8] Release 5.4.4 (#3386) --- build-common/NHibernate.props | 2 +- releasenotes.txt | 25 ++++++++++++++++++++++++- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/build-common/NHibernate.props b/build-common/NHibernate.props index 87df1acdbd8..403c50e24a6 100644 --- a/build-common/NHibernate.props +++ b/build-common/NHibernate.props @@ -5,7 +5,7 @@ 5.4 4 - dev + 9.0 $(NhVersion).$(VersionPatch) diff --git a/releasenotes.txt b/releasenotes.txt index 1a7f3e97521..a15d6949d9d 100644 --- a/releasenotes.txt +++ b/releasenotes.txt @@ -1,4 +1,27 @@ -Build 5.4.3 +Build 5.4.4 +============================= + +Release notes - NHibernate - Version 5.4.4 + +6 issues were resolved in this release. + +** Bug + + * #3359 2nd level cache GetMany ineffective for collections + * #3354 Invalid program generated by FieldInterceptorProxyBuilder for indexer property getter + * #3352 Fetch throws "could not resolve property" error for a property that is not mapped + +** Improvement + + * #3368 Allow internal entity classes/interfaces in .NET Standard 2.0 for field interceptor + +** Task + + * #3386 Release 5.4.4 + * #3367 Update readme with actual dev build information for 5.4 + + +Build 5.4.3 ============================= Release notes - NHibernate - Version 5.4.3