From 3b3504cbf844184734363f3a2a36f0f3c90e2e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericDelaporte@users.noreply.github.com> Date: Thu, 21 Jun 2018 18:23:00 +0200 Subject: [PATCH 1/3] Allow generic dictionaries for dynamic entities Follow-up to #755 --- doc/reference/modules/persistent_classes.xml | 26 ++++-- .../Map/Basic/DynamicClassFixture.cs | 86 ++++++++++++++++++- .../Map/Basic/DynamicClassFixture.cs | 86 ++++++++++++++++++- .../Proxy/Map/MapLazyInitializer.cs | 3 + src/NHibernate/Proxy/Map/MapProxy.cs | 70 ++++++++++++++- .../Tuple/DynamicMapInstantiator.cs | 2 +- 6 files changed, 257 insertions(+), 16 deletions(-) diff --git a/doc/reference/modules/persistent_classes.xml b/doc/reference/modules/persistent_classes.xml index 57d279be08e..92355033151 100644 --- a/doc/reference/modules/persistent_classes.xml +++ b/doc/reference/modules/persistent_classes.xml @@ -254,12 +254,12 @@ namespace Eg Persistent entities don't necessarily have to be represented as POCO classes at runtime. NHibernate also supports dynamic models - (using Dictionaries of Dictionarys at runtime) . With this approach, you don't + (using Dictionaries). With this approach, you don't write persistent classes, only mapping files. - The following examples demonstrates the representation using Maps (Dictionary). + The following examples demonstrates the representation using Dictionaries. First, in the mapping file, an entity-name has to be declared instead of a class name: @@ -305,7 +305,7 @@ namespace Eg - At runtime we can work with Dictionaries of Dictionaries: + At runtime we can work with Dictionaries: + + + A loaded dynamic entity can be manipulated as an IDictionary or + IDictionary<string, object>. + + + >(); + ... +}]]> @@ -357,9 +371,9 @@ using(ITransaction tx = s.BeginTransaction()) - Users may also plug in their own tuplizers. Perhaps you require that a System.Collections.IDictionary - implementation other than System.Collections.Hashtable be used while in the - dynamic-map entity-mode; or perhaps you need to define a different proxy generation strategy + Users may also plug in their own tuplizers. Perhaps you require that a IDictionary + implementation other than System.Collections.Generic.Dictionary<string, object> + is used while in the dynamic-map entity-mode; or perhaps you need to define a different proxy generation strategy than the one used by default. Both would be achieved by defining a custom tuplizer implementation. Tuplizers definitions are attached to the entity or component mapping they are meant to manage. Going back to the example of our customer entity: diff --git a/src/NHibernate.Test/Async/EntityModeTest/Map/Basic/DynamicClassFixture.cs b/src/NHibernate.Test/Async/EntityModeTest/Map/Basic/DynamicClassFixture.cs index 02635f0ebde..70b481243c7 100644 --- a/src/NHibernate.Test/Async/EntityModeTest/Map/Basic/DynamicClassFixture.cs +++ b/src/NHibernate.Test/Async/EntityModeTest/Map/Basic/DynamicClassFixture.cs @@ -10,8 +10,7 @@ using System.Collections; using System.Collections.Generic; -using NHibernate.Cfg; -using NHibernate.Engine; +using Antlr.Runtime.Misc; using NUnit.Framework; using NHibernate.Criterion; @@ -29,7 +28,7 @@ protected override string MappingsAssembly protected override IList Mappings { - get { return new string[] {"EntityModeTest.Map.Basic.ProductLine.hbm.xml"}; } + get { return new[] {"EntityModeTest.Map.Basic.ProductLine.hbm.xml"}; } } public delegate IDictionary SingleCarQueryDelegate(ISession session); @@ -110,5 +109,84 @@ public async Task ShouldWorkWithCriteriaAsync() await (t.CommitAsync(cancellationToken)); } } + + [Test] + public async Task ShouldWorkWithHQLAndGenericsAsync() + { + await (TestLazyDynamicClassAsync( + s => s.CreateQuery("from ProductLine pl order by pl.Description").UniqueResult>(), + s => s.CreateQuery("from Model m").List>())); + } + + [Test] + public async Task ShouldWorkWithCriteriaAndGenericsAsync() + { + await (TestLazyDynamicClassAsync( + s => s.CreateCriteria("ProductLine").AddOrder(Order.Asc("Description")).UniqueResult>(), + s => s.CreateCriteria("Model").List>())); + } + + public async Task TestLazyDynamicClassAsync( + Func> singleCarQueryHandler, + Func>> allModelQueryHandler, CancellationToken cancellationToken = default(CancellationToken)) + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var cars = new Dictionary { ["Description"] = "Cars" }; + + var monaro = new Dictionary + { + ["ProductLine"] = cars, + ["Name"] = "Monaro", + ["Description"] = "Holden Monaro" + }; + + var hsv = new Dictionary + { + ["ProductLine"] = cars, + ["Name"] = "hsv", + ["Description"] = "Holden hsv" + }; + + var models = new List> {monaro, hsv}; + + cars["Models"] = models; + + await (s.SaveAsync("ProductLine", cars, cancellationToken)); + await (t.CommitAsync(cancellationToken)); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var cars = singleCarQueryHandler(s); + var models = (IList) cars["Models"]; + Assert.That(NHibernateUtil.IsInitialized(models), Is.False); + Assert.That(models.Count, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(models), Is.True); + s.Clear(); + var list = allModelQueryHandler(s); + foreach (var dic in list) + { + Assert.That(NHibernateUtil.IsInitialized(dic["ProductLine"]), Is.False); + } + var model = list[0]; + Assert.That(((IList) ((IDictionary) model["ProductLine"])["Models"]).Contains(model), Is.True); + s.Clear(); + + await (t.CommitAsync(cancellationToken)); + } + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.Delete("from ProductLine"); + t.Commit(); + } + } } -} \ No newline at end of file +} diff --git a/src/NHibernate.Test/EntityModeTest/Map/Basic/DynamicClassFixture.cs b/src/NHibernate.Test/EntityModeTest/Map/Basic/DynamicClassFixture.cs index 7d2185493a3..0c232c9290d 100644 --- a/src/NHibernate.Test/EntityModeTest/Map/Basic/DynamicClassFixture.cs +++ b/src/NHibernate.Test/EntityModeTest/Map/Basic/DynamicClassFixture.cs @@ -1,7 +1,6 @@ using System.Collections; using System.Collections.Generic; -using NHibernate.Cfg; -using NHibernate.Engine; +using Antlr.Runtime.Misc; using NUnit.Framework; using NHibernate.Criterion; @@ -17,7 +16,7 @@ protected override string MappingsAssembly protected override IList Mappings { - get { return new string[] {"EntityModeTest.Map.Basic.ProductLine.hbm.xml"}; } + get { return new[] {"EntityModeTest.Map.Basic.ProductLine.hbm.xml"}; } } public delegate IDictionary SingleCarQueryDelegate(ISession session); @@ -98,5 +97,84 @@ public void TestLazyDynamicClass(SingleCarQueryDelegate singleCarQueryHandler, A t.Commit(); } } + + [Test] + public void ShouldWorkWithHQLAndGenerics() + { + TestLazyDynamicClass( + s => s.CreateQuery("from ProductLine pl order by pl.Description").UniqueResult>(), + s => s.CreateQuery("from Model m").List>()); + } + + [Test] + public void ShouldWorkWithCriteriaAndGenerics() + { + TestLazyDynamicClass( + s => s.CreateCriteria("ProductLine").AddOrder(Order.Asc("Description")).UniqueResult>(), + s => s.CreateCriteria("Model").List>()); + } + + public void TestLazyDynamicClass( + Func> singleCarQueryHandler, + Func>> allModelQueryHandler) + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var cars = new Dictionary { ["Description"] = "Cars" }; + + var monaro = new Dictionary + { + ["ProductLine"] = cars, + ["Name"] = "Monaro", + ["Description"] = "Holden Monaro" + }; + + var hsv = new Dictionary + { + ["ProductLine"] = cars, + ["Name"] = "hsv", + ["Description"] = "Holden hsv" + }; + + var models = new List> {monaro, hsv}; + + cars["Models"] = models; + + s.Save("ProductLine", cars); + t.Commit(); + } + + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + var cars = singleCarQueryHandler(s); + var models = (IList) cars["Models"]; + Assert.That(NHibernateUtil.IsInitialized(models), Is.False); + Assert.That(models.Count, Is.EqualTo(2)); + Assert.That(NHibernateUtil.IsInitialized(models), Is.True); + s.Clear(); + var list = allModelQueryHandler(s); + foreach (var dic in list) + { + Assert.That(NHibernateUtil.IsInitialized(dic["ProductLine"]), Is.False); + } + var model = list[0]; + Assert.That(((IList) ((IDictionary) model["ProductLine"])["Models"]).Contains(model), Is.True); + s.Clear(); + + t.Commit(); + } + } + + protected override void OnTearDown() + { + using (var s = OpenSession()) + using (var t = s.BeginTransaction()) + { + s.Delete("from ProductLine"); + t.Commit(); + } + } } -} \ No newline at end of file +} diff --git a/src/NHibernate/Proxy/Map/MapLazyInitializer.cs b/src/NHibernate/Proxy/Map/MapLazyInitializer.cs index 48284d6d8d7..d827e99947b 100644 --- a/src/NHibernate/Proxy/Map/MapLazyInitializer.cs +++ b/src/NHibernate/Proxy/Map/MapLazyInitializer.cs @@ -1,5 +1,6 @@ using System; using System.Collections; +using System.Collections.Generic; using NHibernate.Engine; namespace NHibernate.Proxy.Map @@ -16,6 +17,8 @@ public IDictionary Map get { return (IDictionary) GetImplementation(); } } + public IDictionary GenericMap => (IDictionary) GetImplementation(); + public override System.Type PersistentClass { get diff --git a/src/NHibernate/Proxy/Map/MapProxy.cs b/src/NHibernate/Proxy/Map/MapProxy.cs index b42aef37751..091fca875fe 100644 --- a/src/NHibernate/Proxy/Map/MapProxy.cs +++ b/src/NHibernate/Proxy/Map/MapProxy.cs @@ -1,11 +1,12 @@ using System; using System.Collections; +using System.Collections.Generic; namespace NHibernate.Proxy.Map { /// Proxy for "dynamic-map" entity representations. [Serializable] - public class MapProxy : INHibernateProxy, IDictionary + public class MapProxy : INHibernateProxy, IDictionary, IDictionary { private readonly MapLazyInitializer li; @@ -114,5 +115,72 @@ public IEnumerator GetEnumerator() } #endregion + + #region IDictionary Members + + bool IDictionary.ContainsKey(string key) + { + return li.GenericMap.ContainsKey(key); + } + + void IDictionary.Add(string key, object value) + { + li.GenericMap.Add(key, value); + } + + bool IDictionary.Remove(string key) + { + return li.GenericMap.Remove(key); + } + + bool IDictionary.TryGetValue(string key, out object value) + { + return li.GenericMap.TryGetValue(key, out value); + } + + object IDictionary.this[string key] + { + get => li.GenericMap[key]; + set => li.GenericMap[key] = value; + } + + ICollection IDictionary.Values => li.GenericMap.Values; + + ICollection IDictionary.Keys => li.GenericMap.Keys; + + #endregion + + #region ICollection> Members + + void ICollection>.Add(KeyValuePair item) + { + li.GenericMap.Add(item); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return li.GenericMap.Contains(item); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + li.GenericMap.CopyTo(array, arrayIndex); + } + + bool ICollection>.Remove(KeyValuePair item) + { + return li.GenericMap.Remove(item); + } + + #endregion + + #region IEnumerable> Members + + IEnumerator> IEnumerable>.GetEnumerator() + { + return li.GenericMap.GetEnumerator(); + } + + #endregion } } diff --git a/src/NHibernate/Tuple/DynamicMapInstantiator.cs b/src/NHibernate/Tuple/DynamicMapInstantiator.cs index 0a8cce0e4a9..a9878475ac3 100644 --- a/src/NHibernate/Tuple/DynamicMapInstantiator.cs +++ b/src/NHibernate/Tuple/DynamicMapInstantiator.cs @@ -50,7 +50,7 @@ public object Instantiate() protected virtual IDictionary GenerateMap() { - return new Hashtable(); + return new Dictionary(); } public bool IsInstance(object obj) From a40d7382043b00e1d33e79558e02351f6eeef1d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericDelaporte@users.noreply.github.com> Date: Fri, 22 Jun 2018 00:19:13 +0200 Subject: [PATCH 2/3] Use a new instantiator instead of changing the old one And obsolete the old one To be squashed. --- .../Tuple/DynamicEntityInstantiator.cs | 64 +++++++++++++++++++ .../Tuple/DynamicMapInstantiator.cs | 6 +- .../Tuple/Entity/DynamicMapEntityTuplizer.cs | 2 +- 3 files changed, 68 insertions(+), 4 deletions(-) create mode 100644 src/NHibernate/Tuple/DynamicEntityInstantiator.cs diff --git a/src/NHibernate/Tuple/DynamicEntityInstantiator.cs b/src/NHibernate/Tuple/DynamicEntityInstantiator.cs new file mode 100644 index 00000000000..c95aa007218 --- /dev/null +++ b/src/NHibernate/Tuple/DynamicEntityInstantiator.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using NHibernate.Mapping; + +namespace NHibernate.Tuple +{ + [Serializable] + public class DynamicEntityInstantiator : IInstantiator + { + public const string Key = "$type$"; + + private readonly string _entityName; + private readonly HashSet _isInstanceEntityNames = new HashSet(); + + public DynamicEntityInstantiator(PersistentClass mappingInfo) + { + _entityName = mappingInfo.EntityName; + _isInstanceEntityNames.Add(_entityName); + if (mappingInfo.HasSubclasses) + { + foreach (var subclassInfo in mappingInfo.SubclassClosureIterator) + _isInstanceEntityNames.Add(subclassInfo.EntityName); + } + } + + protected virtual IDictionary GenerateMap() + { + return new Dictionary(); + } + + #region IInstantiator Members + + public object Instantiate(object id) + { + return Instantiate(); + } + + public object Instantiate() + { + var map = GenerateMap(); + if (_entityName != null) + { + map[Key] = _entityName; + } + + return map; + } + + public bool IsInstance(object obj) + { + if (!(obj is IDictionary that)) + return false; + if (_entityName == null) + return true; + + var type = (string) that[Key]; + return type == null || _isInstanceEntityNames.Contains(type); + + } + + #endregion + } +} diff --git a/src/NHibernate/Tuple/DynamicMapInstantiator.cs b/src/NHibernate/Tuple/DynamicMapInstantiator.cs index a9878475ac3..7492dff0db5 100644 --- a/src/NHibernate/Tuple/DynamicMapInstantiator.cs +++ b/src/NHibernate/Tuple/DynamicMapInstantiator.cs @@ -5,6 +5,8 @@ namespace NHibernate.Tuple { + //Since v5.2 + [Obsolete("This class is not used and will be removed in a future version.")] [Serializable] public class DynamicMapInstantiator : IInstantiator { @@ -13,8 +15,6 @@ 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; @@ -50,7 +50,7 @@ public object Instantiate() protected virtual IDictionary GenerateMap() { - return new Dictionary(); + return new Hashtable(); } public bool IsInstance(object obj) diff --git a/src/NHibernate/Tuple/Entity/DynamicMapEntityTuplizer.cs b/src/NHibernate/Tuple/Entity/DynamicMapEntityTuplizer.cs index f06234f4e0c..9efd2af386f 100644 --- a/src/NHibernate/Tuple/Entity/DynamicMapEntityTuplizer.cs +++ b/src/NHibernate/Tuple/Entity/DynamicMapEntityTuplizer.cs @@ -57,7 +57,7 @@ protected override ISetter BuildPropertySetter(Mapping.Property mappedProperty, protected override IInstantiator BuildInstantiator(PersistentClass mappingInfo) { - return new DynamicMapInstantiator(mappingInfo); + return new DynamicEntityInstantiator(mappingInfo); } protected override IProxyFactory BuildProxyFactory(PersistentClass mappingInfo, IGetter idGetter, From eb949604c8c29cba6f915e48a8ba7457550507b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fr=C3=A9d=C3=A9ric=20Delaporte?= <12201973+fredericDelaporte@users.noreply.github.com> Date: Fri, 22 Jun 2018 13:13:44 +0200 Subject: [PATCH 3/3] Fix the new instantiator Coded too fast... To be squashed. --- src/NHibernate/Tuple/DynamicEntityInstantiator.cs | 12 ++---------- 1 file changed, 2 insertions(+), 10 deletions(-) diff --git a/src/NHibernate/Tuple/DynamicEntityInstantiator.cs b/src/NHibernate/Tuple/DynamicEntityInstantiator.cs index c95aa007218..35add21b177 100644 --- a/src/NHibernate/Tuple/DynamicEntityInstantiator.cs +++ b/src/NHibernate/Tuple/DynamicEntityInstantiator.cs @@ -1,5 +1,4 @@ using System; -using System.Collections; using System.Collections.Generic; using NHibernate.Mapping; @@ -39,10 +38,7 @@ public object Instantiate(object id) public object Instantiate() { var map = GenerateMap(); - if (_entityName != null) - { - map[Key] = _entityName; - } + map[Key] = _entityName; return map; } @@ -51,12 +47,8 @@ public bool IsInstance(object obj) { if (!(obj is IDictionary that)) return false; - if (_entityName == null) - return true; - - var type = (string) that[Key]; - return type == null || _isInstanceEntityNames.Contains(type); + return that.TryGetValue(Key, out var type) && _isInstanceEntityNames.Contains(type as string); } #endregion