From ebda719735f282478f240087c0db0fd8b4c2ef70 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, 28 May 2018 17:25:49 +0200 Subject: [PATCH 01/17] Cease losing proxy initialized state on serialization --- .../StaticProxyFactoryFixture.cs | 67 ++++++++++++++++++- .../Proxy/NHibernateProxyBuilder.cs | 24 ++++++- .../Proxy/NHibernateProxyObjectReference.cs | 23 ++++++- 3 files changed, 111 insertions(+), 3 deletions(-) diff --git a/src/NHibernate.Test/StaticProxyTest/StaticProxyFactoryFixture.cs b/src/NHibernate.Test/StaticProxyTest/StaticProxyFactoryFixture.cs index 72a1937ae20..cee5b607f2a 100644 --- a/src/NHibernate.Test/StaticProxyTest/StaticProxyFactoryFixture.cs +++ b/src/NHibernate.Test/StaticProxyTest/StaticProxyFactoryFixture.cs @@ -1,4 +1,7 @@ -using System.Collections.Generic; +using System; +using System.Collections.Generic; +using System.IO; +using System.Runtime.Serialization.Formatters.Binary; using NHibernate.Proxy; using NUnit.Framework; @@ -16,6 +19,12 @@ public class TestClass : ISomething public virtual int Id { get; set; } } + [Serializable] + public class SimpleTestClass + { + public virtual int Id { get; set; } + } + [Test] public void CanCreateProxyForClassWithInternalInterface() { @@ -24,5 +33,61 @@ public void CanCreateProxyForClassWithInternalInterface() var proxy = factory.GetProxy(1, null); Assert.That(proxy, Is.Not.Null); } + + [Test] + public void InitializedProxyStaysInitializedAfterDeserialization() + { + TestsContext.AssumeSystemTypeIsSerializable(); + + var factory = new StaticProxyFactory(); + factory.PostInstantiate(typeof(SimpleTestClass).FullName, typeof(SimpleTestClass), new HashSet {typeof(INHibernateProxy)}, null, null, null); + var proxy = factory.GetProxy(2, null); + Assert.That(proxy, Is.Not.Null, "proxy"); + Assert.That(NHibernateUtil.IsInitialized(proxy), Is.False, "proxy already initialized after creation"); + Assert.That(proxy.HibernateLazyInitializer, Is.Not.Null, "HibernateLazyInitializer"); + + var impl = new SimpleTestClass { Id = 2 }; + proxy.HibernateLazyInitializer.SetImplementation(impl); + Assert.That(NHibernateUtil.IsInitialized(proxy), Is.True, "proxy not initialized after setting implementation"); + + var serializer = new BinaryFormatter(); + object deserialized; + using (var memoryStream = new MemoryStream()) + { + serializer.Serialize(memoryStream, proxy); + memoryStream.Seek(0L, SeekOrigin.Begin); + deserialized = serializer.Deserialize(memoryStream); + } + Assert.That(deserialized, Is.Not.Null, "deserialized"); + Assert.That(deserialized, Is.InstanceOf()); + Assert.That(NHibernateUtil.IsInitialized(deserialized), Is.True, "proxy no more initialized after deserialization"); + Assert.That(deserialized, Is.InstanceOf()); + Assert.That(((SimpleTestClass) deserialized).Id, Is.EqualTo(2)); + } + + [Test] + public void NonInitializedProxyStaysNonInitializedAfterSerialization() + { + TestsContext.AssumeSystemTypeIsSerializable(); + + var factory = new StaticProxyFactory(); + factory.PostInstantiate(typeof(SimpleTestClass).FullName, typeof(SimpleTestClass), new HashSet {typeof(INHibernateProxy)}, null, null, null); + var proxy = factory.GetProxy(2, null); + Assert.That(proxy, Is.Not.Null, "proxy"); + Assert.That(NHibernateUtil.IsInitialized(proxy), Is.False, "proxy already initialized after creation"); + + var serializer = new BinaryFormatter(); + object deserialized; + using (var memoryStream = new MemoryStream()) + { + serializer.Serialize(memoryStream, proxy); + Assert.That(NHibernateUtil.IsInitialized(proxy), Is.False, "proxy initialized after serialization"); + memoryStream.Seek(0L, SeekOrigin.Begin); + deserialized = serializer.Deserialize(memoryStream); + } + Assert.That(deserialized, Is.Not.Null, "deserialized"); + Assert.That(deserialized, Is.InstanceOf()); + Assert.That(NHibernateUtil.IsInitialized(deserialized), Is.False, "proxy initialized after deserialization"); + } } } diff --git a/src/NHibernate/Proxy/NHibernateProxyBuilder.cs b/src/NHibernate/Proxy/NHibernateProxyBuilder.cs index 2d7615f65f9..cf9644cd645 100644 --- a/src/NHibernate/Proxy/NHibernateProxyBuilder.cs +++ b/src/NHibernate/Proxy/NHibernateProxyBuilder.cs @@ -24,6 +24,7 @@ internal class NHibernateProxyBuilder private static readonly PropertyInfo LazyInitializerIdentifierProperty = LazyInitializerType.GetProperty(nameof(ILazyInitializer.Identifier)); private static readonly MethodInfo LazyInitializerInitializeMethod = LazyInitializerType.GetMethod(nameof(ILazyInitializer.Initialize)); private static readonly MethodInfo LazyInitializerGetImplementationMethod = LazyInitializerType.GetMethod(nameof(ILazyInitializer.GetImplementation), System.Type.EmptyTypes); + private static readonly PropertyInfo LazyInitializerIsUninitializedProperty = LazyInitializerType.GetProperty(nameof(ILazyInitializer.IsUninitialized)); private static readonly IProxyAssemblyBuilder ProxyAssemblyBuilder = new DefaultProxyAssemblyBuilder(); private static readonly ConstructorInfo SecurityCriticalAttributeConstructor = typeof(SecurityCriticalAttribute).GetConstructor(System.Type.EmptyTypes); @@ -199,7 +200,12 @@ private static void ImplementGetObjectData(TypeBuilder typeBuilder, FieldInfo pr IL.Emit(OpCodes.Call, ReflectionCache.TypeMethods.GetTypeFromHandle); IL.Emit(OpCodes.Callvirt, SerializationInfoSetTypeMethod); - // (new NHibernateProxyObjectReference(this.__proxyInfo, this.__lazyInitializer.Identifier)).GetObjectData(info, context); + // return + // (new NHibernateProxyObjectReference( + // this.__proxyInfo, + // this.__lazyInitializer.Identifier), + // this.__lazyInitializer.IsUninitialized ? null : this.__lazyInitializer.GetImplementation()) + // .GetObjectData(info, context); //this.__proxyInfo IL.Emit(OpCodes.Ldarg_0); IL.Emit(OpCodes.Ldfld, proxyInfoField); @@ -209,11 +215,27 @@ private static void ImplementGetObjectData(TypeBuilder typeBuilder, FieldInfo pr IL.Emit(OpCodes.Ldfld, lazyInitializerField); IL.Emit(OpCodes.Callvirt, LazyInitializerIdentifierProperty.GetMethod); + // this.__lazyInitializer.IsUninitialized ? null : this.__lazyInitializer.GetImplementation() + var isUnitialized = IL.DefineLabel(); + var endIsUnitializedTernary = IL.DefineLabel(); + IL.Emit(OpCodes.Ldarg_0); + IL.Emit(OpCodes.Ldfld, lazyInitializerField); + IL.Emit(OpCodes.Callvirt, LazyInitializerIsUninitializedProperty.GetMethod); + IL.Emit(OpCodes.Brtrue, isUnitialized); + IL.Emit(OpCodes.Ldarg_0); + IL.Emit(OpCodes.Ldfld, lazyInitializerField); + IL.Emit(OpCodes.Callvirt, LazyInitializerGetImplementationMethod); + IL.Emit(OpCodes.Br, endIsUnitializedTernary); + IL.MarkLabel(isUnitialized); + IL.Emit(OpCodes.Ldnull); + IL.MarkLabel(endIsUnitializedTernary); + var constructor = typeof(NHibernateProxyObjectReference).GetConstructor( new[] { typeof(NHibernateProxyFactoryInfo), typeof(object), + typeof(object) }); IL.Emit(OpCodes.Newobj, constructor); diff --git a/src/NHibernate/Proxy/NHibernateProxyObjectReference.cs b/src/NHibernate/Proxy/NHibernateProxyObjectReference.cs index 9878bc5bc21..b717d7061f3 100644 --- a/src/NHibernate/Proxy/NHibernateProxyObjectReference.cs +++ b/src/NHibernate/Proxy/NHibernateProxyObjectReference.cs @@ -9,23 +9,43 @@ public sealed class NHibernateProxyObjectReference : IObjectReference, ISerializ { private readonly NHibernateProxyFactoryInfo _proxyFactoryInfo; private readonly object _identifier; + private readonly object _implementation; + [Obsolete("Use overload taking an implementation parameter")] public NHibernateProxyObjectReference(NHibernateProxyFactoryInfo proxyFactoryInfo, object identifier) + : this (proxyFactoryInfo, identifier, null) + {} + + public NHibernateProxyObjectReference(NHibernateProxyFactoryInfo proxyFactoryInfo, object identifier, object implementation) { _proxyFactoryInfo = proxyFactoryInfo; _identifier = identifier; + _implementation = implementation; } private NHibernateProxyObjectReference(SerializationInfo info, StreamingContext context) { _proxyFactoryInfo = (NHibernateProxyFactoryInfo) info.GetValue(nameof(_proxyFactoryInfo), typeof(NHibernateProxyFactoryInfo)); _identifier = info.GetValue(nameof(_identifier), typeof(object)); + // 6.0 TODO: simplify with info.GetValue(nameof(_implementation), typeof(object)); + foreach (var entry in info) + { + if (entry.Name == nameof(_implementation)) + { + _implementation = entry.Value; + } + } } [SecurityCritical] public object GetRealObject(StreamingContext context) { - return _proxyFactoryInfo.CreateProxyFactory().GetProxy(_identifier, null); + var proxy = _proxyFactoryInfo.CreateProxyFactory().GetProxy(_identifier, null); + + if (_implementation != null) + proxy.HibernateLazyInitializer.SetImplementation(_implementation); + + return proxy; } [SecurityCritical] @@ -33,6 +53,7 @@ public void GetObjectData(SerializationInfo info, StreamingContext context) { info.AddValue(nameof(_proxyFactoryInfo), _proxyFactoryInfo); info.AddValue(nameof(_identifier), _identifier); + info.AddValue(nameof(_implementation), _implementation); } } } From f5e702c370b6b0e73fb20ad930568a136c8f3127 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, 28 May 2018 15:41:25 +0200 Subject: [PATCH 02/17] Have static proxy not always failing PeVerify --- src/NHibernate/Proxy/NHibernateProxyBuilder.cs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/NHibernate/Proxy/NHibernateProxyBuilder.cs b/src/NHibernate/Proxy/NHibernateProxyBuilder.cs index cf9644cd645..3154ad8e9db 100644 --- a/src/NHibernate/Proxy/NHibernateProxyBuilder.cs +++ b/src/NHibernate/Proxy/NHibernateProxyBuilder.cs @@ -96,7 +96,7 @@ public TypeInfo CreateProxyType(System.Type baseType, IReadOnlyCollection()); typeBuilder.SetCustomAttribute(customAttributeBuilder); - ImplementDeserializationConstructor(typeBuilder); + ImplementDeserializationConstructor(typeBuilder, parentType); ImplementGetObjectData(typeBuilder, proxyInfoField, lazyInitializerField); var proxyType = typeBuilder.CreateTypeInfo(); @@ -169,13 +169,24 @@ private static void ImplementConstructor(TypeBuilder typeBuilder, System.Type pa IL.Emit(OpCodes.Ret); } - private static void ImplementDeserializationConstructor(TypeBuilder typeBuilder) + private static void ImplementDeserializationConstructor(TypeBuilder typeBuilder, System.Type parentType) { var parameterTypes = new[] {typeof (SerializationInfo), typeof (StreamingContext)}; var constructor = typeBuilder.DefineConstructor(constructorAttributes, CallingConventions.Standard, parameterTypes); constructor.SetImplementationFlags(MethodImplAttributes.IL | MethodImplAttributes.Managed); var IL = constructor.GetILGenerator(); + + constructor.SetImplementationFlags(MethodImplAttributes.IL | MethodImplAttributes.Managed); + + var baseConstructor = parentType.GetConstructor(BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public, null, System.Type.EmptyTypes, null); + // if there is no default constructor, or the default constructor is private/internal, call System.Object constructor + // this works, but the generated assembly will fail PeVerify (cannot use in medium trust for example) + if (baseConstructor == null || baseConstructor.IsPrivate || baseConstructor.IsAssembly) + baseConstructor = ObjectConstructor; + IL.Emit(OpCodes.Ldarg_0); + IL.Emit(OpCodes.Call, baseConstructor); + //Everything is done in NHibernateProxyObjectReference, so just return data. IL.Emit(OpCodes.Ret); } From 683df9533be5c0a53a65a967e51431312961660a Mon Sep 17 00:00:00 2001 From: Francois Botha Date: Mon, 4 Jun 2018 18:56:38 +0200 Subject: [PATCH 03/17] GH1730 Query Cache effective only after closing the session that created the cache --- .../Async/NHSpecificTest/GH1730/Fixture.cs | 78 +++++++++++++++++++ .../NHSpecificTest/GH1730/Entity.cs | 10 +++ .../NHSpecificTest/GH1730/Fixture.cs | 67 ++++++++++++++++ .../NHSpecificTest/GH1730/Mappings.hbm.xml | 10 +++ .../Async/Cache/StandardQueryCache.cs | 2 +- src/NHibernate/Cache/StandardQueryCache.cs | 2 +- 6 files changed, 167 insertions(+), 2 deletions(-) create mode 100644 src/NHibernate.Test/Async/NHSpecificTest/GH1730/Fixture.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH1730/Entity.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH1730/Fixture.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/GH1730/Mappings.hbm.xml diff --git a/src/NHibernate.Test/Async/NHSpecificTest/GH1730/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/GH1730/Fixture.cs new file mode 100644 index 00000000000..3b9b66878df --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/GH1730/Fixture.cs @@ -0,0 +1,78 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by AsyncGenerator. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + + +using NHibernate.Cfg; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH1730 +{ + using System.Threading.Tasks; + [TestFixture] + public class FixtureAsync : BugTestCase + { + protected override void Configure(Configuration configuration) + { + cfg.SetProperty(Environment.GenerateStatistics, "true"); + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.CreateQuery("delete from Entity").ExecuteUpdate(); + + transaction.Commit(); + } + } + + [Test] + public async Task HitCacheInSameSessionAsync() + { + await (Sfi.EvictQueriesAsync()); + Sfi.Statistics.Clear(); + var entities = new System.Collections.Generic.List(); + + using (var session = OpenSession()) + { + using (var transaction = session.BeginTransaction()) + { + for (int i = 0; i < 3; i++) + { + var e = new Entity { Name = "Name" + i }; + entities.Add(e); + await (session.SaveAsync(e)); + } + await (transaction.CommitAsync()); + } + + var queryString = "from Entity"; + + using (var tx = session.BeginTransaction()) + { + // this query will hit the database and create the cache + await (session.CreateQuery(queryString).SetCacheable(true).ListAsync()); + await (tx.CommitAsync()); + } + + using (var transaction = session.BeginTransaction()) + { + //and this one SHOULD served by the cache + await (session.CreateQuery(queryString).SetCacheable(true).ListAsync()); + await (transaction.CommitAsync()); + } + + var qs = Sfi.Statistics.GetQueryStatistics(queryString); + Assert.AreEqual(1, qs.CacheHitCount); + Assert.AreEqual(1, qs.CachePutCount); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH1730/Entity.cs b/src/NHibernate.Test/NHSpecificTest/GH1730/Entity.cs new file mode 100644 index 00000000000..2ba2a997abd --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH1730/Entity.cs @@ -0,0 +1,10 @@ +using System; + +namespace NHibernate.Test.NHSpecificTest.GH1730 +{ + class Entity + { + public virtual Guid Id { get; set; } + public virtual string Name { get; set; } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH1730/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/GH1730/Fixture.cs new file mode 100644 index 00000000000..7d69c1c48bb --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH1730/Fixture.cs @@ -0,0 +1,67 @@ +using NHibernate.Cfg; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.GH1730 +{ + [TestFixture] + public class Fixture : BugTestCase + { + protected override void Configure(Configuration configuration) + { + cfg.SetProperty(Environment.GenerateStatistics, "true"); + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var transaction = session.BeginTransaction()) + { + session.CreateQuery("delete from Entity").ExecuteUpdate(); + + transaction.Commit(); + } + } + + [Test] + public void HitCacheInSameSession() + { + Sfi.EvictQueries(); + Sfi.Statistics.Clear(); + var entities = new System.Collections.Generic.List(); + + using (var session = OpenSession()) + { + using (var transaction = session.BeginTransaction()) + { + for (int i = 0; i < 3; i++) + { + var e = new Entity { Name = "Name" + i }; + entities.Add(e); + session.Save(e); + } + transaction.Commit(); + } + + var queryString = "from Entity"; + + using (var tx = session.BeginTransaction()) + { + // this query will hit the database and create the cache + session.CreateQuery(queryString).SetCacheable(true).List(); + tx.Commit(); + } + + using (var transaction = session.BeginTransaction()) + { + //and this one SHOULD served by the cache + session.CreateQuery(queryString).SetCacheable(true).List(); + transaction.Commit(); + } + + var qs = Sfi.Statistics.GetQueryStatistics(queryString); + Assert.AreEqual(1, qs.CacheHitCount); + Assert.AreEqual(1, qs.CachePutCount); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/GH1730/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/GH1730/Mappings.hbm.xml new file mode 100644 index 00000000000..e18236aa712 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/GH1730/Mappings.hbm.xml @@ -0,0 +1,10 @@ + + + + + + + + + diff --git a/src/NHibernate/Async/Cache/StandardQueryCache.cs b/src/NHibernate/Async/Cache/StandardQueryCache.cs index 180064d1ad2..a85a0916311 100644 --- a/src/NHibernate/Async/Cache/StandardQueryCache.cs +++ b/src/NHibernate/Async/Cache/StandardQueryCache.cs @@ -41,7 +41,7 @@ public async Task PutAsync(QueryKey key, ICacheAssembler[] returnTypes, IL if (isNaturalKeyLookup && result.Count == 0) return false; - long ts = session.Timestamp; + long ts = session.Factory.Settings.CacheProvider.NextTimestamp(); if (Log.IsDebugEnabled()) Log.Debug("caching query results in region: '{0}'; {1}", _regionName, key); diff --git a/src/NHibernate/Cache/StandardQueryCache.cs b/src/NHibernate/Cache/StandardQueryCache.cs index b8c612dd520..a1643d0f998 100644 --- a/src/NHibernate/Cache/StandardQueryCache.cs +++ b/src/NHibernate/Cache/StandardQueryCache.cs @@ -60,7 +60,7 @@ public bool Put(QueryKey key, ICacheAssembler[] returnTypes, IList result, bool if (isNaturalKeyLookup && result.Count == 0) return false; - long ts = session.Timestamp; + long ts = session.Factory.Settings.CacheProvider.NextTimestamp(); if (Log.IsDebugEnabled()) Log.Debug("caching query results in region: '{0}'; {1}", _regionName, key); From 25ff3dbe7ee97298e3ad9a9d2eec0cebc202c253 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Wed, 6 Jun 2018 18:07:01 +0300 Subject: [PATCH 04/17] Remove excessive rowIdAlias parameter in Loader --- src/NHibernate/Async/Loader/Loader.cs | 11 +++++------ src/NHibernate/Loader/Loader.cs | 11 +++++------ 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/NHibernate/Async/Loader/Loader.cs b/src/NHibernate/Async/Loader/Loader.cs index 4512a32b186..6e4e4bb3cec 100644 --- a/src/NHibernate/Async/Loader/Loader.cs +++ b/src/NHibernate/Async/Loader/Loader.cs @@ -588,7 +588,6 @@ private async Task GetRowAsync(DbDataReader rs, ILoadable[] persisters { cancellationToken.ThrowIfCancellationRequested(); int cols = persisters.Length; - IEntityAliases[] descriptors = EntityAliases; if (Log.IsDebugEnabled()) { @@ -635,7 +634,7 @@ private async Task GetRowAsync(DbDataReader rs, ILoadable[] persisters else { obj = - await (InstanceNotYetLoadedAsync(rs, i, persister, key, lockModes[i], descriptors[i].RowIdAlias, optionalObjectKey, + await (InstanceNotYetLoadedAsync(rs, i, persister, key, lockModes[i], optionalObjectKey, optionalObject, hydratedObjects, session, cancellationToken)).ConfigureAwait(false); } // #1226: Even if it is already loaded, if it can be loaded from an association with a property ref, make @@ -683,7 +682,7 @@ private async Task InstanceAlreadyLoadedAsync(DbDataReader rs, int i, IEntityPer /// The entity instance is not in the session cache /// private async Task InstanceNotYetLoadedAsync(DbDataReader dr, int i, ILoadable persister, EntityKey key, LockMode lockMode, - string rowIdAlias, EntityKey optionalObjectKey, object optionalObject, + EntityKey optionalObjectKey, object optionalObject, IList hydratedObjects, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -707,7 +706,7 @@ private async Task InstanceNotYetLoadedAsync(DbDataReader dr, int i, ILo // (but don't yet initialize the object itself) // note that we acquired LockMode.READ even if it was not requested LockMode acquiredLockMode = lockMode == LockMode.None ? LockMode.Read : lockMode; - await (LoadFromResultSetAsync(dr, i, obj, instanceClass, key, rowIdAlias, acquiredLockMode, persister, session, cancellationToken)).ConfigureAwait(false); + await (LoadFromResultSetAsync(dr, i, obj, instanceClass, key, acquiredLockMode, persister, session, cancellationToken)).ConfigureAwait(false); // materialize associations (and initialize the object) later hydratedObjects.Add(obj); @@ -721,7 +720,7 @@ private async Task InstanceNotYetLoadedAsync(DbDataReader dr, int i, ILo /// and pass the hydrated state to the session. /// private async Task LoadFromResultSetAsync(DbDataReader rs, int i, object obj, string instanceClass, EntityKey key, - string rowIdAlias, LockMode lockMode, ILoadable rootPersister, + LockMode lockMode, ILoadable rootPersister, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); @@ -750,7 +749,7 @@ private async Task LoadFromResultSetAsync(DbDataReader rs, int i, object obj, st object[] values = await (persister.HydrateAsync(rs, id, obj, rootPersister, cols, eagerPropertyFetch, session, cancellationToken)).ConfigureAwait(false); - object rowId = persister.HasRowId ? rs[rowIdAlias] : null; + object rowId = persister.HasRowId ? rs[EntityAliases[i].RowIdAlias] : null; TwoPhaseLoad.PostHydrate(persister, id, values, rowId, obj, lockMode, !eagerPropertyFetch, session); } diff --git a/src/NHibernate/Loader/Loader.cs b/src/NHibernate/Loader/Loader.cs index 83767386794..5565085de0b 100644 --- a/src/NHibernate/Loader/Loader.cs +++ b/src/NHibernate/Loader/Loader.cs @@ -919,7 +919,6 @@ private object[] GetRow(DbDataReader rs, ILoadable[] persisters, EntityKey[] key ISessionImplementor session, bool mustLoadMissingEntity) { int cols = persisters.Length; - IEntityAliases[] descriptors = EntityAliases; if (Log.IsDebugEnabled()) { @@ -966,7 +965,7 @@ private object[] GetRow(DbDataReader rs, ILoadable[] persisters, EntityKey[] key else { obj = - InstanceNotYetLoaded(rs, i, persister, key, lockModes[i], descriptors[i].RowIdAlias, optionalObjectKey, + InstanceNotYetLoaded(rs, i, persister, key, lockModes[i], optionalObjectKey, optionalObject, hydratedObjects, session); } // #1226: Even if it is already loaded, if it can be loaded from an association with a property ref, make @@ -1045,7 +1044,7 @@ private void CacheByUniqueKey(int i, IEntityPersister persister, object obj, ISe /// The entity instance is not in the session cache /// private object InstanceNotYetLoaded(DbDataReader dr, int i, ILoadable persister, EntityKey key, LockMode lockMode, - string rowIdAlias, EntityKey optionalObjectKey, object optionalObject, + EntityKey optionalObjectKey, object optionalObject, IList hydratedObjects, ISessionImplementor session) { object obj; @@ -1068,7 +1067,7 @@ private object InstanceNotYetLoaded(DbDataReader dr, int i, ILoadable persister, // (but don't yet initialize the object itself) // note that we acquired LockMode.READ even if it was not requested LockMode acquiredLockMode = lockMode == LockMode.None ? LockMode.Read : lockMode; - LoadFromResultSet(dr, i, obj, instanceClass, key, rowIdAlias, acquiredLockMode, persister, session); + LoadFromResultSet(dr, i, obj, instanceClass, key, acquiredLockMode, persister, session); // materialize associations (and initialize the object) later hydratedObjects.Add(obj); @@ -1093,7 +1092,7 @@ private bool IsEagerPropertyFetchEnabled(int i) /// and pass the hydrated state to the session. /// private void LoadFromResultSet(DbDataReader rs, int i, object obj, string instanceClass, EntityKey key, - string rowIdAlias, LockMode lockMode, ILoadable rootPersister, + LockMode lockMode, ILoadable rootPersister, ISessionImplementor session) { object id = key.Identifier; @@ -1121,7 +1120,7 @@ private void LoadFromResultSet(DbDataReader rs, int i, object obj, string instan object[] values = persister.Hydrate(rs, id, obj, rootPersister, cols, eagerPropertyFetch, session); - object rowId = persister.HasRowId ? rs[rowIdAlias] : null; + object rowId = persister.HasRowId ? rs[EntityAliases[i].RowIdAlias] : null; TwoPhaseLoad.PostHydrate(persister, id, values, rowId, obj, lockMode, !eagerPropertyFetch, session); } From 193bfb9116c1b4ebd14de9de526f0d0c3b6c245a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericDelaporte@users.noreply.github.com> Date: Tue, 5 Jun 2018 17:01:38 +0200 Subject: [PATCH 05/17] Fix thread safety issue in Loader --- src/NHibernate/Async/Loader/Loader.cs | 1 + src/NHibernate/Loader/Loader.cs | 32 +++++++++++++-------------- 2 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/NHibernate/Async/Loader/Loader.cs b/src/NHibernate/Async/Loader/Loader.cs index 0167ee2ad08..cc0dadf80b8 100644 --- a/src/NHibernate/Async/Loader/Loader.cs +++ b/src/NHibernate/Async/Loader/Loader.cs @@ -10,6 +10,7 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; using System.Data.Common; diff --git a/src/NHibernate/Loader/Loader.cs b/src/NHibernate/Loader/Loader.cs index 13bb38cdfe3..0882e0168f9 100644 --- a/src/NHibernate/Loader/Loader.cs +++ b/src/NHibernate/Loader/Loader.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Concurrent; using System.Collections.Generic; using System.Data; using System.Data.Common; @@ -31,17 +32,21 @@ namespace NHibernate.Loader /// Abstract superclass of object loading (and querying) strategies. /// /// - ///

+ /// /// This class implements useful common functionality that concrete loaders would delegate to. /// It is not intended that this functionality would be directly accessed by client code (Hence, /// all methods of this class are declared protected or private.) This class relies heavily upon the - /// interface, which is the contract between this class and + /// interface, which is the contract between this class and /// s that may be loaded by it. - ///

- ///

- /// The present implementation is able to load any number of columns of entities and at most + /// + /// + /// The present implementation is able to load any number of columns of entities and at most /// one collection role per query. - ///

+ /// + /// + /// All this class members are thread safe. Entity and collection loaders are held in persisters shared among + /// sessions built from the same session factory. They must be thread safe. + /// ///
/// public abstract partial class Loader @@ -63,8 +68,8 @@ public abstract partial class Loader /// /// Caches subclass entity aliases for given persister index in and subclass entity name /// - private readonly Dictionary, string[][]> _subclassEntityAliasesMap = new Dictionary, string[][]>(); - + private readonly ConcurrentDictionary, string[][]> _subclassEntityAliasesMap = new ConcurrentDictionary, string[][]>(); + protected Loader(ISessionFactoryImplementor factory) { _factory = factory; @@ -1103,14 +1108,9 @@ private void LoadFromResultSet(DbDataReader rs, int i, object obj, string instan private string[][] GetSubclassEntityAliases(int i, ILoadable persister) { var cacheKey = System.Tuple.Create(i, persister.EntityName); - if (_subclassEntityAliasesMap.TryGetValue(cacheKey, out string[][] cols)) - { - return cols; - } - - cols = EntityAliases[i].GetSuffixedPropertyAliases(persister); - _subclassEntityAliasesMap[cacheKey] = cols; - return cols; + return _subclassEntityAliasesMap.GetOrAdd( + cacheKey, + k => EntityAliases[i].GetSuffixedPropertyAliases(persister)); } /// From dc71c7df5c22d82991bc1b009ecdfe2ec9902b45 Mon Sep 17 00:00:00 2001 From: Ricardo Peres Date: Thu, 7 Jun 2018 10:37:36 +0100 Subject: [PATCH 06/17] NH-3670 - Dynamic component should allow generic dictionary (#305) Fixes #755 Co-authored-by: Alexander Zaytsev --- doc/reference/modules/basic_mapping.xml | 3 +- .../NHSpecificTest/NH1039Generic/Fixture.cs | 68 ++++++ .../NHSpecificTest/NH1796Generic/Fixture.cs | 76 +++++++ .../NHSpecificTest/NH2664Generic/Fixture.cs | 124 +++++++++++ .../NHSpecificTest/NH3571Generic/Fixture.cs | 109 ++++++++++ .../DynamicComponentMappingTests.cs | 91 +++++++- .../JoinGenericDynamicComponentTests.cs | 195 ++++++++++++++++++ .../AbstractPropertyContainerMapperTest.cs | 23 +++ .../ComponentPropertyOnDynamicCompoTests.cs | 53 +++++ ...DynComponentPropertyOnDynamicCompoTests.cs | 54 +++++ .../NHSpecificTest/NH1039Generic/Fixture.cs | 57 +++++ .../NH1039Generic/Mappings.hbm.xml | 21 ++ .../NHSpecificTest/NH1039Generic/Model.cs | 35 ++++ .../NHSpecificTest/NH1796Generic/Entity.cs | 12 ++ .../NHSpecificTest/NH1796Generic/Fixture.cs | 65 ++++++ .../NH1796Generic/Mappings.hbm.xml | 16 ++ .../NHSpecificTest/NH2664Generic/Fixture.cs | 134 ++++++++++++ .../NH2664Generic/Mappings.hbm.xml | 17 ++ .../NHSpecificTest/NH2664Generic/Product.cs | 24 +++ .../NHSpecificTest/NH3571Generic/Fixture.cs | 122 +++++++++++ .../NH3571Generic/Mappings.hbm.xml | 17 ++ .../NHSpecificTest/NH3571Generic/Product.cs | 43 ++++ src/NHibernate/Linq/Visitors/VisitorUtil.cs | 2 +- .../ByCode/IPlainPropertyContainerMapper.cs | 2 + .../Impl/CustomizersImpl/JoinCustomizer.cs | 12 +- .../PropertyContainerCustomizer.cs | 13 ++ .../Tuple/Component/ComponentMetamodel.cs | 1 + .../Component/DynamicComponentInstantiator.cs | 15 ++ .../Component/DynamicMapComponentTuplizer.cs | 32 +-- .../Tuple/DynamicMapInstantiator.cs | 2 + 30 files changed, 1403 insertions(+), 35 deletions(-) create mode 100644 src/NHibernate.Test/Async/NHSpecificTest/NH1039Generic/Fixture.cs create mode 100644 src/NHibernate.Test/Async/NHSpecificTest/NH1796Generic/Fixture.cs create mode 100644 src/NHibernate.Test/Async/NHSpecificTest/NH2664Generic/Fixture.cs create mode 100644 src/NHibernate.Test/Async/NHSpecificTest/NH3571Generic/Fixture.cs create mode 100644 src/NHibernate.Test/MappingByCode/ExplicitMappingTests/JoinGenericDynamicComponentTests.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH1039Generic/Fixture.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH1039Generic/Mappings.hbm.xml create mode 100644 src/NHibernate.Test/NHSpecificTest/NH1039Generic/Model.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH1796Generic/Entity.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH1796Generic/Fixture.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH1796Generic/Mappings.hbm.xml create mode 100644 src/NHibernate.Test/NHSpecificTest/NH2664Generic/Fixture.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH2664Generic/Mappings.hbm.xml create mode 100644 src/NHibernate.Test/NHSpecificTest/NH2664Generic/Product.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3571Generic/Fixture.cs create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3571Generic/Mappings.hbm.xml create mode 100644 src/NHibernate.Test/NHSpecificTest/NH3571Generic/Product.cs create mode 100644 src/NHibernate/Tuple/Component/DynamicComponentInstantiator.cs diff --git a/doc/reference/modules/basic_mapping.xml b/doc/reference/modules/basic_mapping.xml index c6d349b29fd..923714dec99 100644 --- a/doc/reference/modules/basic_mapping.xml +++ b/doc/reference/modules/basic_mapping.xml @@ -2081,7 +2081,8 @@ The <dynamic-component> element allows an IDictionary - to be mapped as a component, where the property names refer to keys of the dictionary. + or IDictionaryto be mapped as a component, where the property + names refer to keys of the dictionary. diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH1039Generic/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH1039Generic/Fixture.cs new file mode 100644 index 00000000000..dc9c9831243 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/NH1039Generic/Fixture.cs @@ -0,0 +1,68 @@ +//------------------------------------------------------------------------------ +// +// 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; +using System.Collections.Generic; +using NUnit.Framework; + + +namespace NHibernate.Test.NHSpecificTest.NH1039Generic +{ + using System.Threading.Tasks; + [TestFixture] + public class FixtureAsync : BugTestCase + { + public override string BugNumber + { + get { return "NH1039Generic"; } + } + + protected override void OnTearDown() + { + base.OnTearDown(); + using (ISession s = OpenSession()) + using (ITransaction tx = s.BeginTransaction()) + { + s.Delete("from Person"); + tx.Commit(); + } + } + + [Test] + public async Task testAsync() + { + using (ISession s = OpenSession()) + using (ITransaction tx = s.BeginTransaction()) + { + Person person = new Person("1"); + person.Name = "John Doe"; + var set = new HashSet(); + set.Add("555-1234"); + set.Add("555-4321"); + person.Properties.Add("Phones", set); + + await (s.SaveAsync(person)); + await (tx.CommitAsync()); + } + using (ISession s = OpenSession()) + using (ITransaction tx = s.BeginTransaction()) + { + Person person = (Person)await (s.CreateCriteria(typeof(Person)).UniqueResultAsync()); + + Assert.AreEqual("1", person.ID); + Assert.AreEqual("John Doe", person.Name); + Assert.AreEqual(1, person.Properties.Count); + Assert.That(person.Properties["Phones"], Is.InstanceOf>()); + Assert.IsTrue(((ISet) person.Properties["Phones"]).Contains("555-1234")); + Assert.IsTrue(((ISet) person.Properties["Phones"]).Contains("555-4321")); + } + } + } +} diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH1796Generic/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH1796Generic/Fixture.cs new file mode 100644 index 00000000000..7d31066907f --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/NH1796Generic/Fixture.cs @@ -0,0 +1,76 @@ +//------------------------------------------------------------------------------ +// +// 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.Collections.Generic; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.NH1796Generic +{ + using System.Threading.Tasks; + [TestFixture] + public class FixtureAsync: BugTestCase + { + [Test] + public async Task MergeAsync() + { + var entity = new Entity { Name = "Vinnie Luther" }; + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + await (s.SaveAsync(entity)); + await (t.CommitAsync()); + } + + entity.DynProps = new Dictionary(); + entity.DynProps["StrProp"] = "Modified"; + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + await (s.MergeAsync(entity)); + await (t.CommitAsync()); + } + + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + await (s.CreateQuery("delete from Entity").ExecuteUpdateAsync()); + await (t.CommitAsync()); + } + } + + [Test] + public async Task SaveOrUpdateAsync() + { + var entity = new Entity { Name = "Vinnie Luther" }; + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + await (s.SaveOrUpdateAsync(entity)); + await (t.CommitAsync()); + } + + entity.DynProps = new Dictionary(); + entity.DynProps["StrProp"] = "Modified"; + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + await (s.SaveOrUpdateAsync(entity)); + await (t.CommitAsync()); + } + + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + await (s.CreateQuery("delete from Entity").ExecuteUpdateAsync()); + await (t.CommitAsync()); + } + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH2664Generic/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH2664Generic/Fixture.cs new file mode 100644 index 00000000000..8080952e1f9 --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/NH2664Generic/Fixture.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.Collections; +using System.Linq; +using NHibernate.Linq; +using NUnit.Framework; +using System.Linq.Expressions; +using System; + +namespace NHibernate.Test.NHSpecificTest.NH2664Generic +{ + using System.Threading.Tasks; + [TestFixture] + public class FixtureAsync : TestCase + { + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override IList Mappings => new[] {"NHSpecificTest.NH2664Generic.Mappings.hbm.xml"}; + + /// + /// push some data into the database + /// Really functions as a save test also + /// + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var tran = session.BeginTransaction()) + { + session.Save( + new Product + { + ProductId = "1", + Properties = + { + ["Name"] = "First Product", + ["Description"] = "First Description" + } + }); + + session.Save( + new Product + { + ProductId = "2", + Properties = + { + ["Name"] = "Second Product", + ["Description"] = "Second Description" + } + }); + + session.Save( + new Product + { + ProductId = "3", + Properties = + { + ["Name"] = "val", + ["Description"] = "val" + } + }); + + tran.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var tran = session.BeginTransaction()) + { + session.CreateQuery("delete from Product").ExecuteUpdate(); + tran.Commit(); + } + } + + [Test] + public async Task Query_DynamicComponentAsync() + { + using (var session = OpenSession()) + { + var product = await (( + from p in session.Query() + where (string) p.Properties["Name"] == "First Product" + select p + ).SingleAsync()); + + Assert.That(product, Is.Not.Null); + Assert.That(product.Properties["Name"], Is.EqualTo("First Product")); + } + } + + [Test] + public async Task Multiple_Query_Does_Not_CacheAsync() + { + using (var session = OpenSession()) + { + // Query by name + var product1 = await (( + from p in session.Query() + where (string) p.Properties["Name"] == "First Product" + select p + ).SingleAsync()); + Assert.That(product1.ProductId, Is.EqualTo("1")); + + // Query by description (this test is to verify that the dictionary + // index isn't cached from the query above. + var product2 = await (( + from p in session.Query() + where (string) p.Properties["Description"] == "Second Description" + select p + ).SingleAsync()); + Assert.That(product2.ProductId, Is.EqualTo("2")); + } + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH3571Generic/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH3571Generic/Fixture.cs new file mode 100644 index 00000000000..3af2d88003a --- /dev/null +++ b/src/NHibernate.Test/Async/NHSpecificTest/NH3571Generic/Fixture.cs @@ -0,0 +1,109 @@ +//------------------------------------------------------------------------------ +// +// 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.Collections; +using System.Linq; +using NHibernate.Linq; +using NUnit.Framework; +using System.Linq.Expressions; +using System; + +namespace NHibernate.Test.NHSpecificTest.NH3571Generic +{ + using System.Threading.Tasks; + [TestFixture] + public class FixtureAsync : TestCase + { + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override IList Mappings => new[] {"NHSpecificTest.NH3571Generic.Mappings.hbm.xml"}; + + /// + /// push some data into the database + /// Really functions as a save test also + /// + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var tran = session.BeginTransaction()) + { + var product = new Product {ProductId = "1"}; + product.Details.Properties["Name"] = "First Product"; + product.Details.Properties["Description"] = "First Description"; + + session.Save(product); + + product = new Product {ProductId = "2"}; + product.Details.Properties["Name"] = "Second Product"; + product.Details.Properties["Description"] = "Second Description"; + + session.Save(product); + + product = new Product {ProductId = "3"}; + product.Details.Properties["Name"] = "val"; + product.Details.Properties["Description"] = "val"; + + session.Save(product); + + tran.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var tran = session.BeginTransaction()) + { + session.CreateQuery("delete from Product").ExecuteUpdate(); + tran.Commit(); + } + } + + [Test] + public async Task CanQueryDynamicComponentInComponentAsync() + { + using (var session = OpenSession()) + { + var product = await (( + from p in session.Query() + where (string) p.Details.Properties["Name"] == "First Product" + select p + ).SingleAsync()); + + Assert.That(product, Is.Not.Null); + Assert.That(product.Details.Properties["Name"], Is.EqualTo("First Product")); + } + } + + [Test] + public async Task MultipleQueriesShouldNotCacheAsync() + { + using (var session = OpenSession()) + { + // Query by name + var product1 = await (( + from p in session.Query() + where (string) p.Details.Properties["Name"] == "First Product" + select p + ).SingleAsync()); + Assert.That(product1.ProductId, Is.EqualTo("1")); + + // Query by description (this test is to verify that the dictionary + // index isn't cached from the query above. + var product2 = await (( + from p in session.Query() + where (string) p.Details.Properties["Description"] == "Second Description" + select p + ).SingleAsync()); + Assert.That(product2.ProductId, Is.EqualTo("2")); + } + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/MappingByCode/ExplicitMappingTests/DynamicComponentMappingTests.cs b/src/NHibernate.Test/MappingByCode/ExplicitMappingTests/DynamicComponentMappingTests.cs index c5db3e91f91..457107d5a4e 100644 --- a/src/NHibernate.Test/MappingByCode/ExplicitMappingTests/DynamicComponentMappingTests.cs +++ b/src/NHibernate.Test/MappingByCode/ExplicitMappingTests/DynamicComponentMappingTests.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq; using NHibernate.Cfg.MappingSchema; using NHibernate.Mapping.ByCode; @@ -21,6 +22,17 @@ public IDictionary Info } } + private class PersonWithGenericInfo + { + public int Id { get; set; } + private IDictionary info; + public IDictionary Info + { + get { return info; } + set { info = value; } + } + } + [Test] public void WhenMapDynCompoThenMapItAndItsProperties() { @@ -38,6 +50,23 @@ public void WhenMapDynCompoThenMapItAndItsProperties() Assert.That(hbmDynamicComponent.Properties.Select(x=> x.Name), Is.EquivalentTo(new [] {"MyInt", "MyDate"})); } + [Test] + public void WhenMapDynCompoThenMapItAndItsPropertiesGeneric() + { + var mapper = new ModelMapper(); + mapper.Class(map => + { + map.Id(x => x.Id, idmap => { }); + map.Component(x => x.Info, new { MyInt = 5, MyDate = DateTime.Now }, z => { }); + }); + + var hbmMapping = mapper.CompileMappingFor(new[] { typeof(PersonWithGenericInfo) }); + var hbmClass = hbmMapping.RootClasses[0]; + var hbmDynamicComponent = hbmClass.Properties.OfType().SingleOrDefault(); + Assert.That(hbmDynamicComponent, Is.Not.Null); + Assert.That(hbmDynamicComponent.Properties.Select(x=> x.Name), Is.EquivalentTo(new [] {"MyInt", "MyDate"})); + } + [Test] public void WhenMapDynCompoPropertiesThenShouldAssignPropertyType() { @@ -54,6 +83,22 @@ public void WhenMapDynCompoPropertiesThenShouldAssignPropertyType() Assert.That(hbmDynamicComponent.Properties.OfType().All(x => !string.IsNullOrEmpty(x.type1)), Is.True); } + [Test] + public void WhenMapDynCompoPropertiesThenShouldAssignPropertyTypeGeneric() + { + var mapper = new ModelMapper(); + mapper.Class(map => + { + map.Id(x => x.Id, idmap => { }); + map.Component(x => x.Info, new { MyInt = 5, MyDate = DateTime.Now }, z => { }); + }); + + var hbmMapping = mapper.CompileMappingFor(new[] { typeof(PersonWithGenericInfo) }); + var hbmClass = hbmMapping.RootClasses[0]; + var hbmDynamicComponent = hbmClass.Properties.OfType().Single(); + Assert.That(hbmDynamicComponent.Properties.OfType().All(x => !string.IsNullOrEmpty(x.type1)), Is.True); + } + [Test] public void WhenMapDynCompoAttributesThenMapAttributes() { @@ -61,14 +106,17 @@ public void WhenMapDynCompoAttributesThenMapAttributes() mapper.Class(map => { map.Id(x => x.Id, idmap => { }); - map.Component(x => x.Info, new { MyInt = 5}, z => - { - z.Access(Accessor.Field); - z.Insert(false); - z.Update(false); - z.Unique(true); - z.OptimisticLock(false); - }); + map.Component( + x => x.Info, + new {MyInt = 5}, + z => + { + z.Access(Accessor.Field); + z.Insert(false); + z.Update(false); + z.Unique(true); + z.OptimisticLock(false); + }); }); var hbmMapping = mapper.CompileMappingFor(new[] { typeof(Person) }); @@ -80,5 +128,32 @@ public void WhenMapDynCompoAttributesThenMapAttributes() Assert.That(hbmDynamicComponent.optimisticlock, Is.False); Assert.That(hbmDynamicComponent.unique, Is.True); } + + [Test] + public void WhenMapDynCompoAttributesThenMapAttributesGeneric() + { + var mapper = new ModelMapper(); + mapper.Class(map => + { + map.Id(x => x.Id, idmap => { }); + map.Component(x => x.Info, new { MyInt = 5 }, z => + { + z.Access(Accessor.Field); + z.Insert(false); + z.Update(false); + z.Unique(true); + z.OptimisticLock(false); + }); + }); + + var hbmMapping = mapper.CompileMappingFor(new[] { typeof(PersonWithGenericInfo) }); + var hbmClass = hbmMapping.RootClasses[0]; + var hbmDynamicComponent = hbmClass.Properties.OfType().SingleOrDefault(); + Assert.That(hbmDynamicComponent.access, Does.Contain("field")); + Assert.That(hbmDynamicComponent.insert, Is.False); + Assert.That(hbmDynamicComponent.update, Is.False); + Assert.That(hbmDynamicComponent.optimisticlock, Is.False); + Assert.That(hbmDynamicComponent.unique, Is.True); + } } } diff --git a/src/NHibernate.Test/MappingByCode/ExplicitMappingTests/JoinGenericDynamicComponentTests.cs b/src/NHibernate.Test/MappingByCode/ExplicitMappingTests/JoinGenericDynamicComponentTests.cs new file mode 100644 index 00000000000..d21147c6acd --- /dev/null +++ b/src/NHibernate.Test/MappingByCode/ExplicitMappingTests/JoinGenericDynamicComponentTests.cs @@ -0,0 +1,195 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cfg.MappingSchema; +using NHibernate.Mapping.ByCode; +using NHibernate.Mapping.ByCode.Conformist; +using NUnit.Framework; + +namespace NHibernate.Test.MappingByCode.ExplicitMappingTests +{ + [TestFixture] + public class JoinGenericDynamicComponentTests + { + public class MyClass + { + public virtual int Code { get; set; } + public virtual string JoinedName { get; set; } + public virtual MyOther Relation { get; set; } + public virtual MyOther JoinedRelation { get; set; } + public virtual IDictionary Attributes { get; set; } + public virtual IDictionary JoinedAttributes { get; set; } + } + + public class MyOther + { + public int Id { get; set; } + } + + public class MyClassMap : ClassMapping + { + public MyClassMap() + { + // basic table related properties + ManyToOne(x => x.Relation); + Component(p => p.Attributes, + new + { + IsVisible = false, + Hash = default(Guid), + Reference = default(MyOther) + }, + m => + { + m.Property(p => p.IsVisible); + m.Property(p => p.Hash); + m.ManyToOne(p => p.Reference); + }); + Property(x => x.Code); + + // joined table stuff + Join("JoinedAttributes", x => + { + x.Property(p => p.JoinedName); + x.Component(p => p.JoinedAttributes, + new + { + OtherReference = default(MyOther), + Description = string.Empty, + Age = 0, + }, + m => + { + m.ManyToOne(p => p.OtherReference); + m.Property(p => p.Description); + m.Property(p => p.Age); + }); + x.ManyToOne(p => p.JoinedRelation); + }); + } + } + + private static HbmClass CompileClassMapping() + { + var mapper = new ModelMapper(); + mapper.AddMapping(typeof(MyClassMap)); + + var hbmMapping = mapper.CompileMappingFor(new[] { typeof(MyClass) }); + var hbmClass = hbmMapping.RootClasses[0]; + + return hbmClass; + } + + [Test] + public void WhenPropertyIsMappedOnRootThenItBelongsToRootTable() + { + // + var hbmClass = CompileClassMapping(); + Assert.That(hbmClass, Is.Not.Null); + + var rootProperties = hbmClass.Properties; + // p.Name == "Code"); + + Assert.That(hbmPropCode, Is.Not.Null); + Assert.That(hbmPropCode, Is.TypeOf()); + } + + [Test] + public void WhenDynamicComponentIsMappedOnRootThenItBelongsToRootTable() + { + // + var hbmClass = CompileClassMapping(); + Assert.That(hbmClass, Is.Not.Null); + + var rootProperties = hbmClass.Properties; + // p.Name == "Attributes") + ; + + Assert.That(hbmPropAttributes, Is.Not.Null); + Assert.That(hbmPropAttributes, Is.TypeOf()); + } + + [Test] + public void WhenRelationIsMappedOnRootThenItBelongsToRootTable() + { + // + var hbmClass = CompileClassMapping(); + Assert.That(hbmClass, Is.Not.Null); + + var rootProperties = hbmClass.Properties; + // p.Name == "Relation"); + + Assert.That(hbmPropRelation, Is.Not.Null); + Assert.That(hbmPropRelation, Is.TypeOf()); + } + + [Test] + public void WhenJoinedPropertyIsMappedOnJoinThenItBelongsToJoinTable() + { + // + var hbmClass = CompileClassMapping(); + Assert.That(hbmClass, Is.Not.Null); + + // + var hbmJoined = hbmClass.Joins.FirstOrDefault(); + Assert.That(hbmJoined, Is.Not.Null); + + var rootProperties = hbmJoined.Properties; + // + // p.Name == "JoinedName"); + + Assert.That(hbmPropJoinedName, Is.Not.Null); + Assert.That(hbmPropJoinedName, Is.TypeOf()); + } + + [Test] + public void WhenJoinedRelationIsMappedOnJoinThenItBelongsToJoinTable() + { + // + var hbmClass = CompileClassMapping(); + Assert.That(hbmClass, Is.Not.Null); + + // + var hbmJoined = hbmClass.Joins.FirstOrDefault(); + Assert.That(hbmJoined, Is.Not.Null); + + var rootProperties = hbmJoined.Properties; + // + // p.Name == "JoinedRelation"); + + Assert.That(hbmPropJoinedRelation, Is.Not.Null); + Assert.That(hbmPropJoinedRelation, Is.TypeOf()); + } + + [Test] + public void WhenJoinedDynamicComponentIsMappedOnJoinThenItBelongsToJoinTable() + { + // + var hbmClass = CompileClassMapping(); + Assert.That(hbmClass, Is.Not.Null); + + // + var hbmJoined = hbmClass.Joins.FirstOrDefault(); + Assert.That(hbmJoined, Is.Not.Null); + + var rootProperties = hbmJoined.Properties; + // + // + var hbmPropJoinedAttributes = rootProperties + .FirstOrDefault(p => p.Name == "JoinedAttributes"); + + Assert.That(hbmPropJoinedAttributes, Is.Not.Null); + Assert.That(hbmPropJoinedAttributes, Is.TypeOf()); + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/MappingByCode/MappersTests/AbstractPropertyContainerMapperTest.cs b/src/NHibernate.Test/MappingByCode/MappersTests/AbstractPropertyContainerMapperTest.cs index 2e2201019a1..90288d083f2 100644 --- a/src/NHibernate.Test/MappingByCode/MappersTests/AbstractPropertyContainerMapperTest.cs +++ b/src/NHibernate.Test/MappingByCode/MappersTests/AbstractPropertyContainerMapperTest.cs @@ -37,6 +37,10 @@ private class MyClassWithDynamic { public IDictionary DynCompo { get; set; } } + private class MyClassWithDynamicGeneric + { + public IDictionary DynCompo { get; set; } + } [Test] public void CantCreateWithoutHbmMapping() @@ -126,6 +130,15 @@ public void AddDynamicComponentProperty() Assert.That(properties.Single(), Is.TypeOf().And.Property("Name").EqualTo("DynCompo")); } + [Test] + public void AddDynamicComponentPropertyGeneric() + { + var properties = new List(); + var map = new StubPropertyContainerMapper(properties); + map.Component(For.Property(x => x.DynCompo), (IDynamicComponentMapper cp) => { }); + Assert.That(properties.Single(), Is.TypeOf().And.Property("Name").EqualTo("DynCompo")); + } + [Test] public void CallDynamicComponentMapper() { @@ -136,6 +149,16 @@ public void CallDynamicComponentMapper() Assert.That(called, Is.True); } + [Test] + public void CallDynamicComponentMapperGeneric() + { + var properties = new List(); + var map = new StubPropertyContainerMapper(properties); + var called = false; + map.Component(For.Property(x => x.DynCompo), (IDynamicComponentMapper cp) => called = true); + Assert.That(called, Is.True); + } + private class HackPropertyContainerMapper : AbstractPropertyContainerMapper { public HackPropertyContainerMapper(System.Type container, HbmMapping mapDoc) : base(container, mapDoc) {} diff --git a/src/NHibernate.Test/MappingByCode/MappersTests/DynamicComponentMapperTests/ComponentPropertyOnDynamicCompoTests.cs b/src/NHibernate.Test/MappingByCode/MappersTests/DynamicComponentMapperTests/ComponentPropertyOnDynamicCompoTests.cs index 3eb10a3163b..29962a22737 100644 --- a/src/NHibernate.Test/MappingByCode/MappersTests/DynamicComponentMapperTests/ComponentPropertyOnDynamicCompoTests.cs +++ b/src/NHibernate.Test/MappingByCode/MappersTests/DynamicComponentMapperTests/ComponentPropertyOnDynamicCompoTests.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Generic; using System.Linq; using NHibernate.Cfg.MappingSchema; using NHibernate.Mapping.ByCode; @@ -23,6 +24,18 @@ public IDictionary Info } } + private class PersonWithGenericInfo + { + public int Id { get; set; } +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + private IDictionary info; +#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value + public IDictionary Info + { + get { return info; } + } + } + private class MyClass { public int Something { get; set; } @@ -40,6 +53,19 @@ public void WhenAddThenHas() Assert.That(component.Properties.Select(x => x.Name), Is.EquivalentTo(new[] { "A" })); } + [Test] + public void WhenAddThenHasGeneric() + { + var mapdoc = new HbmMapping(); + var component = new HbmDynamicComponent(); + var mapper = new DynamicComponentMapper(component, For.Property(p => p.Info), mapdoc); + var propertyInfo = (new { A = (MyClass)null }).GetType().GetProperty("A"); + + mapper.Component(propertyInfo, (IComponentMapper x) => { }); + + Assert.That(component.Properties.Select(x => x.Name), Is.EquivalentTo(new[] { "A" })); + } + [Test] public void WhenCustomizeThenCallCustomizer() { @@ -54,6 +80,20 @@ public void WhenCustomizeThenCallCustomizer() Assert.That(called, Is.True); } + [Test] + public void WhenCustomizeThenCallCustomizerGeneric() + { + var mapdoc = new HbmMapping(); + var component = new HbmDynamicComponent(); + var mapper = new DynamicComponentMapper(component, For.Property(p => p.Info), mapdoc); + var propertyInfo = (new { A = (MyClass)null }).GetType().GetProperty("A"); + + var called = false; + mapper.Component(propertyInfo, (IComponentMapper x) => called = true); + + Assert.That(called, Is.True); + } + [Test] public void WhenCustomizeAccessorThenIgnore() { @@ -66,5 +106,18 @@ public void WhenCustomizeAccessorThenIgnore() Assert.That(component.Properties.OfType().Single().Access, Is.Null.Or.Empty); } + + [Test] + public void WhenCustomizeAccessorThenIgnoreGeneric() + { + var mapdoc = new HbmMapping(); + var component = new HbmDynamicComponent(); + var mapper = new DynamicComponentMapper(component, For.Property(p => p.Info), mapdoc); + var propertyInfo = (new { A = (MyClass)null }).GetType().GetProperty("A"); + + mapper.Component(propertyInfo, (IComponentMapper x) => x.Access(Accessor.Field)); + + Assert.That(component.Properties.OfType().Single().Access, Is.Null.Or.Empty); + } } } diff --git a/src/NHibernate.Test/MappingByCode/MappersTests/DynamicComponentMapperTests/DynComponentPropertyOnDynamicCompoTests.cs b/src/NHibernate.Test/MappingByCode/MappersTests/DynamicComponentMapperTests/DynComponentPropertyOnDynamicCompoTests.cs index cb4f8452b28..8d8fc4fcd50 100644 --- a/src/NHibernate.Test/MappingByCode/MappersTests/DynamicComponentMapperTests/DynComponentPropertyOnDynamicCompoTests.cs +++ b/src/NHibernate.Test/MappingByCode/MappersTests/DynamicComponentMapperTests/DynComponentPropertyOnDynamicCompoTests.cs @@ -1,4 +1,5 @@ using System.Collections; +using System.Collections.Generic; using System.Linq; using NHibernate.Cfg.MappingSchema; using NHibernate.Mapping.ByCode; @@ -23,6 +24,19 @@ public IDictionary Info } } + private class PersonWithGenericInfo + { + public int Id { get; set; } +#pragma warning disable CS0649 // Field is never assigned to, and will always have its default value + private IDictionary info; +#pragma warning restore CS0649 // Field is never assigned to, and will always have its default value + public IDictionary Info + { + get { return info; } + } + } + + [Test] public void WhenAddThenHas() { @@ -36,6 +50,19 @@ public void WhenAddThenHas() Assert.That(component.Properties.Select(x => x.Name), Is.EquivalentTo(new[] { "Info" })); } + [Test] + public void WhenAddThenHasGeneric() + { + var mapdoc = new HbmMapping(); + var component = new HbmDynamicComponent(); + var mapper = new DynamicComponentMapper(component, For.Property(p => p.Info), mapdoc); + var propertyInfo = For.Property(p => p.Info);//just as another dyn-compo + + mapper.Component(propertyInfo, (IDynamicComponentMapper x) => { }); + + Assert.That(component.Properties.Select(x => x.Name), Is.EquivalentTo(new[] { "Info" })); + } + [Test] public void WhenCustomizeThenCallCustomizer() { @@ -50,6 +77,20 @@ public void WhenCustomizeThenCallCustomizer() Assert.That(called, Is.True); } + [Test] + public void WhenCustomizeThenCallCustomizerGeneric() + { + var mapdoc = new HbmMapping(); + var component = new HbmDynamicComponent(); + var mapper = new DynamicComponentMapper(component, For.Property(p => p.Info), mapdoc); + var propertyInfo = For.Property(p => p.Info);//just as another dyn-compo + + var called = false; + mapper.Component(propertyInfo, (IDynamicComponentMapper x) => called = true); + + Assert.That(called, Is.True); + } + [Test] public void WhenCustomizeAccessorThenIgnore() { @@ -62,5 +103,18 @@ public void WhenCustomizeAccessorThenIgnore() Assert.That(component.Properties.OfType().Single().Access, Is.Null.Or.Empty); } + + [Test] + public void WhenCustomizeAccessorThenIgnoreGeneric() + { + var mapdoc = new HbmMapping(); + var component = new HbmDynamicComponent(); + var mapper = new DynamicComponentMapper(component, For.Property(p => p.Info), mapdoc); + var propertyInfo = For.Property(p => p.Info);//just as another dyn-compo + + mapper.Component(propertyInfo, (IDynamicComponentMapper x) => x.Access(Accessor.Field)); + + Assert.That(component.Properties.OfType().Single().Access, Is.Null.Or.Empty); + } } } diff --git a/src/NHibernate.Test/NHSpecificTest/NH1039Generic/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/NH1039Generic/Fixture.cs new file mode 100644 index 00000000000..5322334e3ec --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH1039Generic/Fixture.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections.Generic; +using NUnit.Framework; + + +namespace NHibernate.Test.NHSpecificTest.NH1039Generic +{ + [TestFixture] + public class Fixture : BugTestCase + { + public override string BugNumber + { + get { return "NH1039Generic"; } + } + + protected override void OnTearDown() + { + base.OnTearDown(); + using (ISession s = OpenSession()) + using (ITransaction tx = s.BeginTransaction()) + { + s.Delete("from Person"); + tx.Commit(); + } + } + + [Test] + public void test() + { + using (ISession s = OpenSession()) + using (ITransaction tx = s.BeginTransaction()) + { + Person person = new Person("1"); + person.Name = "John Doe"; + var set = new HashSet(); + set.Add("555-1234"); + set.Add("555-4321"); + person.Properties.Add("Phones", set); + + s.Save(person); + tx.Commit(); + } + using (ISession s = OpenSession()) + using (ITransaction tx = s.BeginTransaction()) + { + Person person = (Person)s.CreateCriteria(typeof(Person)).UniqueResult(); + + Assert.AreEqual("1", person.ID); + Assert.AreEqual("John Doe", person.Name); + Assert.AreEqual(1, person.Properties.Count); + Assert.That(person.Properties["Phones"], Is.InstanceOf>()); + Assert.IsTrue(((ISet) person.Properties["Phones"]).Contains("555-1234")); + Assert.IsTrue(((ISet) person.Properties["Phones"]).Contains("555-4321")); + } + } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH1039Generic/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH1039Generic/Mappings.hbm.xml new file mode 100644 index 00000000000..5f6c057add1 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH1039Generic/Mappings.hbm.xml @@ -0,0 +1,21 @@ + + + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/NHSpecificTest/NH1039Generic/Model.cs b/src/NHibernate.Test/NHSpecificTest/NH1039Generic/Model.cs new file mode 100644 index 00000000000..44f1d986396 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH1039Generic/Model.cs @@ -0,0 +1,35 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Text; + +namespace NHibernate.Test.NHSpecificTest.NH1039Generic +{ + public class Person + { + public Person() { } + public Person(string id) { this._ID = id; } + + private string _ID; + public virtual string ID + { + get { return _ID; } + set { _ID = value; } + } + + private string _Name; + public virtual string Name + { + get { return _Name; } + set { _Name = value; } + } + + private IDictionary _Properties = new Dictionary(); + public virtual IDictionary Properties + { + get { return _Properties; } + set { _Properties = value; } + } + } + +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH1796Generic/Entity.cs b/src/NHibernate.Test/NHSpecificTest/NH1796Generic/Entity.cs new file mode 100644 index 00000000000..36b8bf31c54 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH1796Generic/Entity.cs @@ -0,0 +1,12 @@ +using System.Collections; +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.NH1796Generic +{ + public class Entity + { + public virtual int Id { get; set; } + public virtual string Name { get; set; } + public virtual IDictionary DynProps { get; set; } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH1796Generic/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/NH1796Generic/Fixture.cs new file mode 100644 index 00000000000..99d927d9cc3 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH1796Generic/Fixture.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using NUnit.Framework; + +namespace NHibernate.Test.NHSpecificTest.NH1796Generic +{ + [TestFixture] + public class Fixture: BugTestCase + { + [Test] + public void Merge() + { + var entity = new Entity { Name = "Vinnie Luther" }; + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + s.Save(entity); + t.Commit(); + } + + entity.DynProps = new Dictionary(); + entity.DynProps["StrProp"] = "Modified"; + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + s.Merge(entity); + t.Commit(); + } + + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + s.CreateQuery("delete from Entity").ExecuteUpdate(); + t.Commit(); + } + } + + [Test] + public void SaveOrUpdate() + { + var entity = new Entity { Name = "Vinnie Luther" }; + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + s.SaveOrUpdate(entity); + t.Commit(); + } + + entity.DynProps = new Dictionary(); + entity.DynProps["StrProp"] = "Modified"; + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + s.SaveOrUpdate(entity); + t.Commit(); + } + + using (ISession s = OpenSession()) + using (ITransaction t = s.BeginTransaction()) + { + s.CreateQuery("delete from Entity").ExecuteUpdate(); + t.Commit(); + } + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH1796Generic/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH1796Generic/Mappings.hbm.xml new file mode 100644 index 00000000000..dcdbc19a554 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH1796Generic/Mappings.hbm.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/NHSpecificTest/NH2664Generic/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/NH2664Generic/Fixture.cs new file mode 100644 index 00000000000..0a51915da76 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH2664Generic/Fixture.cs @@ -0,0 +1,134 @@ +using System.Collections; +using System.Linq; +using NHibernate.Linq; +using NUnit.Framework; +using System.Linq.Expressions; +using System; + +namespace NHibernate.Test.NHSpecificTest.NH2664Generic +{ + [TestFixture] + public class Fixture : TestCase + { + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override IList Mappings => new[] {"NHSpecificTest.NH2664Generic.Mappings.hbm.xml"}; + + /// + /// push some data into the database + /// Really functions as a save test also + /// + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var tran = session.BeginTransaction()) + { + session.Save( + new Product + { + ProductId = "1", + Properties = + { + ["Name"] = "First Product", + ["Description"] = "First Description" + } + }); + + session.Save( + new Product + { + ProductId = "2", + Properties = + { + ["Name"] = "Second Product", + ["Description"] = "Second Description" + } + }); + + session.Save( + new Product + { + ProductId = "3", + Properties = + { + ["Name"] = "val", + ["Description"] = "val" + } + }); + + tran.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var tran = session.BeginTransaction()) + { + session.CreateQuery("delete from Product").ExecuteUpdate(); + tran.Commit(); + } + } + + [Test] + public void Query_DynamicComponent() + { + using (var session = OpenSession()) + { + var product = ( + from p in session.Query() + where (string) p.Properties["Name"] == "First Product" + select p + ).Single(); + + Assert.That(product, Is.Not.Null); + Assert.That(product.Properties["Name"], Is.EqualTo("First Product")); + } + } + + [Test] + public void Multiple_Query_Does_Not_Cache() + { + using (var session = OpenSession()) + { + // Query by name + var product1 = ( + from p in session.Query() + where (string) p.Properties["Name"] == "First Product" + select p + ).Single(); + Assert.That(product1.ProductId, Is.EqualTo("1")); + + // Query by description (this test is to verify that the dictionary + // index isn't cached from the query above. + var product2 = ( + from p in session.Query() + where (string) p.Properties["Description"] == "Second Description" + select p + ).Single(); + Assert.That(product2.ProductId, Is.EqualTo("2")); + } + } + + [Test] + public void Different_Key_In_DynamicComponentDictionary_Returns_Different_Keys() + { + using (var session = OpenSession()) + { + Expression> key1 = () => + from a in session.Query() + where (string) a.Properties["Name"] == "val" + select a; + Expression> key2 = () => + from a in session.Query() + where (string) a.Properties["Description"] == "val" + select a; + + var nhKey1 = new NhLinqExpression(key1.Body, Sfi); + var nhKey2 = new NhLinqExpression(key2.Body, Sfi); + + Assert.That(nhKey2.Key, Is.Not.EqualTo(nhKey1.Key)); + } + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH2664Generic/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH2664Generic/Mappings.hbm.xml new file mode 100644 index 00000000000..e49ef76e13f --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH2664Generic/Mappings.hbm.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH2664Generic/Product.cs b/src/NHibernate.Test/NHSpecificTest/NH2664Generic/Product.cs new file mode 100644 index 00000000000..aa972b932ab --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH2664Generic/Product.cs @@ -0,0 +1,24 @@ +using System.Collections; +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.NH2664Generic +{ + public class Product + { + public virtual string ProductId { get; set; } + + private IDictionary _properties; + + public virtual IDictionary Properties + { + get + { + if (_properties == null) + _properties = new Dictionary(); + + return _properties; + } + set { _properties = value; } + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3571Generic/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/NH3571Generic/Fixture.cs new file mode 100644 index 00000000000..1a3a396287d --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3571Generic/Fixture.cs @@ -0,0 +1,122 @@ +using System.Collections; +using System.Linq; +using NHibernate.Linq; +using NUnit.Framework; +using System.Linq.Expressions; +using System; + +namespace NHibernate.Test.NHSpecificTest.NH3571Generic +{ + [TestFixture] + public class Fixture : TestCase + { + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override IList Mappings => new[] {"NHSpecificTest.NH3571Generic.Mappings.hbm.xml"}; + + /// + /// push some data into the database + /// Really functions as a save test also + /// + protected override void OnSetUp() + { + using (var session = OpenSession()) + using (var tran = session.BeginTransaction()) + { + var product = new Product {ProductId = "1"}; + product.Details.Properties["Name"] = "First Product"; + product.Details.Properties["Description"] = "First Description"; + + session.Save(product); + + product = new Product {ProductId = "2"}; + product.Details.Properties["Name"] = "Second Product"; + product.Details.Properties["Description"] = "Second Description"; + + session.Save(product); + + product = new Product {ProductId = "3"}; + product.Details.Properties["Name"] = "val"; + product.Details.Properties["Description"] = "val"; + + session.Save(product); + + tran.Commit(); + } + } + + protected override void OnTearDown() + { + using (var session = OpenSession()) + using (var tran = session.BeginTransaction()) + { + session.CreateQuery("delete from Product").ExecuteUpdate(); + tran.Commit(); + } + } + + [Test] + public void CanQueryDynamicComponentInComponent() + { + using (var session = OpenSession()) + { + var product = ( + from p in session.Query() + where (string) p.Details.Properties["Name"] == "First Product" + select p + ).Single(); + + Assert.That(product, Is.Not.Null); + Assert.That(product.Details.Properties["Name"], Is.EqualTo("First Product")); + } + } + + [Test] + public void MultipleQueriesShouldNotCache() + { + using (var session = OpenSession()) + { + // Query by name + var product1 = ( + from p in session.Query() + where (string) p.Details.Properties["Name"] == "First Product" + select p + ).Single(); + Assert.That(product1.ProductId, Is.EqualTo("1")); + + // Query by description (this test is to verify that the dictionary + // index isn't cached from the query above. + var product2 = ( + from p in session.Query() + where (string) p.Details.Properties["Description"] == "Second Description" + select p + ).Single(); + Assert.That(product2.ProductId, Is.EqualTo("2")); + } + } + + [Test] + public void DifferentKeyInDynamicComponentDictionaryReturnsDifferentExpressionKeys() + { + using (var session = OpenSession()) + { +// ReSharper disable AccessToDisposedClosure Ok since the expressions aren't actually used after the using block. + Expression> key1 = () => ( + from a in session.Query() + where (string) a.Details.Properties["Name"] == "val" + select a + ); + Expression> key2 = () => ( + from a in session.Query() + where (string) a.Details.Properties["Description"] == "val" + select a); +// ReSharper restore AccessToDisposedClosure + + var nhKey1 = new NhLinqExpression(key1.Body, Sfi); + var nhKey2 = new NhLinqExpression(key2.Body, Sfi); + + Assert.That(nhKey2.Key, Is.Not.EqualTo(nhKey1.Key)); + } + } + } +} \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3571Generic/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH3571Generic/Mappings.hbm.xml new file mode 100644 index 00000000000..dcd0da1877e --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3571Generic/Mappings.hbm.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/NHibernate.Test/NHSpecificTest/NH3571Generic/Product.cs b/src/NHibernate.Test/NHSpecificTest/NH3571Generic/Product.cs new file mode 100644 index 00000000000..c70990576a6 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH3571Generic/Product.cs @@ -0,0 +1,43 @@ +using System.Collections; +using System.Collections.Generic; + +namespace NHibernate.Test.NHSpecificTest.NH3571Generic +{ + public class Product + { + public Product() + { + Details = new ProductDetails(); + } + public virtual string ProductId { get; set; } + + public virtual ProductDetails Details + { + get; + set; + } + + public virtual IList DetailsList + { + get; + set; + } + } + + public class ProductDetails + { + private IDictionary _properties; + + public virtual IDictionary Properties + { + get + { + if (_properties == null) + _properties = new Dictionary(); + + return _properties; + } + set { _properties = value; } + } + } +} \ No newline at end of file diff --git a/src/NHibernate/Linq/Visitors/VisitorUtil.cs b/src/NHibernate/Linq/Visitors/VisitorUtil.cs index 6175dcf374b..c4fa337d572 100644 --- a/src/NHibernate/Linq/Visitors/VisitorUtil.cs +++ b/src/NHibernate/Linq/Visitors/VisitorUtil.cs @@ -17,7 +17,7 @@ public static bool IsDynamicComponentDictionaryGetter(MethodInfo method, Express // A dynamic component must be an IDictionary with a string key. - if (method.Name != "get_Item" || !typeof(IDictionary).IsAssignableFrom(targetObject.Type)) + if (method.Name != "get_Item" || !typeof(IDictionary).IsAssignableFrom(targetObject.Type) && !typeof(IDictionary).IsAssignableFrom(targetObject.Type)) return false; var key = arguments.First() as ConstantExpression; diff --git a/src/NHibernate/Mapping/ByCode/IPlainPropertyContainerMapper.cs b/src/NHibernate/Mapping/ByCode/IPlainPropertyContainerMapper.cs index 115eae3ef56..582f77ab79a 100644 --- a/src/NHibernate/Mapping/ByCode/IPlainPropertyContainerMapper.cs +++ b/src/NHibernate/Mapping/ByCode/IPlainPropertyContainerMapper.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; @@ -40,6 +41,7 @@ public interface IBasePlainPropertyContainerMapper : IMinimalPlainPr void Component(Expression> property, Action> mapping); void Component(Expression> property); void Component(Expression> property, TComponent dynamicComponentTemplate, Action> mapping); + void Component(Expression>> property, TComponent dynamicComponentTemplate, Action> mapping) where TComponent : class; void Component(string notVisiblePropertyOrFieldName, Action> mapping); void Component(string notVisiblePropertyOrFieldName); diff --git a/src/NHibernate/Mapping/ByCode/Impl/CustomizersImpl/JoinCustomizer.cs b/src/NHibernate/Mapping/ByCode/Impl/CustomizersImpl/JoinCustomizer.cs index f7dbf87d8a1..530890e04f0 100644 --- a/src/NHibernate/Mapping/ByCode/Impl/CustomizersImpl/JoinCustomizer.cs +++ b/src/NHibernate/Mapping/ByCode/Impl/CustomizersImpl/JoinCustomizer.cs @@ -132,8 +132,14 @@ protected override void RegisterComponentMapping(Expression(Expression> property - , Action> mapping) + protected override void RegisterDynamicComponentMapping(Expression> property, Action> mapping) + { + MemberInfo member = TypeExtensions.DecodeMemberAccessExpression(property); + ExplicitDeclarationsHolder.AddAsPropertySplit(new SplitDefinition(typeof(TEntity), splitGroupId, member)); + base.RegisterDynamicComponentMapping(property, mapping); + } + + protected override void RegisterDynamicComponentMapping(Expression>> property, Action> mapping) { MemberInfo member = TypeExtensions.DecodeMemberAccessExpression(property); ExplicitDeclarationsHolder.AddAsPropertySplit(new SplitDefinition(typeof(TEntity), splitGroupId, member)); @@ -161,4 +167,4 @@ protected override void RegisterIdBagMapping(Expression(Expression> propert RegisterDynamicComponentMapping(property, mapping); } + public void Component(Expression>> property, TComponent dynamicComponentTemplate, Action> mapping) where TComponent : class + { + RegisterDynamicComponentMapping(property, mapping); + } + protected virtual void RegisterDynamicComponentMapping(Expression> property, Action> mapping) { MemberInfo member = TypeExtensions.DecodeMemberAccessExpression(property); @@ -105,6 +111,13 @@ protected virtual void RegisterDynamicComponentMapping(Expression(Expression>> property, Action> mapping) where TComponent : class + { + MemberInfo member = TypeExtensions.DecodeMemberAccessExpression(property); + MemberInfo memberOf = TypeExtensions.DecodeMemberAccessExpressionOf(property); + RegisterDynamicComponentMapping(mapping, member, memberOf); + } + protected void RegisterDynamicComponentMapping(Action> mapping, params MemberInfo[] members) { foreach (var member in members) diff --git a/src/NHibernate/Tuple/Component/ComponentMetamodel.cs b/src/NHibernate/Tuple/Component/ComponentMetamodel.cs index b42673c4cb9..274b9e90cb9 100644 --- a/src/NHibernate/Tuple/Component/ComponentMetamodel.cs +++ b/src/NHibernate/Tuple/Component/ComponentMetamodel.cs @@ -26,6 +26,7 @@ public ComponentMetamodel(Mapping.Component component) propertyIndexes[property.Name] = i; i++; } + EntityMode = component.HasPocoRepresentation ? EntityMode.Poco : EntityMode.Map; var componentTuplizerFactory = new ComponentTuplizerFactory(); diff --git a/src/NHibernate/Tuple/Component/DynamicComponentInstantiator.cs b/src/NHibernate/Tuple/Component/DynamicComponentInstantiator.cs new file mode 100644 index 00000000000..719a1afd8be --- /dev/null +++ b/src/NHibernate/Tuple/Component/DynamicComponentInstantiator.cs @@ -0,0 +1,15 @@ +using System; +using System.Collections.Generic; + +namespace NHibernate.Tuple.Component +{ + [Serializable] + internal class DynamicComponentInstantiator : IInstantiator + { + public object Instantiate(object id) => Instantiate(); + + public object Instantiate() => new Dictionary(); + + public bool IsInstance(object obj) => obj is Dictionary; + } +} \ No newline at end of file diff --git a/src/NHibernate/Tuple/Component/DynamicMapComponentTuplizer.cs b/src/NHibernate/Tuple/Component/DynamicMapComponentTuplizer.cs index 681b0128cf3..7332a7f861d 100644 --- a/src/NHibernate/Tuple/Component/DynamicMapComponentTuplizer.cs +++ b/src/NHibernate/Tuple/Component/DynamicMapComponentTuplizer.cs @@ -1,5 +1,7 @@ using System; using NHibernate.Properties; +using System.Collections; +using System.Collections.Generic; namespace NHibernate.Tuple.Component { @@ -16,30 +18,16 @@ public DynamicMapComponentTuplizer(Mapping.Component component) instantiator = BuildInstantiator(component); } - public override System.Type MappedClass - { - get { return typeof(System.Collections.IDictionary); } - } - - protected internal override IInstantiator BuildInstantiator(Mapping.Component component) - { - return new DynamicMapInstantiator(); - } - - protected internal override IGetter BuildGetter(Mapping.Component component, Mapping.Property prop) - { - return BuildPropertyAccessor(prop).GetGetter(null, prop.Name); - } + public override System.Type MappedClass => + typeof(Dictionary); - protected internal override ISetter BuildSetter(Mapping.Component component, Mapping.Property prop) - { - return BuildPropertyAccessor(prop).GetSetter(null, prop.Name); - } + protected internal override IInstantiator BuildInstantiator(Mapping.Component component) => + new DynamicComponentInstantiator(); - private IPropertyAccessor BuildPropertyAccessor(Mapping.Property property) - { - return PropertyAccessorFactory.DynamicMapPropertyAccessor; - } + protected internal override IGetter BuildGetter(Mapping.Component component, Mapping.Property prop) => + PropertyAccessorFactory.DynamicMapPropertyAccessor.GetGetter(null, prop.Name); + protected internal override ISetter BuildSetter(Mapping.Component component, Mapping.Property prop) => + PropertyAccessorFactory.DynamicMapPropertyAccessor.GetSetter(null, prop.Name); } } diff --git a/src/NHibernate/Tuple/DynamicMapInstantiator.cs b/src/NHibernate/Tuple/DynamicMapInstantiator.cs index 0511ea3c7e6..0a8cce0e4a9 100644 --- a/src/NHibernate/Tuple/DynamicMapInstantiator.cs +++ b/src/NHibernate/Tuple/DynamicMapInstantiator.cs @@ -13,6 +13,8 @@ public class DynamicMapInstantiator : IInstantiator private readonly string entityName; private readonly HashSet isInstanceEntityNames = new HashSet(); + //Since v5.2 + [Obsolete("This constructor is not used and will be removed in a future version.")] public DynamicMapInstantiator() { entityName = null; From 90e75b947eb42c5f722673860744eee0607dfcf7 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, 3 Jun 2018 20:52:15 +0200 Subject: [PATCH 07/17] Generate a correct proxy for interfaces Static proxies generated for interfaces were incorrectly attempting to call the base "interface implementation". And they were all sharing the same type name. --- .../StaticProxyFactoryFixture.cs | 28 ++++++++ .../Proxy/NHibernateProxyBuilder.cs | 70 +++++++++++++------ 2 files changed, 78 insertions(+), 20 deletions(-) diff --git a/src/NHibernate.Test/StaticProxyTest/StaticProxyFactoryFixture.cs b/src/NHibernate.Test/StaticProxyTest/StaticProxyFactoryFixture.cs index 4dae9a5b8a2..ad2c4a3f0ab 100644 --- a/src/NHibernate.Test/StaticProxyTest/StaticProxyFactoryFixture.cs +++ b/src/NHibernate.Test/StaticProxyTest/StaticProxyFactoryFixture.cs @@ -107,6 +107,34 @@ public void VerifyProxyForClassWithInternalInterface() #endif } + [Test] + public void VerifyProxyForClassWithAdditionalInterface() + { + var factory = new StaticProxyFactory(); + factory.PostInstantiate( + typeof(PublicInterfaceTestClass).FullName, + typeof(PublicInterfaceTestClass), + // By way of the "proxy" attribute on the "class" mapping, an interface to use for the + // lazy entity load proxy instead of the persistentClass can be specified. This is "translated" into + // having an additional interface in the interface list, instead of just having INHibernateProxy. + // (Quite a loosy semantic...) + new HashSet {typeof(INHibernateProxy), typeof(IPublic)}, + null, null, null); + +#if NETFX + VerifyGeneratedAssembly( + () => + { +#endif + var proxy = factory.GetProxy(1, null); + Assert.That(proxy, Is.Not.Null); + Assert.That(proxy, Is.InstanceOf()); + Assert.That(proxy, Is.Not.InstanceOf()); +#if NETFX + }); +#endif + } + [Test] public void CanSerializeFieldInterceptorProxy() { diff --git a/src/NHibernate/Proxy/NHibernateProxyBuilder.cs b/src/NHibernate/Proxy/NHibernateProxyBuilder.cs index daa15daafed..64b3e4f66a2 100644 --- a/src/NHibernate/Proxy/NHibernateProxyBuilder.cs +++ b/src/NHibernate/Proxy/NHibernateProxyBuilder.cs @@ -36,7 +36,16 @@ public NHibernateProxyBuilder(MethodInfo getIdentifierMethod, MethodInfo setIden public TypeInfo CreateProxyType(System.Type baseType, IReadOnlyCollection baseInterfaces) { - var typeName = $"{baseType.Name}Proxy"; + System.Type interfaceType = null; + if (baseType == typeof(object)) + { + // Mapping option "proxy" allows to ask for using an interface, which switches the base type to object + // and adds the interface to base interfaces set. + // Avoids using object for naming the proxy, as otherwise all entities using the "proxy" option for + // specifying an interface would have their proxies sharing the same full name. + interfaceType = baseInterfaces.FirstOrDefault(i => i != typeof(INHibernateProxy)); + } + var typeName = $"{(interfaceType ?? baseType).Name}Proxy"; var assemblyName = $"{typeName}Assembly"; var moduleName = $"{typeName}Module"; @@ -77,7 +86,7 @@ public TypeInfo CreateProxyType(System.Type baseType, IReadOnlyCollection(value); @@ -258,7 +270,7 @@ private static void ImplementSetIdentifier(TypeBuilder typeBuilder, MethodInfo m var methodOverride = ProxyBuilderHelper.GenerateMethodSignature(method.Name, method, typeBuilder); var IL = methodOverride.GetILGenerator(); - EmitCallBaseIfLazyInitializerIsNull(IL, method, lazyInitializerField); + EmitCallBaseIfLazyInitializerIsNull(IL, method, lazyInitializerField, parentType); IL.Emit(OpCodes.Ldarg_0); IL.Emit(OpCodes.Ldfld, lazyInitializerField); @@ -278,7 +290,8 @@ private static void ImplementSetIdentifier(TypeBuilder typeBuilder, MethodInfo m typeBuilder.DefineMethodOverride(methodOverride, method); } - private static void ImplementCallMethodOnEmbeddedComponentId(TypeBuilder typeBuilder, MethodInfo method, FieldInfo lazyInitializerField) + private static void ImplementCallMethodOnEmbeddedComponentId( + TypeBuilder typeBuilder, MethodInfo method, FieldInfo lazyInitializerField, System.Type parentType) { /* if (this.__lazyInitializer == null) @@ -289,7 +302,7 @@ private static void ImplementCallMethodOnEmbeddedComponentId(TypeBuilder typeBui var IL = methodOverride.GetILGenerator(); - EmitCallBaseIfLazyInitializerIsNull(IL, method, lazyInitializerField); + EmitCallBaseIfLazyInitializerIsNull(IL, method, lazyInitializerField, parentType); IL.Emit(OpCodes.Ldarg_0); IL.Emit(OpCodes.Ldfld, lazyInitializerField); @@ -301,18 +314,19 @@ private static void ImplementCallMethodOnEmbeddedComponentId(TypeBuilder typeBui typeBuilder.DefineMethodOverride(methodOverride, method); } - private static void ImplementCallMethodOnImplementation(TypeBuilder typeBuilder, MethodInfo method, FieldInfo lazyInitializerField) + private static void ImplementCallMethodOnImplementation( + TypeBuilder typeBuilder, MethodInfo method, FieldInfo lazyInitializerField, System.Type parentType) { /* if (this.__lazyInitializer == null) return base.(args..); - return this.__lazyInitializer.GetImplementation().(args..) + return this.__lazyInitializer.GetImplementation().(args..) */ var methodOverride = ProxyBuilderHelper.GenerateMethodSignature(method.Name, method, typeBuilder); var IL = methodOverride.GetILGenerator(); - EmitCallBaseIfLazyInitializerIsNull(IL, method, lazyInitializerField); + EmitCallBaseIfLazyInitializerIsNull(IL, method, lazyInitializerField, parentType); EmitCallImplementation(IL, method, lazyInitializerField); IL.Emit(OpCodes.Ret); @@ -320,10 +334,26 @@ private static void ImplementCallMethodOnImplementation(TypeBuilder typeBuilder, typeBuilder.DefineMethodOverride(methodOverride, method); } - private static void EmitCallBaseIfLazyInitializerIsNull(ILGenerator IL, MethodInfo method, FieldInfo lazyInitializerField) + private static void EmitCallBaseIfLazyInitializerIsNull( + ILGenerator IL, MethodInfo method, FieldInfo lazyInitializerField, System.Type parentType) { - //if (this.__lazyInitializer == null) - // return base.(args..) + /* + + if (this.__lazyInitializer == null) + return base.(args..) + <}> + */ + if (!method.DeclaringType.IsAssignableFrom(parentType)) + // The proxy does not derive from a type implementing the method, do not attempt + // calling its base. In such case, the lazy initializer is never null. + return; + + // When deriving from the entity class, the entity class constructor may trigger + // virtual calls accessing the proxy state before its own constructor has a chance + // to initialize it. So although lazyInitializer is never supplied as null to the + // proxy constructor, we must guard nonetheless against it being null during base + // constructor call. IL.Emit(OpCodes.Ldarg_0); IL.Emit(OpCodes.Ldfld, lazyInitializerField); From 5e81ece0192bea0e2d27d528fe91d5fab1681e41 Mon Sep 17 00:00:00 2001 From: Alexander Zaytsev Date: Fri, 8 Jun 2018 10:58:51 +1200 Subject: [PATCH 08/17] Remove a binary breaking change introduced in #305 --- .../ByCode/IPlainPropertyContainerMapper.cs | 20 +++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/NHibernate/Mapping/ByCode/IPlainPropertyContainerMapper.cs b/src/NHibernate/Mapping/ByCode/IPlainPropertyContainerMapper.cs index 582f77ab79a..9441c73676f 100644 --- a/src/NHibernate/Mapping/ByCode/IPlainPropertyContainerMapper.cs +++ b/src/NHibernate/Mapping/ByCode/IPlainPropertyContainerMapper.cs @@ -3,6 +3,8 @@ using System.Collections.Generic; using System.Linq.Expressions; using System.Reflection; +using NHibernate.Mapping.ByCode.Impl.CustomizersImpl; +using NHibernate.Util; namespace NHibernate.Mapping.ByCode { @@ -41,7 +43,6 @@ public interface IBasePlainPropertyContainerMapper : IMinimalPlainPr void Component(Expression> property, Action> mapping); void Component(Expression> property); void Component(Expression> property, TComponent dynamicComponentTemplate, Action> mapping); - void Component(Expression>> property, TComponent dynamicComponentTemplate, Action> mapping) where TComponent : class; void Component(string notVisiblePropertyOrFieldName, Action> mapping); void Component(string notVisiblePropertyOrFieldName); @@ -56,4 +57,19 @@ public interface IPlainPropertyContainerMapper : IBasePlainPropertyC void OneToOne(Expression> property, Action> mapping) where TProperty : class; void OneToOne(string notVisiblePropertyOrFieldName, Action> mapping) where TProperty : class; } -} \ No newline at end of file + + public static class BasePlainPropertyContainerMapperExtensions + { + //6.0 TODO: Merge into IBasePlainPropertyContainerMapper<> interface + public static void Component( + this IBasePlainPropertyContainerMapper mapper, + Expression>> property, + TComponent dynamicComponentTemplate, + Action> mapping) where TComponent : class + { + var customizer = ReflectHelper.CastOrThrow>( + mapper, "mapping a generic dictionary as a dynamic component"); + customizer.Component(property, dynamicComponentTemplate, mapping); + } + } +} From 39836c290574b5eefb2ea17ca79b6d523c822522 Mon Sep 17 00:00:00 2001 From: Roman Artiukhin Date: Sat, 9 Jun 2018 11:42:24 +0300 Subject: [PATCH 09/17] Upgrade to AsyncGenerator 0.8.2.7 --- Tools/packages.config | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Tools/packages.config b/Tools/packages.config index 1c60b906681..367755e5532 100644 --- a/Tools/packages.config +++ b/Tools/packages.config @@ -7,7 +7,7 @@ - + From efbb08aafe78f36c666b9f3bbec6e002ba3566c2 Mon Sep 17 00:00:00 2001 From: Brenden Weel Date: Mon, 30 Sep 2013 09:46:50 -0500 Subject: [PATCH 10/17] NH-3543 - Add support for multi query to DB2 Driver Fixes #874 --- src/NHibernate/Driver/DB2Driver.cs | 24 +++++++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/src/NHibernate/Driver/DB2Driver.cs b/src/NHibernate/Driver/DB2Driver.cs index 93c1b26bd1f..8fd015678d7 100644 --- a/src/NHibernate/Driver/DB2Driver.cs +++ b/src/NHibernate/Driver/DB2Driver.cs @@ -1,4 +1,5 @@ using System; +using NHibernate.Engine; namespace NHibernate.Driver { @@ -40,5 +41,26 @@ public override bool SupportsMultipleOpenReaders { get { return false; } } + + /// + /// Gets a value indicating whether the driver [supports multiple queries]. + /// + /// + /// true if [supports multiple queries]; otherwise, false. + /// + public override bool SupportsMultipleQueries + { + get { return true; } + } + + /// + /// Gets the result sets command. + /// + /// The implementor of the session. + /// + public override IResultSetsCommand GetResultSetsCommand(ISessionImplementor session) + { + return new BasicResultSetsCommand(session); + } } -} \ No newline at end of file +} From 059cabf5ff440585ce7d93e80602960cb61a3e2f Mon Sep 17 00:00:00 2001 From: Alexander Zaytsev Date: Sun, 10 Jun 2018 01:37:38 +1200 Subject: [PATCH 11/17] Fix DbType.Binary registration in DB2Dialect --- src/NHibernate/Dialect/DB2Dialect.cs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/NHibernate/Dialect/DB2Dialect.cs b/src/NHibernate/Dialect/DB2Dialect.cs index 6186ef6d81c..89f6c305fdc 100644 --- a/src/NHibernate/Dialect/DB2Dialect.cs +++ b/src/NHibernate/Dialect/DB2Dialect.cs @@ -32,6 +32,7 @@ public DB2Dialect() RegisterColumnType(DbType.AnsiString, "VARCHAR(254)"); RegisterColumnType(DbType.AnsiString, 8000, "VARCHAR($l)"); RegisterColumnType(DbType.AnsiString, 2147483647, "CLOB"); + RegisterColumnType(DbType.Binary, "BLOB"); RegisterColumnType(DbType.Binary, 2147483647, "BLOB"); RegisterColumnType(DbType.Boolean, "SMALLINT"); RegisterColumnType(DbType.Byte, "SMALLINT"); From 411cfd9597cda564cf8f49e475715d94834c40da Mon Sep 17 00:00:00 2001 From: Alexander Zaytsev Date: Sun, 10 Jun 2018 18:49:28 +1200 Subject: [PATCH 12/17] Add new DB2CoreDriver to use with IBM.Data.DB2.Core provider --- src/NHibernate/Driver/DB2CoreDriver.cs | 12 ++++++ src/NHibernate/Driver/DB2Driver.cs | 58 +------------------------- src/NHibernate/Driver/DB2DriverBase.cs | 45 ++++++++++++++++++++ 3 files changed, 59 insertions(+), 56 deletions(-) create mode 100644 src/NHibernate/Driver/DB2CoreDriver.cs create mode 100644 src/NHibernate/Driver/DB2DriverBase.cs diff --git a/src/NHibernate/Driver/DB2CoreDriver.cs b/src/NHibernate/Driver/DB2CoreDriver.cs new file mode 100644 index 00000000000..85c25a76641 --- /dev/null +++ b/src/NHibernate/Driver/DB2CoreDriver.cs @@ -0,0 +1,12 @@ +namespace NHibernate.Driver +{ + /// + /// A NHibernate Driver for using the IBM.Data.DB2.Core DataProvider. + /// + public class DB2CoreDriver : DB2DriverBase + { + public DB2CoreDriver() : base("IBM.Data.DB2.Core") + { + } + } +} diff --git a/src/NHibernate/Driver/DB2Driver.cs b/src/NHibernate/Driver/DB2Driver.cs index 8fd015678d7..aec89f96f1f 100644 --- a/src/NHibernate/Driver/DB2Driver.cs +++ b/src/NHibernate/Driver/DB2Driver.cs @@ -1,66 +1,12 @@ -using System; -using NHibernate.Engine; - namespace NHibernate.Driver { /// /// A NHibernate Driver for using the IBM.Data.DB2 DataProvider. /// - public class DB2Driver : ReflectionBasedDriver + public class DB2Driver : DB2DriverBase { - /// - /// Initializes a new instance of the class. - /// - /// - /// Thrown when the IBM.Data.DB2 assembly can not be loaded. - /// - public DB2Driver() : base( - "IBM.Data.DB2", - "IBM.Data.DB2", - "IBM.Data.DB2.DB2Connection", - "IBM.Data.DB2.DB2Command") + public DB2Driver() : base("IBM.Data.DB2") { } - - public override bool UseNamedPrefixInSql - { - get { return false; } - } - - public override bool UseNamedPrefixInParameter - { - get { return false; } - } - - public override string NamedPrefix - { - get { return String.Empty; } - } - - public override bool SupportsMultipleOpenReaders - { - get { return false; } - } - - /// - /// Gets a value indicating whether the driver [supports multiple queries]. - /// - /// - /// true if [supports multiple queries]; otherwise, false. - /// - public override bool SupportsMultipleQueries - { - get { return true; } - } - - /// - /// Gets the result sets command. - /// - /// The implementor of the session. - /// - public override IResultSetsCommand GetResultSetsCommand(ISessionImplementor session) - { - return new BasicResultSetsCommand(session); - } } } diff --git a/src/NHibernate/Driver/DB2DriverBase.cs b/src/NHibernate/Driver/DB2DriverBase.cs new file mode 100644 index 00000000000..cc92fbfc2e2 --- /dev/null +++ b/src/NHibernate/Driver/DB2DriverBase.cs @@ -0,0 +1,45 @@ +using NHibernate.Engine; + +namespace NHibernate.Driver +{ + /// + /// A base for NHibernate Driver for using the IBM.Data.DB2 or IBM.Data.DB2.Core DataProvider. + /// + public abstract class DB2DriverBase : ReflectionBasedDriver + { + /// + /// + /// Thrown when the assemblyName assembly can not be loaded. + /// + protected DB2DriverBase(string assemblyName) : + base(assemblyName, assemblyName, assemblyName + ".DB2Connection", assemblyName + ".DB2Command") + { + } + + public override bool UseNamedPrefixInSql => false; + + public override bool UseNamedPrefixInParameter => false; + + public override string NamedPrefix => string.Empty; + + public override bool SupportsMultipleOpenReaders => false; + + /// + /// Gets a value indicating whether the driver [supports multiple queries]. + /// + /// + /// true if [supports multiple queries]; otherwise, false. + /// + public override bool SupportsMultipleQueries => true; + + /// + /// Gets the result sets command. + /// + /// The implementor of the session. + /// + public override IResultSetsCommand GetResultSetsCommand(ISessionImplementor session) + { + return new BasicResultSetsCommand(session); + } + } +} From 684587ae4bc175c39ff5b324c6c8f79db0502be9 Mon Sep 17 00:00:00 2001 From: Alexander Zaytsev Date: Mon, 11 Jun 2018 00:03:17 +1200 Subject: [PATCH 13/17] Fix documentation build --- doc/reference/modules/basic_mapping.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/doc/reference/modules/basic_mapping.xml b/doc/reference/modules/basic_mapping.xml index 923714dec99..1cc8e06fac1 100644 --- a/doc/reference/modules/basic_mapping.xml +++ b/doc/reference/modules/basic_mapping.xml @@ -2081,7 +2081,7 @@ The <dynamic-component> element allows an IDictionary - or IDictionaryto be mapped as a component, where the property + or IDictionary<string, object>to be mapped as a component, where the property names refer to keys of the dictionary. From 91cd6e201fa9c3d3102205281b3220c00f861396 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, 10 Jun 2018 14:24:10 +0200 Subject: [PATCH 14/17] Release 5.1.3 (#1716) Co-authored-by: Alexander Zaytsev --- appveyor.yml | 2 +- build-common/NHibernate.props | 2 +- build-common/common.xml | 4 ++-- releasenotes.txt | 17 +++++++++++++++++ 4 files changed, 21 insertions(+), 4 deletions(-) diff --git a/appveyor.yml b/appveyor.yml index 7132ba24ccd..c4f82989f1b 100644 --- a/appveyor.yml +++ b/appveyor.yml @@ -1,4 +1,4 @@ -version: 5.1.2.{build} +version: 5.1.3.{build} image: Visual Studio 2017 environment: matrix: diff --git a/build-common/NHibernate.props b/build-common/NHibernate.props index 344344dad1a..331fd0bda73 100644 --- a/build-common/NHibernate.props +++ b/build-common/NHibernate.props @@ -2,7 +2,7 @@ 5 1 - 2 + 3 $(VersionMajor).$(VersionMinor).$(VersionPatch) diff --git a/build-common/common.xml b/build-common/common.xml index 089bd9828b1..3cbf4c1bb88 100644 --- a/build-common/common.xml +++ b/build-common/common.xml @@ -13,8 +13,8 @@ - - + + diff --git a/releasenotes.txt b/releasenotes.txt index 2da4aeca3bd..96e55077acf 100644 --- a/releasenotes.txt +++ b/releasenotes.txt @@ -1,3 +1,20 @@ +Build 5.1.3 +============================= + +Release notes - NHibernate - Version 5.1.3 + +** Bug + + * #1741 Fix DbType.Binary registration in DB2Dialect + * #1732 Dictionary failure in Loader + * #1730 Query cache always missed in session having altered the entities + * #1711 Fix static proxy serialization + +** Task + + * #1716 Release 5.1.3 + + Build 5.1.2 ============================= From c966f347eda9fd7f3363a475505f0e01b859ec6c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericDelaporte@users.noreply.github.com> Date: Tue, 12 Jun 2018 12:52:39 +0200 Subject: [PATCH 15/17] Remove obsoleted hibernate configuration prefix The documentation was still prefixing some configuration properties with the "hibernate." prefix, which has been removed since NH 2.0.0.GA. --- doc/reference/modules/toolset_guide.xml | 12 ++++++------ src/NHibernate/CacheMode.cs | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/doc/reference/modules/toolset_guide.xml b/doc/reference/modules/toolset_guide.xml index 4a6a4b764cb..1e885781bcf 100644 --- a/doc/reference/modules/toolset_guide.xml +++ b/doc/reference/modules/toolset_guide.xml @@ -66,7 +66,7 @@ You must specify a SQL Dialect via the - hibernate.dialect property when using this tool. + dialect configuration property when using this tool. @@ -262,23 +262,23 @@ new SchemaExport(cfg).Create(false, true);]]> - hibernate.connection.driver_class + connection.driver_class jdbc driver class - hibernate.connection.url + connection.url jdbc url - hibernate.connection.username + connection.username database user - hibernate.connection.password + connection.password user password - hibernate.dialect + dialect dialect diff --git a/src/NHibernate/CacheMode.cs b/src/NHibernate/CacheMode.cs index 30e76531478..2dec004c1ce 100644 --- a/src/NHibernate/CacheMode.cs +++ b/src/NHibernate/CacheMode.cs @@ -33,9 +33,9 @@ public enum CacheMode /// /// The session will never read items from the cache, but will add items /// to the cache as it reads them from the database. In this mode, the - /// effect of hibernate.cache.use_minimal_puts is bypassed, in + /// effect of cache.use_minimal_puts is bypassed, in /// order to force a cache refresh /// Refresh = Put | 4 // NH: include Put but have a different value } -} \ No newline at end of file +} From 1024b71aec3b5e75c80e0ad6515943d6aee4adba Mon Sep 17 00:00:00 2001 From: Gunnar Liljas Date: Wed, 13 Jun 2018 11:59:00 +0200 Subject: [PATCH 16/17] Add support for query space synchronization (#1392) Fix redundant cache clearing --- src/AsyncGenerator.yml | 2 + .../NativeSQLBulkOperationsWithCache.cs | 72 ++++++++ .../SqlTest/Query/NativeSQLQueriesFixture.cs | 163 +++++++++++------- .../BulkOperationCleanupActionFixture.cs | 71 ++++++++ .../NativeSQLBulkOperationsWithCache.cs | 108 ++++++++++++ src/NHibernate.Test/MappingByCode/For.cs | 2 +- src/NHibernate.Test/NHibernate.Test.csproj | 2 +- .../SqlTest/Query/NativeSQLQueriesFixture.cs | 163 +++++++++++------- src/NHibernate.sln | 2 +- .../Action/BulkOperationCleanupAction.cs | 44 ++--- .../Action/BulkOperationCleanupAction.cs | 51 +++--- .../Async/Engine/Query/NativeSQLQueryPlan.cs | 15 +- .../ANTLR/Exec/AbstractStatementExecutor.cs | 17 +- .../Async/Hql/Ast/ANTLR/Exec/BasicExecutor.cs | 2 +- .../ANTLR/Exec/MultiTableDeleteExecutor.cs | 2 +- .../ANTLR/Exec/MultiTableUpdateExecutor.cs | 2 +- src/NHibernate/Async/ISessionFactory.cs | 82 +++++++++ .../Async/Impl/SessionFactoryImpl.cs | 66 +++++++ src/NHibernate/Async/Impl/SqlQueryImpl.cs | 2 +- .../Engine/Query/NativeSQLQueryPlan.cs | 2 - .../ANTLR/Exec/AbstractStatementExecutor.cs | 27 ++- src/NHibernate/ISQLQuery.cs | 59 +++++++ src/NHibernate/ISessionFactory.cs | 77 +++++++++ src/NHibernate/ISynchronizableQuery.cs | 34 ++++ src/NHibernate/Impl/AbstractQueryImpl.cs | 2 +- src/NHibernate/Impl/SessionFactoryImpl.cs | 39 +++++ src/NHibernate/Impl/SqlQueryImpl.cs | 41 ++++- 27 files changed, 934 insertions(+), 215 deletions(-) create mode 100644 src/NHibernate.Test/Async/BulkManipulation/NativeSQLBulkOperationsWithCache.cs create mode 100644 src/NHibernate.Test/BulkManipulation/BulkOperationCleanupActionFixture.cs create mode 100644 src/NHibernate.Test/BulkManipulation/NativeSQLBulkOperationsWithCache.cs create mode 100644 src/NHibernate/ISynchronizableQuery.cs diff --git a/src/AsyncGenerator.yml b/src/AsyncGenerator.yml index 7b8a269d01c..3c541dc7315 100644 --- a/src/AsyncGenerator.yml +++ b/src/AsyncGenerator.yml @@ -170,6 +170,8 @@ applyChanges: true analyzation: methodConversion: + - conversion: Copy + name: AfterTransactionCompletionProcess_EvictsFromCache - conversion: Copy hasAttributeName: OneTimeSetUpAttribute - conversion: Copy diff --git a/src/NHibernate.Test/Async/BulkManipulation/NativeSQLBulkOperationsWithCache.cs b/src/NHibernate.Test/Async/BulkManipulation/NativeSQLBulkOperationsWithCache.cs new file mode 100644 index 00000000000..4361dbefa5a --- /dev/null +++ b/src/NHibernate.Test/Async/BulkManipulation/NativeSQLBulkOperationsWithCache.cs @@ -0,0 +1,72 @@ +//------------------------------------------------------------------------------ +// +// 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; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cache; +using NHibernate.Cfg; +using NSubstitute; +using NUnit.Framework; +using Environment = NHibernate.Cfg.Environment; + +namespace NHibernate.Test.BulkManipulation +{ + using System.Threading.Tasks; + [TestFixture] + public class NativeSQLBulkOperationsWithCacheAsync : TestCase + { + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override IList Mappings => new[] { "BulkManipulation.Vehicle.hbm.xml" }; + + protected override void Configure(Configuration configuration) + { + cfg.SetProperty(Environment.UseQueryCache, "true"); + cfg.SetProperty(Environment.UseSecondLevelCache, "true"); + cfg.SetProperty(Environment.CacheProvider, typeof(SubstituteCacheProvider).AssemblyQualifiedName); + } + + [Test] + public async Task SimpleNativeSQLInsert_DoesNotEvictEntireCacheWhenQuerySpacesAreAddedAsync() + { + List clearCalls = new List(); + (Sfi.Settings.CacheProvider as SubstituteCacheProvider).OnClear(x => + { + clearCalls.Add(x); + }); + using (var s = OpenSession()) + { + string ssql = "UPDATE Vehicle SET Vin='123' WHERE Vin='123c'"; + + using (var t = s.BeginTransaction()) + { + + await (s.CreateSQLQuery(ssql).ExecuteUpdateAsync()); + await (t.CommitAsync()); + + Assert.AreEqual(1, clearCalls.Count); + } + + clearCalls.Clear(); + + using (var t = s.BeginTransaction()) + { + await (s.CreateSQLQuery(ssql).AddSynchronizedQuerySpace("Unknown").ExecuteUpdateAsync()); + await (t.CommitAsync()); + + Assert.AreEqual(0, clearCalls.Count); + } + } + } + } +} diff --git a/src/NHibernate.Test/Async/SqlTest/Query/NativeSQLQueriesFixture.cs b/src/NHibernate.Test/Async/SqlTest/Query/NativeSQLQueriesFixture.cs index 3a69352f4f8..4b3d76497da 100644 --- a/src/NHibernate.Test/Async/SqlTest/Query/NativeSQLQueriesFixture.cs +++ b/src/NHibernate.Test/Async/SqlTest/Query/NativeSQLQueriesFixture.cs @@ -54,7 +54,7 @@ public class GeneralTestAsync : TestCase protected override IList Mappings { - get { return new[] { "SqlTest.Query.NativeSQLQueries.hbm.xml" }; } + get { return new[] {"SqlTest.Query.NativeSQLQueries.hbm.xml"}; } } protected override string MappingsAssembly @@ -103,17 +103,17 @@ public async Task SQLQueryInterfaceAsync() await (s.SaveAsync(emp)); IList l = await (s.CreateSQLQuery(OrgEmpRegionSQL) - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .AddScalar("regionCode", NHibernateUtil.String) - .ListAsync()); + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .AddScalar("regionCode", NHibernateUtil.String) + .ListAsync()); Assert.AreEqual(2, l.Count); l = await (s.CreateSQLQuery(OrgEmpPersonSQL) - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .AddJoin("pers", "emp.employee") - .ListAsync()); + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .AddJoin("pers", "emp.employee") + .ListAsync()); Assert.AreEqual(l.Count, 1); await (t.CommitAsync()); @@ -122,13 +122,14 @@ public async Task SQLQueryInterfaceAsync() s = OpenSession(); t = s.BeginTransaction(); - l = await (s.CreateSQLQuery("select {org.*}, {emp.*} " + - "from ORGANIZATION org " + - " left outer join EMPLOYMENT emp on org.ORGID = emp.EMPLOYER, ORGANIZATION org2") - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .SetResultTransformer(new DistinctRootEntityResultTransformer()) - .ListAsync()); + l = await (s.CreateSQLQuery( + "select {org.*}, {emp.*} " + + "from ORGANIZATION org " + + " left outer join EMPLOYMENT emp on org.ORGID = emp.EMPLOYER, ORGANIZATION org2") + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .SetResultTransformer(new DistinctRootEntityResultTransformer()) + .ListAsync()); Assert.AreEqual(l.Count, 2); await (t.CommitAsync()); @@ -162,13 +163,13 @@ public async Task ResultSetMappingDefinitionAsync() await (s.SaveAsync(emp)); IList l = await (s.CreateSQLQuery(OrgEmpRegionSQL) - .SetResultSetMapping("org-emp-regionCode") - .ListAsync()); + .SetResultSetMapping("org-emp-regionCode") + .ListAsync()); Assert.AreEqual(l.Count, 2); l = await (s.CreateSQLQuery(OrgEmpPersonSQL) - .SetResultSetMapping("org-emp-person") - .ListAsync()); + .SetResultSetMapping("org-emp-person") + .ListAsync()); Assert.AreEqual(l.Count, 1); await (s.DeleteAsync(emp)); @@ -313,7 +314,7 @@ public async Task MappedAliasStrategyAsync() sqlQuery.SetResultTransformer(CriteriaSpecification.AliasToEntityMap); list = await (sqlQuery.ListAsync()); Assert.AreEqual(2, list.Count); - m = (IDictionary)list[0]; + m = (IDictionary) list[0]; Assert.IsTrue(m.Contains("org")); AssertClassAssignability(m["org"].GetType(), typeof(Organization)); Assert.IsTrue(m.Contains("emp")); @@ -381,28 +382,29 @@ public async Task CompositeIdJoinsFailureExpectedAsync() s = OpenSession(); t = s.BeginTransaction(); - object[] o = (object[]) (await (s.CreateSQLQuery("select\r\n" + - " product.orgid as {product.id.orgid}," + - " product.productnumber as {product.id.productnumber}," + - " {prod_orders}.orgid as orgid3_1_,\r\n" + - " {prod_orders}.ordernumber as ordernum2_3_1_,\r\n" + - " product.name as {product.name}," + - " {prod_orders.element.*}," + - /*" orders.PROD_NO as PROD4_3_1_,\r\n" + - " orders.person as person3_1_,\r\n" + - " orders.PROD_ORGID as PROD3_0__,\r\n" + - " orders.PROD_NO as PROD4_0__,\r\n" + - " orders.orgid as orgid0__,\r\n" + - " orders.ordernumber as ordernum2_0__ \r\n" +*/ - " from\r\n" + - " Product product \r\n" + - " inner join\r\n" + - " TBL_ORDER {prod_orders} \r\n" + - " on product.orgid={prod_orders}.PROD_ORGID \r\n" + - " and product.productnumber={prod_orders}.PROD_NO") - .AddEntity("product", typeof(Product)) - .AddJoin("prod_orders", "product.orders") - .ListAsync()))[0]; + object[] o = (object[]) (await (s.CreateSQLQuery( + "select\r\n" + + " product.orgid as {product.id.orgid}," + + " product.productnumber as {product.id.productnumber}," + + " {prod_orders}.orgid as orgid3_1_,\r\n" + + " {prod_orders}.ordernumber as ordernum2_3_1_,\r\n" + + " product.name as {product.name}," + + " {prod_orders.element.*}," + + /*" orders.PROD_NO as PROD4_3_1_,\r\n" + + " orders.person as person3_1_,\r\n" + + " orders.PROD_ORGID as PROD3_0__,\r\n" + + " orders.PROD_NO as PROD4_0__,\r\n" + + " orders.orgid as orgid0__,\r\n" + + " orders.ordernumber as ordernum2_0__ \r\n" +*/ + " from\r\n" + + " Product product \r\n" + + " inner join\r\n" + + " TBL_ORDER {prod_orders} \r\n" + + " on product.orgid={prod_orders}.PROD_ORGID \r\n" + + " and product.productnumber={prod_orders}.PROD_NO") + .AddEntity("product", typeof(Product)) + .AddJoin("prod_orders", "product.orders") + .ListAsync()))[0]; p = (Product) o[0]; Assert.IsTrue(NHibernateUtil.IsInitialized(p.Orders)); @@ -432,8 +434,8 @@ public async Task AutoDetectAliasingAsync() s = OpenSession(); t = s.BeginTransaction(); IList list = await (s.CreateSQLQuery(EmploymentSQL) - .AddEntity(typeof(Employment).FullName) - .ListAsync()); + .AddEntity(typeof(Employment).FullName) + .ListAsync()); Assert.AreEqual(1, list.Count); Employment emp2 = (Employment) list[0]; @@ -444,9 +446,9 @@ public async Task AutoDetectAliasingAsync() s.Clear(); list = await (s.CreateSQLQuery(EmploymentSQL) - .AddEntity(typeof(Employment).FullName) - .SetResultTransformer(CriteriaSpecification.AliasToEntityMap) - .ListAsync()); + .AddEntity(typeof(Employment).FullName) + .SetResultTransformer(CriteriaSpecification.AliasToEntityMap) + .ListAsync()); Assert.AreEqual(1, list.Count); IDictionary m = (IDictionary) list[0]; Assert.IsTrue(m.Contains("Employment")); @@ -485,17 +487,17 @@ public async Task AutoDetectAliasingAsync() s.Clear(); list = await (s.CreateSQLQuery(OrganizationJoinEmploymentSQL) - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .ListAsync()); + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .ListAsync()); Assert.AreEqual(2, list.Count); s.Clear(); list = await (s.CreateSQLQuery(OrganizationFetchJoinEmploymentSQL) - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .ListAsync()); + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .ListAsync()); Assert.AreEqual(2, list.Count); s.Clear(); @@ -569,8 +571,8 @@ public async Task MixAndMatchEntityScalarAsync() s.Clear(); IList l = await (s.CreateSQLQuery("select name, id, flength, name as scalarName from Speech") - .SetResultSetMapping("speech") - .ListAsync()); + .SetResultSetMapping("speech") + .ListAsync()); Assert.AreEqual(l.Count, 1); await (t.RollbackAsync()); @@ -583,9 +585,9 @@ public async Task ParameterListAsync() using (ISession s = OpenSession()) { IList l = await (s.CreateSQLQuery("select id from Speech where id in (:idList)") - .AddScalar("id", NHibernateUtil.Int32) - .SetParameterList("idList", new int[] {0, 1, 2, 3}, NHibernateUtil.Int32) - .ListAsync()); + .AddScalar("id", NHibernateUtil.Int32) + .SetParameterList("idList", new int[] {0, 1, 2, 3}, NHibernateUtil.Int32) + .ListAsync()); } } @@ -607,23 +609,26 @@ private double ExtractDoubleValue(object value) public static void AssertClassAssignability(System.Type source, System.Type target) { - Assert.IsTrue(target.IsAssignableFrom(source), - "Classes were not assignment-compatible : source<" + - source.FullName + - "> target<" + - target.FullName + ">" - ); + Assert.IsTrue( + target.IsAssignableFrom(source), + "Classes were not assignment-compatible : source<" + + source.FullName + + "> target<" + + target.FullName + ">" + ); } class TestResultSetTransformer : IResultTransformer { public bool TransformTupleCalled { get; set; } public bool TransformListCalled { get; set; } + public object TransformTuple(object[] tuple, string[] aliases) { this.TransformTupleCalled = true; return tuple; } + public IList TransformList(IList collection) { this.TransformListCalled = true; @@ -716,5 +721,33 @@ public async Task CanExecuteFutureValueAsync() Assert.AreEqual("Ricardo", v); } } + + [Test] + public async Task HandlesManualSynchronizationAsync() + { + using (var s = OpenSession()) + using (s.BeginTransaction()) + { + s.SessionFactory.Statistics.IsStatisticsEnabled = true; + s.SessionFactory.Statistics.Clear(); + + // create an Organization... + Organization jboss = new Organization("JBoss"); + await (s.PersistAsync(jboss)); + + // now query on Employment, this should not cause an auto-flush + await (s.CreateSQLQuery(EmploymentSQL).AddSynchronizedQuerySpace("ABC").ListAsync()); + Assert.AreEqual(0, s.SessionFactory.Statistics.EntityInsertCount); + + // now try to query on Employment but this time add Organization as a synchronized query space... + await (s.CreateSQLQuery(EmploymentSQL).AddSynchronizedEntityClass(typeof(Organization)).ListAsync()); + Assert.AreEqual(1, s.SessionFactory.Statistics.EntityInsertCount); + + // clean up + await (s.DeleteAsync(jboss)); + await (s.Transaction.CommitAsync()); + s.Close(); + } + } } } diff --git a/src/NHibernate.Test/BulkManipulation/BulkOperationCleanupActionFixture.cs b/src/NHibernate.Test/BulkManipulation/BulkOperationCleanupActionFixture.cs new file mode 100644 index 00000000000..53a099c753f --- /dev/null +++ b/src/NHibernate.Test/BulkManipulation/BulkOperationCleanupActionFixture.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Action; +using NHibernate.Engine; +using NHibernate.Metadata; +using NHibernate.Persister.Entity; +using NSubstitute; +using NUnit.Framework; + +namespace NHibernate.Test.BulkManipulation +{ + [TestFixture] + public class BulkOperationCleanupActionFixture + { + private ISessionImplementor _session; + private ISessionFactoryImplementor _factory; + private IEntityPersister _persister; + + [SetUp] + public void SetupTest() + { + _session = Substitute.For(); + _factory = Substitute.For(); + _persister = Substitute.For(); + _session.Factory.Returns(_factory); + _factory.GetAllClassMetadata().Returns(new Dictionary { ["TestClass"] = null }); + _factory.GetEntityPersister("TestClass").Returns(_persister); + _factory.GetCollectionRolesByEntityParticipant("TestClass").Returns(new HashSet(new[] { "TestClass.Children" })); + _persister.QuerySpaces.Returns(new[] { "TestClass" }); + _persister.EntityName.Returns("TestClass"); + } + + [TestCase("TestClass", true, 1, 1, 1)] + [TestCase("AnotherClass", true, 1, 0, 0)] + [TestCase("AnotherClass,TestClass", true, 2, 1, 1)] + [TestCase("TestClass", false, 1, 0, 1)] + [TestCase("", true, 1, 1, 1)] + [Test] + // 6.0 TODO: remove this ignore. + [Ignore("Must wait for the tested methods to be actually added to ISessionFactoryImplementor")] + public void AfterTransactionCompletionProcess_EvictsFromCache(string querySpaces, bool persisterHasCache, int expectedPropertySpaceLength, int expectedEntityEvictionCount, int expectedCollectionEvictionCount) + { + _persister.HasCache.Returns(persisterHasCache); + + var target = new BulkOperationCleanupAction(_session, new HashSet(querySpaces.Split(new []{','},StringSplitOptions.RemoveEmptyEntries))); + + target.AfterTransactionCompletionProcess(true); + + Assert.AreEqual(expectedPropertySpaceLength, target.PropertySpaces.Length); + + if (expectedEntityEvictionCount > 0) + { + _factory.Received(1).EvictEntity(Arg.Is>(x => x.Count() == expectedEntityEvictionCount)); + } + else + { + _factory.DidNotReceive().EvictEntity(Arg.Any>()); + } + + if (expectedCollectionEvictionCount > 0) + { + _factory.Received(1).EvictCollection(Arg.Is>(x => x.Count() == expectedCollectionEvictionCount)); + } + else + { + _factory.DidNotReceive().EvictCollection(Arg.Any>()); + } + } + } +} diff --git a/src/NHibernate.Test/BulkManipulation/NativeSQLBulkOperationsWithCache.cs b/src/NHibernate.Test/BulkManipulation/NativeSQLBulkOperationsWithCache.cs new file mode 100644 index 00000000000..54b19b970aa --- /dev/null +++ b/src/NHibernate.Test/BulkManipulation/NativeSQLBulkOperationsWithCache.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections; +using System.Collections.Concurrent; +using System.Collections.Generic; +using System.Linq; +using NHibernate.Cache; +using NHibernate.Cfg; +using NSubstitute; +using NUnit.Framework; +using Environment = NHibernate.Cfg.Environment; + +namespace NHibernate.Test.BulkManipulation +{ + [TestFixture] + public class NativeSQLBulkOperationsWithCache : TestCase + { + protected override string MappingsAssembly => "NHibernate.Test"; + + protected override IList Mappings => new[] { "BulkManipulation.Vehicle.hbm.xml" }; + + protected override void Configure(Configuration configuration) + { + cfg.SetProperty(Environment.UseQueryCache, "true"); + cfg.SetProperty(Environment.UseSecondLevelCache, "true"); + cfg.SetProperty(Environment.CacheProvider, typeof(SubstituteCacheProvider).AssemblyQualifiedName); + } + + [Test] + public void SimpleNativeSQLInsert_DoesNotEvictEntireCacheWhenQuerySpacesAreAdded() + { + List clearCalls = new List(); + (Sfi.Settings.CacheProvider as SubstituteCacheProvider).OnClear(x => + { + clearCalls.Add(x); + }); + using (var s = OpenSession()) + { + string ssql = "UPDATE Vehicle SET Vin='123' WHERE Vin='123c'"; + + using (var t = s.BeginTransaction()) + { + + s.CreateSQLQuery(ssql).ExecuteUpdate(); + t.Commit(); + + Assert.AreEqual(1, clearCalls.Count); + } + + clearCalls.Clear(); + + using (var t = s.BeginTransaction()) + { + s.CreateSQLQuery(ssql).AddSynchronizedQuerySpace("Unknown").ExecuteUpdate(); + t.Commit(); + + Assert.AreEqual(0, clearCalls.Count); + } + } + } + } + + public class SubstituteCacheProvider : ICacheProvider + { + private readonly ConcurrentDictionary> _caches = new ConcurrentDictionary>(); + private Action _onClear; + + public ICache BuildCache(string regionName, IDictionary properties) + { + return _caches.GetOrAdd(regionName, x => new Lazy(() => + { + var cache = Substitute.For(); + cache.RegionName.Returns(regionName); + cache.When(c => c.Clear()).Do(c => _onClear?.Invoke(regionName)); + return cache; + })).Value; + } + + public long NextTimestamp() + { + return Timestamper.Next(); + } + + public void Start(IDictionary properties) + { + } + + public void Stop() + { + } + + public ICache GetCache(string region) + { + Lazy cache; + _caches.TryGetValue(region, out cache); + return cache?.Value; + } + + public IEnumerable GetAllCaches() + { + return _caches.Values.Select(x => x.Value); + } + + public void OnClear(Action callback) + { + _onClear = callback; + } + } +} diff --git a/src/NHibernate.Test/MappingByCode/For.cs b/src/NHibernate.Test/MappingByCode/For.cs index 46f76f8b05b..1ab97920ec0 100644 --- a/src/NHibernate.Test/MappingByCode/For.cs +++ b/src/NHibernate.Test/MappingByCode/For.cs @@ -16,4 +16,4 @@ public static MemberInfo Property(Expression> propertyGetter) return TypeExtensions.DecodeMemberAccessExpression(propertyGetter); } } -} \ No newline at end of file +} diff --git a/src/NHibernate.Test/NHibernate.Test.csproj b/src/NHibernate.Test/NHibernate.Test.csproj index 3ae3f734cda..6e6df5e293d 100644 --- a/src/NHibernate.Test/NHibernate.Test.csproj +++ b/src/NHibernate.Test/NHibernate.Test.csproj @@ -49,7 +49,7 @@ - + diff --git a/src/NHibernate.Test/SqlTest/Query/NativeSQLQueriesFixture.cs b/src/NHibernate.Test/SqlTest/Query/NativeSQLQueriesFixture.cs index 69cc26b6ee3..f655d771cb9 100644 --- a/src/NHibernate.Test/SqlTest/Query/NativeSQLQueriesFixture.cs +++ b/src/NHibernate.Test/SqlTest/Query/NativeSQLQueriesFixture.cs @@ -43,7 +43,7 @@ public class GeneralTest : TestCase protected override IList Mappings { - get { return new[] { "SqlTest.Query.NativeSQLQueries.hbm.xml" }; } + get { return new[] {"SqlTest.Query.NativeSQLQueries.hbm.xml"}; } } protected override string MappingsAssembly @@ -92,17 +92,17 @@ public void SQLQueryInterface() s.Save(emp); IList l = s.CreateSQLQuery(OrgEmpRegionSQL) - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .AddScalar("regionCode", NHibernateUtil.String) - .List(); + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .AddScalar("regionCode", NHibernateUtil.String) + .List(); Assert.AreEqual(2, l.Count); l = s.CreateSQLQuery(OrgEmpPersonSQL) - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .AddJoin("pers", "emp.employee") - .List(); + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .AddJoin("pers", "emp.employee") + .List(); Assert.AreEqual(l.Count, 1); t.Commit(); @@ -111,13 +111,14 @@ public void SQLQueryInterface() s = OpenSession(); t = s.BeginTransaction(); - l = s.CreateSQLQuery("select {org.*}, {emp.*} " + - "from ORGANIZATION org " + - " left outer join EMPLOYMENT emp on org.ORGID = emp.EMPLOYER, ORGANIZATION org2") - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .SetResultTransformer(new DistinctRootEntityResultTransformer()) - .List(); + l = s.CreateSQLQuery( + "select {org.*}, {emp.*} " + + "from ORGANIZATION org " + + " left outer join EMPLOYMENT emp on org.ORGID = emp.EMPLOYER, ORGANIZATION org2") + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .SetResultTransformer(new DistinctRootEntityResultTransformer()) + .List(); Assert.AreEqual(l.Count, 2); t.Commit(); @@ -151,13 +152,13 @@ public void ResultSetMappingDefinition() s.Save(emp); IList l = s.CreateSQLQuery(OrgEmpRegionSQL) - .SetResultSetMapping("org-emp-regionCode") - .List(); + .SetResultSetMapping("org-emp-regionCode") + .List(); Assert.AreEqual(l.Count, 2); l = s.CreateSQLQuery(OrgEmpPersonSQL) - .SetResultSetMapping("org-emp-person") - .List(); + .SetResultSetMapping("org-emp-person") + .List(); Assert.AreEqual(l.Count, 1); s.Delete(emp); @@ -302,7 +303,7 @@ public void MappedAliasStrategy() sqlQuery.SetResultTransformer(CriteriaSpecification.AliasToEntityMap); list = sqlQuery.List(); Assert.AreEqual(2, list.Count); - m = (IDictionary)list[0]; + m = (IDictionary) list[0]; Assert.IsTrue(m.Contains("org")); AssertClassAssignability(m["org"].GetType(), typeof(Organization)); Assert.IsTrue(m.Contains("emp")); @@ -370,28 +371,29 @@ public void CompositeIdJoinsFailureExpected() s = OpenSession(); t = s.BeginTransaction(); - object[] o = (object[]) s.CreateSQLQuery("select\r\n" + - " product.orgid as {product.id.orgid}," + - " product.productnumber as {product.id.productnumber}," + - " {prod_orders}.orgid as orgid3_1_,\r\n" + - " {prod_orders}.ordernumber as ordernum2_3_1_,\r\n" + - " product.name as {product.name}," + - " {prod_orders.element.*}," + - /*" orders.PROD_NO as PROD4_3_1_,\r\n" + - " orders.person as person3_1_,\r\n" + - " orders.PROD_ORGID as PROD3_0__,\r\n" + - " orders.PROD_NO as PROD4_0__,\r\n" + - " orders.orgid as orgid0__,\r\n" + - " orders.ordernumber as ordernum2_0__ \r\n" +*/ - " from\r\n" + - " Product product \r\n" + - " inner join\r\n" + - " TBL_ORDER {prod_orders} \r\n" + - " on product.orgid={prod_orders}.PROD_ORGID \r\n" + - " and product.productnumber={prod_orders}.PROD_NO") - .AddEntity("product", typeof(Product)) - .AddJoin("prod_orders", "product.orders") - .List()[0]; + object[] o = (object[]) s.CreateSQLQuery( + "select\r\n" + + " product.orgid as {product.id.orgid}," + + " product.productnumber as {product.id.productnumber}," + + " {prod_orders}.orgid as orgid3_1_,\r\n" + + " {prod_orders}.ordernumber as ordernum2_3_1_,\r\n" + + " product.name as {product.name}," + + " {prod_orders.element.*}," + + /*" orders.PROD_NO as PROD4_3_1_,\r\n" + + " orders.person as person3_1_,\r\n" + + " orders.PROD_ORGID as PROD3_0__,\r\n" + + " orders.PROD_NO as PROD4_0__,\r\n" + + " orders.orgid as orgid0__,\r\n" + + " orders.ordernumber as ordernum2_0__ \r\n" +*/ + " from\r\n" + + " Product product \r\n" + + " inner join\r\n" + + " TBL_ORDER {prod_orders} \r\n" + + " on product.orgid={prod_orders}.PROD_ORGID \r\n" + + " and product.productnumber={prod_orders}.PROD_NO") + .AddEntity("product", typeof(Product)) + .AddJoin("prod_orders", "product.orders") + .List()[0]; p = (Product) o[0]; Assert.IsTrue(NHibernateUtil.IsInitialized(p.Orders)); @@ -421,8 +423,8 @@ public void AutoDetectAliasing() s = OpenSession(); t = s.BeginTransaction(); IList list = s.CreateSQLQuery(EmploymentSQL) - .AddEntity(typeof(Employment).FullName) - .List(); + .AddEntity(typeof(Employment).FullName) + .List(); Assert.AreEqual(1, list.Count); Employment emp2 = (Employment) list[0]; @@ -433,9 +435,9 @@ public void AutoDetectAliasing() s.Clear(); list = s.CreateSQLQuery(EmploymentSQL) - .AddEntity(typeof(Employment).FullName) - .SetResultTransformer(CriteriaSpecification.AliasToEntityMap) - .List(); + .AddEntity(typeof(Employment).FullName) + .SetResultTransformer(CriteriaSpecification.AliasToEntityMap) + .List(); Assert.AreEqual(1, list.Count); IDictionary m = (IDictionary) list[0]; Assert.IsTrue(m.Contains("Employment")); @@ -474,17 +476,17 @@ public void AutoDetectAliasing() s.Clear(); list = s.CreateSQLQuery(OrganizationJoinEmploymentSQL) - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .List(); + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .List(); Assert.AreEqual(2, list.Count); s.Clear(); list = s.CreateSQLQuery(OrganizationFetchJoinEmploymentSQL) - .AddEntity("org", typeof(Organization)) - .AddJoin("emp", "org.employments") - .List(); + .AddEntity("org", typeof(Organization)) + .AddJoin("emp", "org.employments") + .List(); Assert.AreEqual(2, list.Count); s.Clear(); @@ -558,8 +560,8 @@ public void MixAndMatchEntityScalar() s.Clear(); IList l = s.CreateSQLQuery("select name, id, flength, name as scalarName from Speech") - .SetResultSetMapping("speech") - .List(); + .SetResultSetMapping("speech") + .List(); Assert.AreEqual(l.Count, 1); t.Rollback(); @@ -572,9 +574,9 @@ public void ParameterList() using (ISession s = OpenSession()) { IList l = s.CreateSQLQuery("select id from Speech where id in (:idList)") - .AddScalar("id", NHibernateUtil.Int32) - .SetParameterList("idList", new int[] {0, 1, 2, 3}, NHibernateUtil.Int32) - .List(); + .AddScalar("id", NHibernateUtil.Int32) + .SetParameterList("idList", new int[] {0, 1, 2, 3}, NHibernateUtil.Int32) + .List(); } } @@ -596,23 +598,26 @@ private double ExtractDoubleValue(object value) public static void AssertClassAssignability(System.Type source, System.Type target) { - Assert.IsTrue(target.IsAssignableFrom(source), - "Classes were not assignment-compatible : source<" + - source.FullName + - "> target<" + - target.FullName + ">" - ); + Assert.IsTrue( + target.IsAssignableFrom(source), + "Classes were not assignment-compatible : source<" + + source.FullName + + "> target<" + + target.FullName + ">" + ); } class TestResultSetTransformer : IResultTransformer { public bool TransformTupleCalled { get; set; } public bool TransformListCalled { get; set; } + public object TransformTuple(object[] tuple, string[] aliases) { this.TransformTupleCalled = true; return tuple; } + public IList TransformList(IList collection) { this.TransformListCalled = true; @@ -705,5 +710,33 @@ public void CanExecuteFutureValue() Assert.AreEqual("Ricardo", v); } } + + [Test] + public void HandlesManualSynchronization() + { + using (var s = OpenSession()) + using (s.BeginTransaction()) + { + s.SessionFactory.Statistics.IsStatisticsEnabled = true; + s.SessionFactory.Statistics.Clear(); + + // create an Organization... + Organization jboss = new Organization("JBoss"); + s.Persist(jboss); + + // now query on Employment, this should not cause an auto-flush + s.CreateSQLQuery(EmploymentSQL).AddSynchronizedQuerySpace("ABC").List(); + Assert.AreEqual(0, s.SessionFactory.Statistics.EntityInsertCount); + + // now try to query on Employment but this time add Organization as a synchronized query space... + s.CreateSQLQuery(EmploymentSQL).AddSynchronizedEntityClass(typeof(Organization)).List(); + Assert.AreEqual(1, s.SessionFactory.Statistics.EntityInsertCount); + + // clean up + s.Delete(jboss); + s.Transaction.Commit(); + s.Close(); + } + } } } diff --git a/src/NHibernate.sln b/src/NHibernate.sln index 67d957ed5d6..2c7f1c7eb97 100644 --- a/src/NHibernate.sln +++ b/src/NHibernate.sln @@ -1,6 +1,6 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 -VisualStudioVersion = 15.0.26730.12 +VisualStudioVersion = 15.0.27130.2024 MinimumVisualStudioVersion = 10.0.40219.1 Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{593DCEA7-C933-46F3-939F-D8172399AB05}" ProjectSection(SolutionItems) = preProject diff --git a/src/NHibernate/Action/BulkOperationCleanupAction.cs b/src/NHibernate/Action/BulkOperationCleanupAction.cs index 1e6edd1d0b3..53cbaf177bc 100644 --- a/src/NHibernate/Action/BulkOperationCleanupAction.cs +++ b/src/NHibernate/Action/BulkOperationCleanupAction.cs @@ -1,8 +1,12 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using NHibernate.Engine; using NHibernate.Metadata; using NHibernate.Persister.Entity; +using IQueryable = NHibernate.Persister.Entity.IQueryable; namespace NHibernate.Action { @@ -10,7 +14,7 @@ namespace NHibernate.Action /// Implementation of BulkOperationCleanupAction. /// [Serializable] - public partial class BulkOperationCleanupAction: IExecutable + public partial class BulkOperationCleanupAction : IExecutable { private readonly ISessionImplementor session; private readonly HashSet affectedEntityNames = new HashSet(); @@ -84,14 +88,8 @@ private bool AffectedEntity(ISet querySpaces, string[] entitySpaces) return true; } - for (int i = 0; i < entitySpaces.Length; i++) - { - if (querySpaces.Contains(entitySpaces[i])) - { - return true; - } - } - return false; + + return entitySpaces.Any(querySpaces.Contains); } #region IExecutable Members @@ -113,8 +111,8 @@ public void Execute() public BeforeTransactionCompletionProcessDelegate BeforeTransactionCompletionProcess { - get - { + get + { return null; } } @@ -133,32 +131,36 @@ public AfterTransactionCompletionProcessDelegate AfterTransactionCompletionProce private void EvictCollectionRegions() { - if (affectedCollectionRoles != null) + if (affectedCollectionRoles != null && affectedCollectionRoles.Any()) { - foreach (string roleName in affectedCollectionRoles) - { - session.Factory.EvictCollection(roleName); - } + session.Factory.EvictCollection(affectedCollectionRoles); } } private void EvictEntityRegions() { - if (affectedEntityNames != null) + if (affectedEntityNames != null && affectedEntityNames.Any()) { - foreach (string entityName in affectedEntityNames) - { - session.Factory.EvictEntity(entityName); - } + session.Factory.EvictEntity(affectedEntityNames); } } #endregion + // Since v5.2 + [Obsolete("This method has no more usage in NHibernate and will be removed in a future version.")] public virtual void Init() { EvictEntityRegions(); EvictCollectionRegions(); } + + // Since v5.2 + [Obsolete("This method has no more usage in NHibernate and will be removed in a future version.")] + public virtual async Task InitAsync(CancellationToken cancellationToken) + { + await EvictEntityRegionsAsync(cancellationToken); + await EvictCollectionRegionsAsync(cancellationToken); + } } } diff --git a/src/NHibernate/Async/Action/BulkOperationCleanupAction.cs b/src/NHibernate/Async/Action/BulkOperationCleanupAction.cs index 899db3bc5d0..c65a41770fc 100644 --- a/src/NHibernate/Async/Action/BulkOperationCleanupAction.cs +++ b/src/NHibernate/Async/Action/BulkOperationCleanupAction.cs @@ -10,15 +10,17 @@ using System; using System.Collections.Generic; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; using NHibernate.Engine; using NHibernate.Metadata; using NHibernate.Persister.Entity; +using IQueryable = NHibernate.Persister.Entity.IQueryable; namespace NHibernate.Action { - using System.Threading.Tasks; - using System.Threading; - public partial class BulkOperationCleanupAction: IExecutable + public partial class BulkOperationCleanupAction : IExecutable { #region IExecutable Members @@ -57,37 +59,46 @@ public Task ExecuteAsync(CancellationToken cancellationToken) } } - private async Task EvictCollectionRegionsAsync(CancellationToken cancellationToken) + private Task EvictCollectionRegionsAsync(CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); - if (affectedCollectionRoles != null) + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try { - foreach (string roleName in affectedCollectionRoles) + if (affectedCollectionRoles != null && affectedCollectionRoles.Any()) { - await (session.Factory.EvictCollectionAsync(roleName, cancellationToken)).ConfigureAwait(false); + return session.Factory.EvictCollectionAsync(affectedCollectionRoles, cancellationToken); } + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); } } - private async Task EvictEntityRegionsAsync(CancellationToken cancellationToken) + private Task EvictEntityRegionsAsync(CancellationToken cancellationToken) { - cancellationToken.ThrowIfCancellationRequested(); - if (affectedEntityNames != null) + if (cancellationToken.IsCancellationRequested) { - foreach (string entityName in affectedEntityNames) + return Task.FromCanceled(cancellationToken); + } + try + { + if (affectedEntityNames != null && affectedEntityNames.Any()) { - await (session.Factory.EvictEntityAsync(entityName, cancellationToken)).ConfigureAwait(false); + return session.Factory.EvictEntityAsync(affectedEntityNames, cancellationToken); } + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); } } #endregion - - public virtual async Task InitAsync(CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - await (EvictEntityRegionsAsync(cancellationToken)).ConfigureAwait(false); - await (EvictCollectionRegionsAsync(cancellationToken)).ConfigureAwait(false); - } } } diff --git a/src/NHibernate/Async/Engine/Query/NativeSQLQueryPlan.cs b/src/NHibernate/Async/Engine/Query/NativeSQLQueryPlan.cs index 2cf63ed75c9..19edd8b92ca 100644 --- a/src/NHibernate/Async/Engine/Query/NativeSQLQueryPlan.cs +++ b/src/NHibernate/Async/Engine/Query/NativeSQLQueryPlan.cs @@ -34,24 +34,11 @@ namespace NHibernate.Engine.Query public partial class NativeSQLQueryPlan { - private async Task CoordinateSharedCacheCleanupAsync(ISessionImplementor session, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - BulkOperationCleanupAction action = new BulkOperationCleanupAction(session, CustomQuery.QuerySpaces); - - await (action.InitAsync(cancellationToken)).ConfigureAwait(false); - - if (session.IsEventSource) - { - ((IEventSource)session).ActionQueue.AddAction(action); - } - } - // DONE : H3.2 Executable query (now can be supported for named SQL query/ storedProcedure) public async Task PerformExecuteUpdateAsync(QueryParameters queryParameters, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - await (CoordinateSharedCacheCleanupAsync(session, cancellationToken)).ConfigureAwait(false); + CoordinateSharedCacheCleanup(session); if (queryParameters.Callable) { diff --git a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs index f3300c08454..046ff3d4c51 100644 --- a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs +++ b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs @@ -24,29 +24,16 @@ using NHibernate.Transaction; using NHibernate.Util; using System.Data; +using System.Threading; +using System.Threading.Tasks; namespace NHibernate.Hql.Ast.ANTLR.Exec { - using System.Threading.Tasks; - using System.Threading; public abstract partial class AbstractStatementExecutor : IStatementExecutor { public abstract Task ExecuteAsync(QueryParameters parameters, ISessionImplementor session, CancellationToken cancellationToken); - protected virtual async Task CoordinateSharedCacheCleanupAsync(ISessionImplementor session, CancellationToken cancellationToken) - { - cancellationToken.ThrowIfCancellationRequested(); - var action = new BulkOperationCleanupAction(session, AffectedQueryables); - - await (action.InitAsync(cancellationToken)).ConfigureAwait(false); - - if (session.IsEventSource) - { - ((IEventSource)session).ActionQueue.AddAction(action); - } - } - protected virtual async Task CreateTemporaryTableIfNecessaryAsync(IQueryable persister, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/BasicExecutor.cs b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/BasicExecutor.cs index b5f4fd04194..62499dcdd5c 100644 --- a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/BasicExecutor.cs +++ b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/BasicExecutor.cs @@ -34,7 +34,7 @@ public partial class BasicExecutor : AbstractStatementExecutor public override async Task ExecuteAsync(QueryParameters parameters, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - await (CoordinateSharedCacheCleanupAsync(session, cancellationToken)).ConfigureAwait(false); + CoordinateSharedCacheCleanup(session); DbCommand st = null; RowSelection selection = parameters.RowSelection; diff --git a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/MultiTableDeleteExecutor.cs b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/MultiTableDeleteExecutor.cs index 27146fd0f95..2377dfb8ba3 100644 --- a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/MultiTableDeleteExecutor.cs +++ b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/MultiTableDeleteExecutor.cs @@ -31,7 +31,7 @@ public partial class MultiTableDeleteExecutor : AbstractStatementExecutor public override async Task ExecuteAsync(QueryParameters parameters, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - await (CoordinateSharedCacheCleanupAsync(session, cancellationToken)).ConfigureAwait(false); + CoordinateSharedCacheCleanup(session); await (CreateTemporaryTableIfNecessaryAsync(persister, session, cancellationToken)).ConfigureAwait(false); diff --git a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/MultiTableUpdateExecutor.cs b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/MultiTableUpdateExecutor.cs index 71de9bb7c59..a7b4c996647 100644 --- a/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/MultiTableUpdateExecutor.cs +++ b/src/NHibernate/Async/Hql/Ast/ANTLR/Exec/MultiTableUpdateExecutor.cs @@ -33,7 +33,7 @@ public partial class MultiTableUpdateExecutor : AbstractStatementExecutor public override async Task ExecuteAsync(QueryParameters parameters, ISessionImplementor session, CancellationToken cancellationToken) { cancellationToken.ThrowIfCancellationRequested(); - await (CoordinateSharedCacheCleanupAsync(session, cancellationToken)).ConfigureAwait(false); + CoordinateSharedCacheCleanup(session); await (CreateTemporaryTableIfNecessaryAsync(persister, session, cancellationToken)).ConfigureAwait(false); diff --git a/src/NHibernate/Async/ISessionFactory.cs b/src/NHibernate/Async/ISessionFactory.cs index aea30be06be..744c59b3bde 100644 --- a/src/NHibernate/Async/ISessionFactory.cs +++ b/src/NHibernate/Async/ISessionFactory.cs @@ -14,6 +14,7 @@ using System.Data.Common; using NHibernate.Connection; using NHibernate.Engine; +using NHibernate.Impl; using NHibernate.Metadata; using NHibernate.Stat; @@ -21,6 +22,87 @@ namespace NHibernate { using System.Threading.Tasks; using System.Threading; + public static partial class SessionFactoryExtension + { + /// + /// Evict all entries from the process-level cache. This method occurs outside + /// of any transaction; it performs an immediate "hard" remove, so does not respect + /// any transaction isolation semantics of the usage strategy. Use with care. + /// + /// The session factory. + /// The classes of the entities to evict. + /// A cancellation token that can be used to cancel the work + public static async Task EvictAsync(this ISessionFactory factory, IEnumerable persistentClasses, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + if (factory is SessionFactoryImpl sfi) + { + await (sfi.EvictAsync(persistentClasses, cancellationToken)).ConfigureAwait(false); + } + else + { + if (persistentClasses == null) + throw new ArgumentNullException(nameof(persistentClasses)); + foreach (var @class in persistentClasses) + { + await (factory.EvictAsync(@class, cancellationToken)).ConfigureAwait(false); + } + } + } + + /// + /// Evict all entries from the second-level cache. This method occurs outside + /// of any transaction; it performs an immediate "hard" remove, so does not respect + /// any transaction isolation semantics of the usage strategy. Use with care. + /// + /// The session factory. + /// The names of the entities to evict. + /// A cancellation token that can be used to cancel the work + public static async Task EvictEntityAsync(this ISessionFactory factory, IEnumerable entityNames, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + if (factory is SessionFactoryImpl sfi) + { + await (sfi.EvictEntityAsync(entityNames, cancellationToken)).ConfigureAwait(false); + } + else + { + if (entityNames == null) + throw new ArgumentNullException(nameof(entityNames)); + foreach (var name in entityNames) + { + await (factory.EvictEntityAsync(name, cancellationToken)).ConfigureAwait(false); + } + } + } + + /// + /// Evict all entries from the process-level cache. This method occurs outside + /// of any transaction; it performs an immediate "hard" remove, so does not respect + /// any transaction isolation semantics of the usage strategy. Use with care. + /// + /// The session factory. + /// The names of the collections to evict. + /// A cancellation token that can be used to cancel the work + public static async Task EvictCollectionAsync(this ISessionFactory factory, IEnumerable roleNames, CancellationToken cancellationToken = default(CancellationToken)) + { + cancellationToken.ThrowIfCancellationRequested(); + if (factory is SessionFactoryImpl sfi) + { + await (sfi.EvictCollectionAsync(roleNames, cancellationToken)).ConfigureAwait(false); + } + else + { + if (roleNames == null) + throw new ArgumentNullException(nameof(roleNames)); + foreach (var role in roleNames) + { + await (factory.EvictCollectionAsync(role, cancellationToken)).ConfigureAwait(false); + } + } + } + } + public partial interface ISessionFactory : IDisposable { diff --git a/src/NHibernate/Async/Impl/SessionFactoryImpl.cs b/src/NHibernate/Async/Impl/SessionFactoryImpl.cs index 05b23b537ea..8dcb9b347da 100644 --- a/src/NHibernate/Async/Impl/SessionFactoryImpl.cs +++ b/src/NHibernate/Async/Impl/SessionFactoryImpl.cs @@ -165,6 +165,24 @@ public sealed partial class SessionFactoryImpl : ISessionFactoryImplementor, IOb } } + public Task EvictAsync(IEnumerable persistentClasses, CancellationToken cancellationToken) + { + if (persistentClasses == null) + throw new ArgumentNullException(nameof(persistentClasses)); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + try + { + return EvictEntityAsync(persistentClasses.Select(x => x.FullName), cancellationToken); + } + catch (Exception ex) + { + return Task.FromException(ex); + } + } + public Task EvictEntityAsync(string entityName, CancellationToken cancellationToken = default(CancellationToken)) { if (cancellationToken.IsCancellationRequested) @@ -190,6 +208,30 @@ public sealed partial class SessionFactoryImpl : ISessionFactoryImplementor, IOb } } + public Task EvictEntityAsync(IEnumerable entityNames, CancellationToken cancellationToken) + { + if (entityNames == null) + throw new ArgumentNullException(nameof(entityNames)); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return InternalEvictEntityAsync(); + async Task InternalEvictEntityAsync() + { + + foreach (var cacheGroup in entityNames.Select(GetEntityPersister).Where(x => x.HasCache).GroupBy(x => x.Cache)) + { + if (log.IsDebugEnabled()) + { + log.Debug("evicting second-level cache for: {0}", + string.Join(", ", cacheGroup.Select(p => p.EntityName))); + } + await (cacheGroup.Key.ClearAsync(cancellationToken)).ConfigureAwait(false); + } + } + } + public Task EvictEntityAsync(string entityName, object id, CancellationToken cancellationToken = default(CancellationToken)) { if (cancellationToken.IsCancellationRequested) @@ -267,6 +309,30 @@ public sealed partial class SessionFactoryImpl : ISessionFactoryImplementor, IOb } } + public Task EvictCollectionAsync(IEnumerable roleNames, CancellationToken cancellationToken) + { + if (roleNames == null) + throw new ArgumentNullException(nameof(roleNames)); + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + return InternalEvictCollectionAsync(); + async Task InternalEvictCollectionAsync() + { + + foreach (var cacheGroup in roleNames.Select(GetCollectionPersister).Where(x => x.HasCache).GroupBy(x => x.Cache)) + { + if (log.IsDebugEnabled()) + { + log.Debug("evicting second-level cache for: {0}", + string.Join(", ", cacheGroup.Select(p => p.Role))); + } + await (cacheGroup.Key.ClearAsync(cancellationToken)).ConfigureAwait(false); + } + } + } + public async Task EvictQueriesAsync(CancellationToken cancellationToken = default(CancellationToken)) { cancellationToken.ThrowIfCancellationRequested(); diff --git a/src/NHibernate/Async/Impl/SqlQueryImpl.cs b/src/NHibernate/Async/Impl/SqlQueryImpl.cs index 8f85915879a..6811ce34792 100644 --- a/src/NHibernate/Async/Impl/SqlQueryImpl.cs +++ b/src/NHibernate/Async/Impl/SqlQueryImpl.cs @@ -21,7 +21,7 @@ namespace NHibernate.Impl { using System.Threading.Tasks; using System.Threading; - public partial class SqlQueryImpl : AbstractQueryImpl, ISQLQuery + public partial class SqlQueryImpl : AbstractQueryImpl, ISQLQuery, ISynchronizableSQLQuery { public override async Task ListAsync(CancellationToken cancellationToken = default(CancellationToken)) diff --git a/src/NHibernate/Engine/Query/NativeSQLQueryPlan.cs b/src/NHibernate/Engine/Query/NativeSQLQueryPlan.cs index f2e9a275f45..2d71b63bfe2 100644 --- a/src/NHibernate/Engine/Query/NativeSQLQueryPlan.cs +++ b/src/NHibernate/Engine/Query/NativeSQLQueryPlan.cs @@ -48,8 +48,6 @@ private void CoordinateSharedCacheCleanup(ISessionImplementor session) { BulkOperationCleanupAction action = new BulkOperationCleanupAction(session, CustomQuery.QuerySpaces); - action.Init(); - if (session.IsEventSource) { ((IEventSource)session).ActionQueue.AddAction(action); diff --git a/src/NHibernate/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs b/src/NHibernate/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs index a45819d7c6c..8b864f8da1d 100644 --- a/src/NHibernate/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs +++ b/src/NHibernate/Hql/Ast/ANTLR/Exec/AbstractStatementExecutor.cs @@ -14,6 +14,8 @@ using NHibernate.Transaction; using NHibernate.Util; using System.Data; +using System.Threading; +using System.Threading.Tasks; namespace NHibernate.Hql.Ast.ANTLR.Exec { @@ -47,11 +49,30 @@ protected virtual void CoordinateSharedCacheCleanup(ISessionImplementor session) { var action = new BulkOperationCleanupAction(session, AffectedQueryables); - action.Init(); - if (session.IsEventSource) { - ((IEventSource)session).ActionQueue.AddAction(action); + ((IEventSource) session).ActionQueue.AddAction(action); + } + else + { + action.AfterTransactionCompletionProcess(true); + } + } + + // Since v5.2 + [Obsolete("This method has no more actual async calls to do, use its sync version instead.")] + protected virtual Task CoordinateSharedCacheCleanupAsync(ISessionImplementor session, CancellationToken cancellationToken) + { + cancellationToken.ThrowIfCancellationRequested(); + + try + { + CoordinateSharedCacheCleanup(session); + return Task.CompletedTask; + } + catch (Exception ex) + { + return Task.FromException(ex); } } diff --git a/src/NHibernate/ISQLQuery.cs b/src/NHibernate/ISQLQuery.cs index 5e8ea66dacd..cb28cac13d0 100644 --- a/src/NHibernate/ISQLQuery.cs +++ b/src/NHibernate/ISQLQuery.cs @@ -1,7 +1,66 @@ +using System; +using System.Collections.Generic; using NHibernate.Type; +using NHibernate.Util; namespace NHibernate { + // 6.0 TODO: remove after having done ISynchronizableSQLQuery todo + public static class SQLQueryExtension + { + /// + /// Adds a query space for auto-flush synchronization and second level cache invalidation. + /// + /// The query. + /// The query space. + /// The query. + public static ISQLQuery AddSynchronizedQuerySpace(this ISQLQuery sqlQuery, string querySpace) + { + var ssq = ReflectHelper.CastOrThrow(sqlQuery, "synchronizable query"); + return ssq.AddSynchronizedQuerySpace(querySpace); + } + + /// + /// Adds an entity name for auto-flush synchronization and second level cache invalidation. + /// + /// The query. + /// The entity name. + /// The query. + public static ISQLQuery AddSynchronizedEntityName(this ISQLQuery sqlQuery, string entityName) + { + var ssq = ReflectHelper.CastOrThrow(sqlQuery, "synchronizable query"); + return ssq.AddSynchronizedEntityName(entityName); + } + + /// + /// Adds an entity type for auto-flush synchronization and second level cache invalidation. + /// + /// The query. + /// The entity type. + /// The query. + public static ISQLQuery AddSynchronizedEntityClass(this ISQLQuery sqlQuery, System.Type entityType) + { + var ssq = ReflectHelper.CastOrThrow(sqlQuery, "synchronizable query"); + return ssq.AddSynchronizedEntityClass(entityType); + } + + /// + /// Returns the synchronized query spaces added to the query. + /// + /// The query. + /// The synchronized query spaces. + public static IReadOnlyCollection GetSynchronizedQuerySpaces(this ISQLQuery sqlQuery) + { + var ssq = ReflectHelper.CastOrThrow(sqlQuery, "synchronizable query"); + return ssq.GetSynchronizedQuerySpaces(); + } + } + + // 6.0 TODO: obsolete ISynchronizableSQLQuery and have ISQLQuery directly extending ISynchronizableQuery + public interface ISynchronizableSQLQuery : ISQLQuery, ISynchronizableQuery + { + } + public interface ISQLQuery : IQuery { /// diff --git a/src/NHibernate/ISessionFactory.cs b/src/NHibernate/ISessionFactory.cs index 6881abf59aa..544f79d1a57 100644 --- a/src/NHibernate/ISessionFactory.cs +++ b/src/NHibernate/ISessionFactory.cs @@ -4,11 +4,88 @@ using System.Data.Common; using NHibernate.Connection; using NHibernate.Engine; +using NHibernate.Impl; using NHibernate.Metadata; using NHibernate.Stat; namespace NHibernate { + // 6.0 TODO: move below methods directly in ISessionFactory then remove SessionFactoryExtension + public static partial class SessionFactoryExtension + { + /// + /// Evict all entries from the process-level cache. This method occurs outside + /// of any transaction; it performs an immediate "hard" remove, so does not respect + /// any transaction isolation semantics of the usage strategy. Use with care. + /// + /// The session factory. + /// The classes of the entities to evict. + public static void Evict(this ISessionFactory factory, IEnumerable persistentClasses) + { + if (factory is SessionFactoryImpl sfi) + { + sfi.Evict(persistentClasses); + } + else + { + if (persistentClasses == null) + throw new ArgumentNullException(nameof(persistentClasses)); + foreach (var @class in persistentClasses) + { + factory.Evict(@class); + } + } + } + + /// + /// Evict all entries from the second-level cache. This method occurs outside + /// of any transaction; it performs an immediate "hard" remove, so does not respect + /// any transaction isolation semantics of the usage strategy. Use with care. + /// + /// The session factory. + /// The names of the entities to evict. + public static void EvictEntity(this ISessionFactory factory, IEnumerable entityNames) + { + if (factory is SessionFactoryImpl sfi) + { + sfi.EvictEntity(entityNames); + } + else + { + if (entityNames == null) + throw new ArgumentNullException(nameof(entityNames)); + foreach (var name in entityNames) + { + factory.EvictEntity(name); + } + } + } + + /// + /// Evict all entries from the process-level cache. This method occurs outside + /// of any transaction; it performs an immediate "hard" remove, so does not respect + /// any transaction isolation semantics of the usage strategy. Use with care. + /// + /// The session factory. + /// The names of the collections to evict. + public static void EvictCollection(this ISessionFactory factory, IEnumerable roleNames) + { + if (factory is SessionFactoryImpl sfi) + { + sfi.EvictCollection(roleNames); + } + else + { + if (roleNames == null) + throw new ArgumentNullException(nameof(roleNames)); + foreach (var role in roleNames) + { + factory.EvictCollection(role); + } + } + } + } + /// /// Creates ISessions. /// diff --git a/src/NHibernate/ISynchronizableQuery.cs b/src/NHibernate/ISynchronizableQuery.cs new file mode 100644 index 00000000000..03535fea750 --- /dev/null +++ b/src/NHibernate/ISynchronizableQuery.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; + +namespace NHibernate +{ + public interface ISynchronizableQuery where T : ISynchronizableQuery + { + /// + /// Adds a query space for auto-flush synchronization and second level cache invalidation. + /// + /// The query space. + /// The query. + T AddSynchronizedQuerySpace(string querySpace); + + /// + /// Adds an entity name for auto-flush synchronization and second level cache invalidation. + /// + /// The entity name. + /// The query. + T AddSynchronizedEntityName(string entityName); + + /// + /// Adds an entity type for auto-flush synchronization and second level cache invalidation. + /// + /// The entity type. + /// The query. + T AddSynchronizedEntityClass(System.Type entityType); + + /// + /// Returns the synchronized query spaces added to the query. + /// + /// The synchronized query spaces. + IReadOnlyCollection GetSynchronizedQuerySpaces(); + } +} diff --git a/src/NHibernate/Impl/AbstractQueryImpl.cs b/src/NHibernate/Impl/AbstractQueryImpl.cs index 4331813c028..f77e4b4a1cf 100644 --- a/src/NHibernate/Impl/AbstractQueryImpl.cs +++ b/src/NHibernate/Impl/AbstractQueryImpl.cs @@ -18,7 +18,7 @@ namespace NHibernate.Impl public abstract partial class AbstractQueryImpl : IQuery { private readonly string queryString; - private readonly ISessionImplementor session; + protected readonly ISessionImplementor session; protected internal ParameterMetadata parameterMetadata; private readonly RowSelection selection; diff --git a/src/NHibernate/Impl/SessionFactoryImpl.cs b/src/NHibernate/Impl/SessionFactoryImpl.cs index d80c1a1fdb7..a715723f71e 100644 --- a/src/NHibernate/Impl/SessionFactoryImpl.cs +++ b/src/NHibernate/Impl/SessionFactoryImpl.cs @@ -901,6 +901,13 @@ public void Evict(System.Type persistentClass) } } + public void Evict(IEnumerable persistentClasses) + { + if (persistentClasses == null) + throw new ArgumentNullException(nameof(persistentClasses)); + EvictEntity(persistentClasses.Select(x => x.FullName)); + } + public void EvictEntity(string entityName) { IEntityPersister p = GetEntityPersister(entityName); @@ -914,6 +921,22 @@ public void EvictEntity(string entityName) } } + public void EvictEntity(IEnumerable entityNames) + { + if (entityNames == null) + throw new ArgumentNullException(nameof(entityNames)); + + foreach (var cacheGroup in entityNames.Select(GetEntityPersister).Where(x => x.HasCache).GroupBy(x => x.Cache)) + { + if (log.IsDebugEnabled()) + { + log.Debug("evicting second-level cache for: {0}", + string.Join(", ", cacheGroup.Select(p => p.EntityName))); + } + cacheGroup.Key.Clear(); + } + } + public void EvictEntity(string entityName, object id) { IEntityPersister p = GetEntityPersister(entityName); @@ -969,6 +992,22 @@ public void EvictCollection(string roleName) } } + public void EvictCollection(IEnumerable roleNames) + { + if (roleNames == null) + throw new ArgumentNullException(nameof(roleNames)); + + foreach (var cacheGroup in roleNames.Select(GetCollectionPersister).Where(x => x.HasCache).GroupBy(x => x.Cache)) + { + if (log.IsDebugEnabled()) + { + log.Debug("evicting second-level cache for: {0}", + string.Join(", ", cacheGroup.Select(p => p.Role))); + } + cacheGroup.Key.Clear(); + } + } + public IType GetReferencedPropertyType(string className, string propertyName) { return GetEntityPersister(className).GetPropertyType(propertyName); diff --git a/src/NHibernate/Impl/SqlQueryImpl.cs b/src/NHibernate/Impl/SqlQueryImpl.cs index 43bb2544500..0cc008d2173 100644 --- a/src/NHibernate/Impl/SqlQueryImpl.cs +++ b/src/NHibernate/Impl/SqlQueryImpl.cs @@ -22,12 +22,13 @@ namespace NHibernate.Impl /// </sql-query-name> /// /// - public partial class SqlQueryImpl : AbstractQueryImpl, ISQLQuery + public partial class SqlQueryImpl : AbstractQueryImpl, ISQLQuery, ISynchronizableSQLQuery { private readonly IList queryReturns; private readonly ICollection querySpaces; private readonly bool callable; private bool autoDiscoverTypes; + private readonly HashSet addedQuerySpaces = new HashSet(); /// Constructs a SQLQueryImpl given a sql query defined in the mappings. /// The representation of the defined sql-query. @@ -170,10 +171,15 @@ public override IList List() public NativeSQLQuerySpecification GenerateQuerySpecification(IDictionary parameters) { + var allQuerySpaces = new List(GetSynchronizedQuerySpaces()); + if (querySpaces != null) + { + allQuerySpaces.AddRange(querySpaces); + } return new NativeSQLQuerySpecification( ExpandParameterLists(parameters), GetQueryReturns(), - querySpaces); + allQuerySpaces); } public override QueryParameters GetQueryParameters(IDictionary namedParams) @@ -320,5 +326,36 @@ protected internal override IEnumerable GetTranslators(ISessionImpl var sqlQuery = this as ISQLQuery; yield return new SqlTranslator(sqlQuery, sessionImplementor.Factory); } + + public ISynchronizableSQLQuery AddSynchronizedQuerySpace(string querySpace) + { + addedQuerySpaces.Add(querySpace); + return this; + } + + public ISynchronizableSQLQuery AddSynchronizedEntityName(string entityName) + { + var persister = session.Factory.GetEntityPersister(entityName); + foreach (var querySpace in persister.QuerySpaces) + { + addedQuerySpaces.Add(querySpace); + } + return this; + } + + public ISynchronizableSQLQuery AddSynchronizedEntityClass(System.Type entityType) + { + var persister = session.Factory.GetEntityPersister(entityType.FullName); + foreach (var querySpace in persister.QuerySpaces) + { + addedQuerySpaces.Add(querySpace); + } + return this; + } + + public IReadOnlyCollection GetSynchronizedQuerySpaces() + { + return addedQuerySpaces; + } } } From 06ad6ac9c68217ed3401d7ee8331910c5efd1924 Mon Sep 17 00:00:00 2001 From: Alexander Zaytsev Date: Thu, 14 Jun 2018 01:46:46 +1200 Subject: [PATCH 17/17] NH-1316 - Fix returing generated identity for PostgreSQL Fixes #1201 --- .../{NH2204 => NH1316}/Fixture.cs | 9 +++---- .../{NH2204 => NH1316}/Fixture.cs | 13 +++------ .../NHSpecificTest/NH1316/Mappings.hbm.xml | 27 +++++++++++++++++++ .../NHSpecificTest/NH1316/Model.cs | 19 +++++++++++++ .../NHSpecificTest/NH2204/Mappings.hbm.xml | 1 - .../NHSpecificTest/NH2204/Model.cs | 1 - src/NHibernate/Dialect/Dialect.cs | 18 ++++++++++++- src/NHibernate/Dialect/PostgreSQL81Dialect.cs | 5 ++++ src/NHibernate/Dialect/PostgreSQLDialect.cs | 2 +- src/NHibernate/Id/IdentityGenerator.cs | 2 +- .../Id/Insert/InsertSelectIdentityInsert.cs | 13 ++++++++- .../Id/Insert/ReturningIdentifierInsert.cs | 4 +-- 12 files changed, 91 insertions(+), 23 deletions(-) rename src/NHibernate.Test/Async/NHSpecificTest/{NH2204 => NH1316}/Fixture.cs (91%) rename src/NHibernate.Test/NHSpecificTest/{NH2204 => NH1316}/Fixture.cs (89%) create mode 100644 src/NHibernate.Test/NHSpecificTest/NH1316/Mappings.hbm.xml create mode 100644 src/NHibernate.Test/NHSpecificTest/NH1316/Model.cs delete mode 100644 src/NHibernate.Test/NHSpecificTest/NH2204/Mappings.hbm.xml delete mode 100644 src/NHibernate.Test/NHSpecificTest/NH2204/Model.cs diff --git a/src/NHibernate.Test/Async/NHSpecificTest/NH2204/Fixture.cs b/src/NHibernate.Test/Async/NHSpecificTest/NH1316/Fixture.cs similarity index 91% rename from src/NHibernate.Test/Async/NHSpecificTest/NH2204/Fixture.cs rename to src/NHibernate.Test/Async/NHSpecificTest/NH1316/Fixture.cs index 258b6ad29ef..6807ee65e2d 100644 --- a/src/NHibernate.Test/Async/NHSpecificTest/NH2204/Fixture.cs +++ b/src/NHibernate.Test/Async/NHSpecificTest/NH1316/Fixture.cs @@ -14,7 +14,7 @@ using NHibernate.Dialect; -namespace NHibernate.Test.NHSpecificTest.NH2204 +namespace NHibernate.Test.NHSpecificTest.NH1316 { using System.Threading.Tasks; [TestFixture] @@ -66,7 +66,7 @@ protected override void OnTearDown() [Test] - public async Task KnownFailure_Correct_Id_Returned_When_Using_TriggerAsync() + public async Task Correct_Id_Returned_When_Using_TriggerAsync() { //We expected this test to fail - if the problem has been fixed, clean-up the test. var entity1 = new Parent {Name = "Parent1_0"}; // when saved this entity should have the id of 1 @@ -100,13 +100,10 @@ public async Task KnownFailure_Correct_Id_Returned_When_Using_TriggerAsync() await (s.SaveAsync(entity3)); await (s.FlushAsync()); - Warn.Unless( + Assert.That( entity3.Id, Is.EqualTo(3), "oh uh - it would appear that lastval() is not our friend when a trigger updates other sequences."); - - // now would be a good time to look at the data in the tables and see that they have the IDs as expected - // which are not the same as those returned by nhibernate } } } diff --git a/src/NHibernate.Test/NHSpecificTest/NH2204/Fixture.cs b/src/NHibernate.Test/NHSpecificTest/NH1316/Fixture.cs similarity index 89% rename from src/NHibernate.Test/NHSpecificTest/NH2204/Fixture.cs rename to src/NHibernate.Test/NHSpecificTest/NH1316/Fixture.cs index aa937b312b6..954b00c3e44 100644 --- a/src/NHibernate.Test/NHSpecificTest/NH2204/Fixture.cs +++ b/src/NHibernate.Test/NHSpecificTest/NH1316/Fixture.cs @@ -1,10 +1,8 @@ using System; -using System.Data.Common; -using NUnit.Framework; using NHibernate.Dialect; +using NUnit.Framework; - -namespace NHibernate.Test.NHSpecificTest.NH2204 +namespace NHibernate.Test.NHSpecificTest.NH1316 { [TestFixture] public class Fixture : BugTestCase @@ -55,7 +53,7 @@ protected override void OnTearDown() [Test] - public void KnownFailure_Correct_Id_Returned_When_Using_Trigger() + public void Correct_Id_Returned_When_Using_Trigger() { //We expected this test to fail - if the problem has been fixed, clean-up the test. var entity1 = new Parent {Name = "Parent1_0"}; // when saved this entity should have the id of 1 @@ -89,13 +87,10 @@ public void KnownFailure_Correct_Id_Returned_When_Using_Trigger() s.Save(entity3); s.Flush(); - Warn.Unless( + Assert.That( entity3.Id, Is.EqualTo(3), "oh uh - it would appear that lastval() is not our friend when a trigger updates other sequences."); - - // now would be a good time to look at the data in the tables and see that they have the IDs as expected - // which are not the same as those returned by nhibernate } } } diff --git a/src/NHibernate.Test/NHSpecificTest/NH1316/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH1316/Mappings.hbm.xml new file mode 100644 index 00000000000..40777a2bee0 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH1316/Mappings.hbm.xml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/NHibernate.Test/NHSpecificTest/NH1316/Model.cs b/src/NHibernate.Test/NHSpecificTest/NH1316/Model.cs new file mode 100644 index 00000000000..a7b8879ddc9 --- /dev/null +++ b/src/NHibernate.Test/NHSpecificTest/NH1316/Model.cs @@ -0,0 +1,19 @@ +using System; + +namespace NHibernate.Test.NHSpecificTest.NH1316 +{ + public class Parent + { + public virtual int Id { get; set; } + public virtual string Name { get; set; } + } + + public class ParentHistory + { + public virtual int HistId { get; set; } + public virtual DateTime HistWhen { get; set; } + + public virtual int Id { get; set; } + public virtual string Name { get; set; } + } +} diff --git a/src/NHibernate.Test/NHSpecificTest/NH2204/Mappings.hbm.xml b/src/NHibernate.Test/NHSpecificTest/NH2204/Mappings.hbm.xml deleted file mode 100644 index e9f5d749abb..00000000000 --- a/src/NHibernate.Test/NHSpecificTest/NH2204/Mappings.hbm.xml +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/NHibernate.Test/NHSpecificTest/NH2204/Model.cs b/src/NHibernate.Test/NHSpecificTest/NH2204/Model.cs deleted file mode 100644 index ce2d849941d..00000000000 --- a/src/NHibernate.Test/NHSpecificTest/NH2204/Model.cs +++ /dev/null @@ -1 +0,0 @@ -using System; namespace NHibernate.Test.NHSpecificTest.NH2204 { public class Parent { public virtual int Id { get; set; } public virtual string Name { get; set; } } public class ParentHistory { public virtual int HistId { get; set; } public virtual DateTime HistWhen { get; set; } public virtual int Id { get; set; } public virtual string Name { get; set; } } } diff --git a/src/NHibernate/Dialect/Dialect.cs b/src/NHibernate/Dialect/Dialect.cs index 0010d073232..15437a40499 100644 --- a/src/NHibernate/Dialect/Dialect.cs +++ b/src/NHibernate/Dialect/Dialect.cs @@ -976,7 +976,7 @@ public virtual bool HasDataTypeInIdentityColumn } /// - /// Provided we , then attch the + /// Provided we , then attach the /// "select identity" clause to the insert statement. /// /// The insert command @@ -990,6 +990,22 @@ public virtual SqlString AppendIdentitySelectToInsert(SqlString insertString) return insertString; } + /// + /// Provided we , then attach the + /// "select identity" clause to the insert statement. + /// + /// The insert command + /// The identifier name + /// + /// The insert command with any necessary identity select clause attached. + /// Note, if == false then + /// the insert-string should be returned without modification. + /// + public virtual SqlString AppendIdentitySelectToInsert(SqlString insertString, string identifierColumnName) + { + return AppendIdentitySelectToInsert(insertString); + } + /// /// Get the select command to use to retrieve the last generated IDENTITY /// value for a particular table. diff --git a/src/NHibernate/Dialect/PostgreSQL81Dialect.cs b/src/NHibernate/Dialect/PostgreSQL81Dialect.cs index c131396f646..81b062bafdd 100644 --- a/src/NHibernate/Dialect/PostgreSQL81Dialect.cs +++ b/src/NHibernate/Dialect/PostgreSQL81Dialect.cs @@ -109,6 +109,11 @@ public override SqlString AppendIdentitySelectToInsert(SqlString insertSql) return insertSql.Append("; " + IdentitySelectString); } + public override SqlString AppendIdentitySelectToInsert(SqlString insertString, string identifierColumnName) + { + return insertString.Append(" returning ").Append(identifierColumnName); + } + public override bool SupportsInsertSelectIdentity { get { return true; } diff --git a/src/NHibernate/Dialect/PostgreSQLDialect.cs b/src/NHibernate/Dialect/PostgreSQLDialect.cs index 2f0c9a9574a..d9664a3c523 100644 --- a/src/NHibernate/Dialect/PostgreSQLDialect.cs +++ b/src/NHibernate/Dialect/PostgreSQLDialect.cs @@ -175,7 +175,7 @@ public override string GetDropSequenceString(string sequenceName) public override SqlString AddIdentifierOutParameterToInsert(SqlString insertString, string identifierColumnName, string parameterName) { - return insertString.Append(" returning " + identifierColumnName); + return insertString.Append(" returning ").Append(identifierColumnName); } public override InsertGeneratedIdentifierRetrievalMethod InsertGeneratedIdentifierRetrievalMethod diff --git a/src/NHibernate/Id/IdentityGenerator.cs b/src/NHibernate/Id/IdentityGenerator.cs index 8c04c9a0f31..22d736b81d2 100644 --- a/src/NHibernate/Id/IdentityGenerator.cs +++ b/src/NHibernate/Id/IdentityGenerator.cs @@ -60,7 +60,7 @@ public InsertSelectDelegate(IPostInsertIdentityPersister persister, ISessionFact public override IdentifierGeneratingInsert PrepareIdentifierGeneratingInsert() { - InsertSelectIdentityInsert insert = new InsertSelectIdentityInsert(factory); + var insert = new InsertSelectIdentityInsert(factory, persister.RootTableKeyColumnNames[0]); insert.AddIdentityColumn(persister.RootTableKeyColumnNames[0]); return insert; } diff --git a/src/NHibernate/Id/Insert/InsertSelectIdentityInsert.cs b/src/NHibernate/Id/Insert/InsertSelectIdentityInsert.cs index a2fe5015e9c..c42517eeac1 100644 --- a/src/NHibernate/Id/Insert/InsertSelectIdentityInsert.cs +++ b/src/NHibernate/Id/Insert/InsertSelectIdentityInsert.cs @@ -1,3 +1,4 @@ +using System; using NHibernate.Engine; using NHibernate.SqlCommand; @@ -10,14 +11,24 @@ namespace NHibernate.Id.Insert /// public class InsertSelectIdentityInsert : IdentifierGeneratingInsert { + private readonly string _identifierColumnName; + + //Since v5.2 + [Obsolete("Please use constructor accepting identifierColumnName parameter.")] public InsertSelectIdentityInsert(ISessionFactoryImplementor factory) : base(factory) { } + public InsertSelectIdentityInsert(ISessionFactoryImplementor factory, string identifierColumnName) + : base(factory) + { + _identifierColumnName = identifierColumnName; + } + public override SqlString ToSqlString() { - return Dialect.AppendIdentitySelectToInsert(base.ToSqlString()); + return Dialect.AppendIdentitySelectToInsert(base.ToSqlString(), _identifierColumnName); } } } diff --git a/src/NHibernate/Id/Insert/ReturningIdentifierInsert.cs b/src/NHibernate/Id/Insert/ReturningIdentifierInsert.cs index ad2ad2f59cd..2143c211130 100644 --- a/src/NHibernate/Id/Insert/ReturningIdentifierInsert.cs +++ b/src/NHibernate/Id/Insert/ReturningIdentifierInsert.cs @@ -15,8 +15,8 @@ public class ReturningIdentifierInsert : NoCommentsInsert private readonly string identifierColumnName; private readonly string returnParameterName; - public ReturningIdentifierInsert(ISessionFactoryImplementor factory, string identifierColumnName, - string returnParameterName) : base(factory) + public ReturningIdentifierInsert(ISessionFactoryImplementor factory, string identifierColumnName, string returnParameterName) + : base(factory) { this.returnParameterName = returnParameterName; this.identifierColumnName = identifierColumnName;