Skip to content

Commit af319b6

Browse files
Allow generic dictionaries for dynamic entities (#1767)
Follow-up to #755
1 parent 82f199e commit af319b6

File tree

8 files changed

+315
-18
lines changed

8 files changed

+315
-18
lines changed

doc/reference/modules/persistent_classes.xml

+20-6
Original file line numberDiff line numberDiff line change
@@ -254,12 +254,12 @@ namespace Eg
254254
<para>
255255
Persistent entities don't necessarily have to be represented as POCO classes
256256
at runtime. NHibernate also supports dynamic models
257-
(using <literal>Dictionaries</literal> of <literal>Dictionary</literal>s at runtime) . With this approach, you don't
257+
(using <literal>Dictionaries</literal>). With this approach, you don't
258258
write persistent classes, only mapping files.
259259
</para>
260260

261261
<para>
262-
The following examples demonstrates the representation using <literal>Map</literal>s (Dictionary).
262+
The following examples demonstrates the representation using <literal>Dictionaries</literal>.
263263
First, in the mapping file, an <literal>entity-name</literal> has to be declared
264264
instead of a class name:
265265
</para>
@@ -305,7 +305,7 @@ namespace Eg
305305
</para>
306306

307307
<para>
308-
At runtime we can work with <literal>Dictionaries</literal> of <literal>Dictionaries</literal>:
308+
At runtime we can work with <literal>Dictionaries</literal>:
309309
</para>
310310

311311
<programlisting><![CDATA[using(ISession s = OpenSession())
@@ -336,6 +336,20 @@ using(ITransaction tx = s.BeginTransaction())
336336
to the NHibernate mapping, the database schema can easily be normalized and sound,
337337
allowing to add a proper domain model implementation on top later on.
338338
</para>
339+
340+
<para>
341+
A loaded dynamic entity can be manipulated as an <literal>IDictionary</literal> or
342+
<literal>IDictionary&lt;string, object&gt;</literal>.
343+
</para>
344+
345+
<programlisting><![CDATA[using(ISession s = OpenSession())
346+
using(ITransaction tx = s.BeginTransaction())
347+
{
348+
var customers = s
349+
.CreateQuery("from Customer")
350+
.List<IDictionary<string, object>>();
351+
...
352+
}]]></programlisting>
339353
</sect1>
340354

341355
<sect1 id="persistent-classes-tuplizers" revision="1">
@@ -357,9 +371,9 @@ using(ITransaction tx = s.BeginTransaction())
357371
</para>
358372

359373
<para>
360-
Users may also plug in their own tuplizers. Perhaps you require that a <literal>System.Collections.IDictionary</literal>
361-
implementation other than <literal>System.Collections.Hashtable</literal> be used while in the
362-
dynamic-map entity-mode; or perhaps you need to define a different proxy generation strategy
374+
Users may also plug in their own tuplizers. Perhaps you require that a <literal>IDictionary</literal>
375+
implementation other than <literal>System.Collections.Generic.Dictionary&lt;string, object&gt;</literal>
376+
is used while in the dynamic-map entity-mode; or perhaps you need to define a different proxy generation strategy
363377
than the one used by default. Both would be achieved by defining a custom tuplizer
364378
implementation. Tuplizers definitions are attached to the entity or component mapping they
365379
are meant to manage. Going back to the example of our customer entity:

src/NHibernate.Test/Async/EntityModeTest/Map/Basic/DynamicClassFixture.cs

+82-4
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010

1111
using System.Collections;
1212
using System.Collections.Generic;
13-
using NHibernate.Cfg;
14-
using NHibernate.Engine;
13+
using Antlr.Runtime.Misc;
1514
using NUnit.Framework;
1615
using NHibernate.Criterion;
1716

@@ -29,7 +28,7 @@ protected override string MappingsAssembly
2928

3029
protected override IList Mappings
3130
{
32-
get { return new string[] {"EntityModeTest.Map.Basic.ProductLine.hbm.xml"}; }
31+
get { return new[] {"EntityModeTest.Map.Basic.ProductLine.hbm.xml"}; }
3332
}
3433

3534
public delegate IDictionary SingleCarQueryDelegate(ISession session);
@@ -110,5 +109,84 @@ public async Task ShouldWorkWithCriteriaAsync()
110109
await (t.CommitAsync(cancellationToken));
111110
}
112111
}
112+
113+
[Test]
114+
public async Task ShouldWorkWithHQLAndGenericsAsync()
115+
{
116+
await (TestLazyDynamicClassAsync(
117+
s => s.CreateQuery("from ProductLine pl order by pl.Description").UniqueResult<IDictionary<string, object>>(),
118+
s => s.CreateQuery("from Model m").List<IDictionary<string, object>>()));
119+
}
120+
121+
[Test]
122+
public async Task ShouldWorkWithCriteriaAndGenericsAsync()
123+
{
124+
await (TestLazyDynamicClassAsync(
125+
s => s.CreateCriteria("ProductLine").AddOrder(Order.Asc("Description")).UniqueResult<IDictionary<string, object>>(),
126+
s => s.CreateCriteria("Model").List<IDictionary<string, object>>()));
127+
}
128+
129+
public async Task TestLazyDynamicClassAsync(
130+
Func<ISession, IDictionary<string, object>> singleCarQueryHandler,
131+
Func<ISession, IList<IDictionary<string, object>>> allModelQueryHandler, CancellationToken cancellationToken = default(CancellationToken))
132+
{
133+
using (var s = OpenSession())
134+
using (var t = s.BeginTransaction())
135+
{
136+
var cars = new Dictionary<string, object> { ["Description"] = "Cars" };
137+
138+
var monaro = new Dictionary<string, object>
139+
{
140+
["ProductLine"] = cars,
141+
["Name"] = "Monaro",
142+
["Description"] = "Holden Monaro"
143+
};
144+
145+
var hsv = new Dictionary<string, object>
146+
{
147+
["ProductLine"] = cars,
148+
["Name"] = "hsv",
149+
["Description"] = "Holden hsv"
150+
};
151+
152+
var models = new List<IDictionary<string, object>> {monaro, hsv};
153+
154+
cars["Models"] = models;
155+
156+
await (s.SaveAsync("ProductLine", cars, cancellationToken));
157+
await (t.CommitAsync(cancellationToken));
158+
}
159+
160+
using (var s = OpenSession())
161+
using (var t = s.BeginTransaction())
162+
{
163+
var cars = singleCarQueryHandler(s);
164+
var models = (IList<object>) cars["Models"];
165+
Assert.That(NHibernateUtil.IsInitialized(models), Is.False);
166+
Assert.That(models.Count, Is.EqualTo(2));
167+
Assert.That(NHibernateUtil.IsInitialized(models), Is.True);
168+
s.Clear();
169+
var list = allModelQueryHandler(s);
170+
foreach (var dic in list)
171+
{
172+
Assert.That(NHibernateUtil.IsInitialized(dic["ProductLine"]), Is.False);
173+
}
174+
var model = list[0];
175+
Assert.That(((IList<object>) ((IDictionary<string, object>) model["ProductLine"])["Models"]).Contains(model), Is.True);
176+
s.Clear();
177+
178+
await (t.CommitAsync(cancellationToken));
179+
}
180+
}
181+
182+
protected override void OnTearDown()
183+
{
184+
using (var s = OpenSession())
185+
using (var t = s.BeginTransaction())
186+
{
187+
s.Delete("from ProductLine");
188+
t.Commit();
189+
}
190+
}
113191
}
114-
}
192+
}

src/NHibernate.Test/EntityModeTest/Map/Basic/DynamicClassFixture.cs

+82-4
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
using System.Collections;
22
using System.Collections.Generic;
3-
using NHibernate.Cfg;
4-
using NHibernate.Engine;
3+
using Antlr.Runtime.Misc;
54
using NUnit.Framework;
65
using NHibernate.Criterion;
76

@@ -17,7 +16,7 @@ protected override string MappingsAssembly
1716

1817
protected override IList Mappings
1918
{
20-
get { return new string[] {"EntityModeTest.Map.Basic.ProductLine.hbm.xml"}; }
19+
get { return new[] {"EntityModeTest.Map.Basic.ProductLine.hbm.xml"}; }
2120
}
2221

2322
public delegate IDictionary SingleCarQueryDelegate(ISession session);
@@ -98,5 +97,84 @@ public void TestLazyDynamicClass(SingleCarQueryDelegate singleCarQueryHandler, A
9897
t.Commit();
9998
}
10099
}
100+
101+
[Test]
102+
public void ShouldWorkWithHQLAndGenerics()
103+
{
104+
TestLazyDynamicClass(
105+
s => s.CreateQuery("from ProductLine pl order by pl.Description").UniqueResult<IDictionary<string, object>>(),
106+
s => s.CreateQuery("from Model m").List<IDictionary<string, object>>());
107+
}
108+
109+
[Test]
110+
public void ShouldWorkWithCriteriaAndGenerics()
111+
{
112+
TestLazyDynamicClass(
113+
s => s.CreateCriteria("ProductLine").AddOrder(Order.Asc("Description")).UniqueResult<IDictionary<string, object>>(),
114+
s => s.CreateCriteria("Model").List<IDictionary<string, object>>());
115+
}
116+
117+
public void TestLazyDynamicClass(
118+
Func<ISession, IDictionary<string, object>> singleCarQueryHandler,
119+
Func<ISession, IList<IDictionary<string, object>>> allModelQueryHandler)
120+
{
121+
using (var s = OpenSession())
122+
using (var t = s.BeginTransaction())
123+
{
124+
var cars = new Dictionary<string, object> { ["Description"] = "Cars" };
125+
126+
var monaro = new Dictionary<string, object>
127+
{
128+
["ProductLine"] = cars,
129+
["Name"] = "Monaro",
130+
["Description"] = "Holden Monaro"
131+
};
132+
133+
var hsv = new Dictionary<string, object>
134+
{
135+
["ProductLine"] = cars,
136+
["Name"] = "hsv",
137+
["Description"] = "Holden hsv"
138+
};
139+
140+
var models = new List<IDictionary<string, object>> {monaro, hsv};
141+
142+
cars["Models"] = models;
143+
144+
s.Save("ProductLine", cars);
145+
t.Commit();
146+
}
147+
148+
using (var s = OpenSession())
149+
using (var t = s.BeginTransaction())
150+
{
151+
var cars = singleCarQueryHandler(s);
152+
var models = (IList<object>) cars["Models"];
153+
Assert.That(NHibernateUtil.IsInitialized(models), Is.False);
154+
Assert.That(models.Count, Is.EqualTo(2));
155+
Assert.That(NHibernateUtil.IsInitialized(models), Is.True);
156+
s.Clear();
157+
var list = allModelQueryHandler(s);
158+
foreach (var dic in list)
159+
{
160+
Assert.That(NHibernateUtil.IsInitialized(dic["ProductLine"]), Is.False);
161+
}
162+
var model = list[0];
163+
Assert.That(((IList<object>) ((IDictionary<string, object>) model["ProductLine"])["Models"]).Contains(model), Is.True);
164+
s.Clear();
165+
166+
t.Commit();
167+
}
168+
}
169+
170+
protected override void OnTearDown()
171+
{
172+
using (var s = OpenSession())
173+
using (var t = s.BeginTransaction())
174+
{
175+
s.Delete("from ProductLine");
176+
t.Commit();
177+
}
178+
}
101179
}
102-
}
180+
}

src/NHibernate/Proxy/Map/MapLazyInitializer.cs

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections;
3+
using System.Collections.Generic;
34
using NHibernate.Engine;
45

56
namespace NHibernate.Proxy.Map
@@ -16,6 +17,8 @@ public IDictionary Map
1617
get { return (IDictionary) GetImplementation(); }
1718
}
1819

20+
public IDictionary<string, object> GenericMap => (IDictionary<string, object>) GetImplementation();
21+
1922
public override System.Type PersistentClass
2023
{
2124
get

src/NHibernate/Proxy/Map/MapProxy.cs

+69-1
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
using System;
22
using System.Collections;
3+
using System.Collections.Generic;
34

45
namespace NHibernate.Proxy.Map
56
{
67
/// <summary> Proxy for "dynamic-map" entity representations. </summary>
78
[Serializable]
8-
public class MapProxy : INHibernateProxy, IDictionary
9+
public class MapProxy : INHibernateProxy, IDictionary, IDictionary<string, object>
910
{
1011
private readonly MapLazyInitializer li;
1112

@@ -114,5 +115,72 @@ public IEnumerator GetEnumerator()
114115
}
115116

116117
#endregion
118+
119+
#region IDictionary<string, object> Members
120+
121+
bool IDictionary<string, object>.ContainsKey(string key)
122+
{
123+
return li.GenericMap.ContainsKey(key);
124+
}
125+
126+
void IDictionary<string, object>.Add(string key, object value)
127+
{
128+
li.GenericMap.Add(key, value);
129+
}
130+
131+
bool IDictionary<string, object>.Remove(string key)
132+
{
133+
return li.GenericMap.Remove(key);
134+
}
135+
136+
bool IDictionary<string, object>.TryGetValue(string key, out object value)
137+
{
138+
return li.GenericMap.TryGetValue(key, out value);
139+
}
140+
141+
object IDictionary<string, object>.this[string key]
142+
{
143+
get => li.GenericMap[key];
144+
set => li.GenericMap[key] = value;
145+
}
146+
147+
ICollection<object> IDictionary<string, object>.Values => li.GenericMap.Values;
148+
149+
ICollection<string> IDictionary<string, object>.Keys => li.GenericMap.Keys;
150+
151+
#endregion
152+
153+
#region ICollection<KeyValuePair<string, object>> Members
154+
155+
void ICollection<KeyValuePair<string, object>>.Add(KeyValuePair<string, object> item)
156+
{
157+
li.GenericMap.Add(item);
158+
}
159+
160+
bool ICollection<KeyValuePair<string, object>>.Contains(KeyValuePair<string, object> item)
161+
{
162+
return li.GenericMap.Contains(item);
163+
}
164+
165+
void ICollection<KeyValuePair<string, object>>.CopyTo(KeyValuePair<string, object>[] array, int arrayIndex)
166+
{
167+
li.GenericMap.CopyTo(array, arrayIndex);
168+
}
169+
170+
bool ICollection<KeyValuePair<string, object>>.Remove(KeyValuePair<string, object> item)
171+
{
172+
return li.GenericMap.Remove(item);
173+
}
174+
175+
#endregion
176+
177+
#region IEnumerable<KeyValuePair<string, object>> Members
178+
179+
IEnumerator<KeyValuePair<string, object>> IEnumerable<KeyValuePair<string, object>>.GetEnumerator()
180+
{
181+
return li.GenericMap.GetEnumerator();
182+
}
183+
184+
#endregion
117185
}
118186
}

0 commit comments

Comments
 (0)