Skip to content

Commit bfa9311

Browse files
fixup! Recompute hashcodes on deserialization
Re-implement Printer.ToString
1 parent f31fe59 commit bfa9311

File tree

14 files changed

+341
-143
lines changed

14 files changed

+341
-143
lines changed

src/NHibernate.Test/CacheTest/FilterKeyFixture.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,13 @@ public void ToStringIncludeAll()
2020
var f = new FilterImpl(Sfi.GetFilterDefinition(filterName));
2121
f.SetParameter("pLike", "so%");
2222
var fk = new FilterKey(f);
23-
Assert.That(fk.ToString(), Is.EqualTo("FilterKey[DescriptionLike{'pLike'='so%'}]"), "Like");
23+
Assert.That(fk.ToString(), Is.EqualTo("FilterKey[DescriptionLike['[pLike, so%]']]"), "Like");
2424

2525
filterName = "DescriptionEqualAndValueGT";
2626
f = new FilterImpl(Sfi.GetFilterDefinition(filterName));
2727
f.SetParameter("pDesc", "something").SetParameter("pValue", 10);
2828
fk = new FilterKey(f);
29-
Assert.That(fk.ToString(), Is.EqualTo("FilterKey[DescriptionEqualAndValueGT{'pDesc'='something', 'pValue'='10'}]"), "Value");
29+
Assert.That(fk.ToString(), Is.EqualTo("FilterKey[DescriptionEqualAndValueGT['[pDesc, something]', '[pValue, 10]']]"), "Value");
3030
}
3131

3232
[Test]

src/NHibernate.Test/CacheTest/QueryKeyFixture.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
1-
using System.Collections;
21
using System.Collections.Generic;
32
using NHibernate.Cache;
43
using NHibernate.Engine;
54
using NHibernate.Impl;
65
using NHibernate.SqlCommand;
6+
using NHibernate.Util;
77
using NUnit.Framework;
88

99
namespace NHibernate.Test.CacheTest
@@ -88,7 +88,7 @@ public void HashCodeWithFilters()
8888
public void NotEqualHashCodeWithFilters()
8989
{
9090
// GetHashCode semantic does not guarantee no collision may ever occur, but the algorithm should
91-
// generates different hashcodes for similar but inequal cases. These tests check that cache keys
91+
// generates different hashcodes for similar but unequal cases. These tests check that cache keys
9292
// for a query generated for different parameters values are no more equal.
9393
QueryKeyFilterDescLikeToCompare(out var qk, out var qk1, false);
9494
Assert.That(qk.GetHashCode(), Is.Not.EqualTo(qk1.GetHashCode()), "qk & qk1");
@@ -100,6 +100,20 @@ public void NotEqualHashCodeWithFilters()
100100
Assert.That(qk1.GetHashCode(), Is.Not.EqualTo(qvk1.GetHashCode()), "qk1 & qvk1");
101101
}
102102

103+
[Test]
104+
public void HashCodeWithFiltersAndSerialization()
105+
{
106+
QueryKeyFilterDescLikeToCompare(out var qk, out var qk1, true);
107+
var bytes = SerializationHelper.Serialize(qk);
108+
qk = (QueryKey) SerializationHelper.Deserialize(bytes);
109+
Assert.That(qk.GetHashCode(), Is.EqualTo(qk1.GetHashCode()), "Like");
110+
111+
QueryKeyFilterDescValueToCompare(out qk, out qk1, true);
112+
bytes = SerializationHelper.Serialize(qk);
113+
qk = (QueryKey) SerializationHelper.Deserialize(bytes);
114+
Assert.That(qk.GetHashCode(), Is.EqualTo(qk1.GetHashCode()), "Value");
115+
}
116+
103117
[Test]
104118
public void ToStringWithFilters()
105119
{

src/NHibernate.Test/DebugSessionFactory.cs

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,25 +30,31 @@ namespace NHibernate.Test
3030
/// it is used when testing to check that tests clean up after themselves.
3131
/// </summary>
3232
/// <remarks>Sessions opened from other sessions are not tracked.</remarks>
33+
[Serializable]
3334
public partial class DebugSessionFactory : ISessionFactoryImplementor
3435
{
36+
[NonSerialized]
37+
private DebugConnectionProvider _debugConnectionProvider;
38+
3539
/// <summary>
3640
/// The debug connection provider if configured for using it, <see langword="null"/> otherwise.
3741
/// Use <c>ActualFactory.ConnectionProvider</c> if needing unconditionally the connection provider, be
3842
/// it debug or not.
3943
/// </summary>
40-
public DebugConnectionProvider DebugConnectionProvider { get; }
44+
public DebugConnectionProvider DebugConnectionProvider
45+
=> _debugConnectionProvider ??
46+
(_debugConnectionProvider = ActualFactory.ConnectionProvider as DebugConnectionProvider);
4147
public ISessionFactoryImplementor ActualFactory { get; }
4248

4349
public EventListeners EventListeners => ((SessionFactoryImpl)ActualFactory).EventListeners;
4450

51+
[NonSerialized]
4552
private readonly ConcurrentBag<ISessionImplementor> _openedSessions = new ConcurrentBag<ISessionImplementor>();
4653
private static readonly ILog _log = LogManager.GetLogger(typeof(DebugSessionFactory).Assembly, typeof(TestCase));
4754

4855
public DebugSessionFactory(ISessionFactory actualFactory)
4956
{
5057
ActualFactory = (ISessionFactoryImplementor)actualFactory;
51-
DebugConnectionProvider = ActualFactory.ConnectionProvider as DebugConnectionProvider;
5258
}
5359

5460
#region Session tracking

src/NHibernate/Cache/CacheKey.cs

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
using System;
2+
using System.Runtime.Serialization;
23
using NHibernate.Engine;
34
using NHibernate.Type;
45

@@ -10,12 +11,15 @@ namespace NHibernate.Cache
1011
/// keys which do not properly implement equals()/hashCode().
1112
/// </summary>
1213
[Serializable]
13-
public class CacheKey
14+
public class CacheKey : IDeserializationCallback
1415
{
1516
private readonly object key;
1617
private readonly IType type;
1718
private readonly string entityOrRoleName;
18-
private readonly int hashCode;
19+
// hashcode may vary among processes, they cannot be stored and have to be re-computed after deserialization
20+
[NonSerialized]
21+
private int? _hashCode;
22+
private readonly ISessionFactoryImplementor _factory;
1923

2024
/// <summary>
2125
/// Construct a new key for a collection or entity instance.
@@ -31,7 +35,9 @@ public CacheKey(object id, IType type, string entityOrRoleName, ISessionFactoryI
3135
key = id;
3236
this.type = type;
3337
this.entityOrRoleName = entityOrRoleName;
34-
hashCode = type.GetHashCode(key, factory);
38+
_factory = factory;
39+
40+
_hashCode = GenerateHashCode();
3541
}
3642

3743
//Mainly for SysCache and Memcache
@@ -50,7 +56,22 @@ public override bool Equals(object obj)
5056

5157
public override int GetHashCode()
5258
{
53-
return hashCode;
59+
// If the object is put in a set or dictionary during deserialization, the hashcode will not yet be
60+
// computed. Compute the hashcode on the fly. So long as this happens only during deserialization, there
61+
// will be no thread safety issues. For the hashcode to be always defined after deserialization, the
62+
// deserialization callback is used.
63+
return _hashCode ?? GenerateHashCode();
64+
}
65+
66+
/// <inheritdoc />
67+
public void OnDeserialization(object sender)
68+
{
69+
_hashCode = GenerateHashCode();
70+
}
71+
72+
private int GenerateHashCode()
73+
{
74+
return type.GetHashCode(key, _factory);
5475
}
5576

5677
public object Key
@@ -63,4 +84,4 @@ public string EntityOrRoleName
6384
get { return entityOrRoleName; }
6485
}
6586
}
66-
}
87+
}

src/NHibernate/Cache/FilterKey.cs

Lines changed: 30 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
34
using NHibernate.Engine;
45
using NHibernate.Impl;
56
using NHibernate.Type;
@@ -11,29 +12,38 @@ namespace NHibernate.Cache
1112
public class FilterKey
1213
{
1314
private readonly string _filterName;
14-
private readonly Dictionary<string, TypedValue> _filterParameters = new Dictionary<string, TypedValue>();
15+
16+
// Sets and dictionaries are populated last during deserialization, causing them to be potentially empty
17+
// during the deserialization callback. This causes them to be unreliable when used in hashcode or equals
18+
// computations. These computations occur during the deserialization callback for example when another
19+
// serialized set or dictionary contain an instance of this class. (QueryKey was having such a dictionary
20+
// or set.)
21+
// So better serialize them as other structures, so long for Equals implementation which actually needs a
22+
// dictionary.
23+
private readonly KeyValuePair<string, TypedValue>[] _filterParameters;
1524

1625
// Since v5.2
1726
[Obsolete("Use overload taking a FilterImpl")]
1827
public FilterKey(string name, IEnumerable<KeyValuePair<string, object>> @params, IDictionary<string, IType> types)
1928
{
2029
_filterName = name;
21-
foreach (KeyValuePair<string, object> me in @params)
22-
{
23-
IType type = types[me.Key];
24-
_filterParameters[me.Key] = new TypedValue(type, me.Value);
25-
}
30+
_filterParameters = @params.Select(
31+
p => new KeyValuePair<string, TypedValue>(
32+
p.Key,
33+
new TypedValue(types[p.Key], p.Value))).ToArray();
2634
}
2735

2836
public FilterKey(FilterImpl filter)
2937
{
3038
var types = filter.FilterDefinition.ParameterTypes;
3139
_filterName = filter.Name;
32-
foreach (var me in filter.Parameters)
33-
{
34-
var type = types[me.Key];
35-
_filterParameters[me.Key] = new TypedValue(type, me.Value, filter.GetParameterSpan(me.Key) != null);
36-
}
40+
_filterParameters = filter.Parameters.Select(
41+
p => new KeyValuePair<string, TypedValue>(
42+
p.Key,
43+
new TypedValue(
44+
types[p.Key],
45+
p.Value,
46+
filter.GetParameterSpan(p.Key) != null))).ToArray();
3747
}
3848

3949
public override int GetHashCode()
@@ -47,15 +57,16 @@ public override int GetHashCode()
4757
public override bool Equals(object other)
4858
{
4959
var that = other as FilterKey;
50-
if (that == null)
51-
{
52-
return false;
53-
}
54-
if (!that._filterName.Equals(_filterName))
60+
if (that == null || !that._filterName.Equals(_filterName))
5561
return false;
56-
if (!CollectionHelper.DictionaryEquals<string, TypedValue>(that._filterParameters, _filterParameters))
57-
return false;
58-
return true;
62+
63+
// BagEquals is less efficient than a DictionaryEquals, but serializing dictionaries causes issues on
64+
// deserialization if GetHashCode or Equals are called in its deserialization callback. And building
65+
// dictionaries on the fly will in most cases be worst than BagEquals, unless re-coding its short-circuits.
66+
return CollectionHelper.BagEquals(
67+
_filterParameters,
68+
that._filterParameters,
69+
NamedParameterComparer.Instance);
5970
}
6071

6172
public override string ToString()

src/NHibernate/Cache/QueryKey.cs

Lines changed: 41 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
using System;
22
using System.Collections.Generic;
3+
using System.Linq;
4+
using System.Runtime.Serialization;
35
using System.Text;
46
using NHibernate.Engine;
57
using NHibernate.Impl;
@@ -11,23 +13,32 @@
1113
namespace NHibernate.Cache
1214
{
1315
[Serializable]
14-
public class QueryKey
16+
public class QueryKey : IDeserializationCallback
1517
{
1618
private readonly ISessionFactoryImplementor _factory;
1719
private readonly SqlString _sqlQueryString;
1820
private readonly IType[] _types;
1921
private readonly object[] _values;
2022
private readonly int _firstRow = RowSelection.NoValue;
2123
private readonly int _maxRows = RowSelection.NoValue;
22-
private readonly IDictionary<string, TypedValue> _namedParameters;
23-
private readonly ISet<FilterKey> _filters;
24+
25+
// Sets and dictionaries are populated last during deserialization, causing them to be potentially empty
26+
// during the deserialization callback. This causes them to be unreliable when used in hashcode or equals
27+
// computations. These computations occur during the deserialization callback for example when another
28+
// serialized set or dictionary contain an instance of this class.
29+
// So better serialize them as other structures, so long for Equals implementation which actually needs a
30+
// dictionary and set.
31+
private readonly KeyValuePair<string, TypedValue>[] _namedParameters;
32+
private readonly FilterKey[] _filters;
33+
2434
private readonly CacheableResultTransformer _customTransformer;
25-
private readonly int _hashCode;
35+
// hashcode may vary among processes, they cannot be stored and have to be re-computed after deserialization
36+
[NonSerialized]
37+
private int? _hashCode;
2638

2739
private int[] _multiQueriesFirstRows;
2840
private int[] _multiQueriesMaxRows;
2941

30-
3142
/// <summary>
3243
/// Initializes a new instance of the <see cref="QueryKey"/> class.
3344
/// </summary>
@@ -55,9 +66,11 @@ public QueryKey(ISessionFactoryImplementor factory, SqlString queryString, Query
5566
_firstRow = RowSelection.NoValue;
5667
_maxRows = RowSelection.NoValue;
5768
}
58-
_namedParameters = queryParameters.NamedParameters;
59-
_filters = filters;
69+
70+
_namedParameters = queryParameters.NamedParameters?.ToArray();
71+
_filters = filters?.ToArray();
6072
_customTransformer = customTransformer;
73+
6174
_hashCode = ComputeHashCode();
6275
}
6376

@@ -127,15 +140,14 @@ public override bool Equals(object other)
127140
}
128141
}
129142

130-
if (!CollectionHelper.SetEquals(_filters, that._filters))
131-
{
143+
// BagEquals is less efficient than a SetEquals or DictionaryEquals, but serializing dictionaries causes
144+
// issues on deserialization if GetHashCode or Equals are called in its deserialization callback. And
145+
// building sets or dictionaries on the fly will in most cases be worst than BagEquals, unless re-coding
146+
// its short-circuits.
147+
if (!CollectionHelper.BagEquals(_filters, that._filters))
132148
return false;
133-
}
134-
135-
if (!CollectionHelper.DictionaryEquals(_namedParameters, that._namedParameters))
136-
{
149+
if (!CollectionHelper.BagEquals(_namedParameters, that._namedParameters, NamedParameterComparer.Instance))
137150
return false;
138-
}
139151

140152
if (!CollectionHelper.SequenceEquals<int>(_multiQueriesFirstRows, that._multiQueriesFirstRows))
141153
{
@@ -150,7 +162,17 @@ public override bool Equals(object other)
150162

151163
public override int GetHashCode()
152164
{
153-
return _hashCode;
165+
// If the object is put in a set or dictionary during deserialization, the hashcode will not yet be
166+
// computed. Compute the hashcode on the fly. So long as this happens only during deserialization, there
167+
// will be no thread safety issues. For the hashcode to be always defined after deserialization, the
168+
// deserialization callback is used.
169+
return _hashCode ?? ComputeHashCode();
170+
}
171+
172+
/// <inheritdoc />
173+
public void OnDeserialization(object sender)
174+
{
175+
_hashCode = ComputeHashCode();
154176
}
155177

156178
public int ComputeHashCode()
@@ -161,7 +183,9 @@ public int ComputeHashCode()
161183
result = 37 * result + _firstRow.GetHashCode();
162184
result = 37 * result + _maxRows.GetHashCode();
163185

164-
result = 37 * result + (_namedParameters == null ? 0 : CollectionHelper.GetHashCode(_namedParameters, NamedParameterComparer.Instance));
186+
result = 37 * result + (_namedParameters == null
187+
? 0
188+
: CollectionHelper.GetHashCode(_namedParameters, NamedParameterComparer.Instance));
165189

166190
for (int i = 0; i < _types.Length; i++)
167191
{
@@ -190,10 +214,7 @@ public int ComputeHashCode()
190214

191215
if (_filters != null)
192216
{
193-
foreach (object filter in _filters)
194-
{
195-
result = 37 * result + filter.GetHashCode();
196-
}
217+
result = 37 * result + CollectionHelper.GetHashCode(_filters);
197218
}
198219

199220
result = 37 * result + (_customTransformer == null ? 0 : _customTransformer.GetHashCode());

src/NHibernate/Engine/AssociationKey.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,11 @@ internal sealed class AssociationKey
1212
{
1313
private readonly EntityKey ownerKey;
1414
private readonly string propertyName;
15-
private readonly int hashCode;
1615

1716
public AssociationKey(EntityKey ownerKey, string propertyName)
1817
{
1918
this.ownerKey = ownerKey;
2019
this.propertyName = propertyName;
21-
hashCode = ownerKey.GetHashCode() ^ propertyName.GetHashCode() ^ ownerKey.EntityName.GetHashCode();
2220
}
2321

2422
public override bool Equals(object that)
@@ -40,7 +38,7 @@ public override bool Equals(object that)
4038

4139
public override int GetHashCode()
4240
{
43-
return hashCode;
41+
return ownerKey.GetHashCode() ^ propertyName.GetHashCode() ^ ownerKey.EntityName.GetHashCode();
4442
}
4543
}
46-
}
44+
}

0 commit comments

Comments
 (0)