Skip to content

Commit 9a41ffb

Browse files
committed
Merge branch '#49'
2 parents c535af3 + a70fa5c commit 9a41ffb

File tree

5 files changed

+134
-48
lines changed

5 files changed

+134
-48
lines changed

Slapper.AutoMapper.Tests/CachingBehaviorTests.cs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,13 @@ public class OrderItem
4747
public int Id { get; set; }
4848
}
4949

50+
51+
public class OrderWithLongId
52+
{
53+
public long Id { get; set; }
54+
public List<OrderItem> OrderItems { get; set; }
55+
}
56+
5057
[Test]
5158
public void Previously_Instantiated_Objects_Will_Be_Returned_Until_The_Cache_Is_Cleared()
5259
{
@@ -111,6 +118,32 @@ public void Test_Nested_Duplicate_Instances()
111118
Assert.AreSame(employeeList[0].Department, employeeList[1].Department);
112119
}
113120

121+
[Test]
122+
public void Test_Long_Ids_With_Colliding_HashValues()
123+
{
124+
// This test could fail if MS GetHashCode implementation for long changed. We would then have to find new long values
125+
// having the same hashcode.
126+
const long longId1 = 95988224123597;
127+
var item1 = new Dictionary<string, object>()
128+
{
129+
{ "Id", longId1 },
130+
{ "OrderDetail_Id", 1 }
131+
};
132+
133+
const long longId2 = 95983929156300;
134+
var item2 = new Dictionary<string, object>()
135+
{
136+
{ "Id", longId2 },
137+
{ "OrderDetail_Id", 2 }
138+
};
139+
140+
var list = new List<Dictionary<string, object>>() { item1, item2 };
141+
var orderList = AutoMapper.Map<OrderWithLongId>(list).ToList();
142+
143+
Assert.AreEqual(longId1.GetHashCode(), longId2.GetHashCode());
144+
Assert.AreEqual(orderList.Count, list.Count);
145+
}
146+
114147
[Test]
115148
public void Cache_is_cleared_if_KeepCache_is_false()
116149
{

Slapper.AutoMapper.Tests/ComplexMapTests.cs

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,11 +117,16 @@ public void Can_Map_Complex_Nested_Members()
117117
}
118118

119119
/// <summary>
120+
/// OLD SUMMARY ===
120121
/// When mapping, it internally keeps a cache of instantiated objects with the key being the
121122
/// hash of the objects identifier hashes summed together so when another record with the exact
122123
/// same identifier hash is detected, it will re-use the existing instantiated object instead of
123124
/// creating a second one alleviating the burden of the consumer of the library to group objects
124125
/// by their identifier.
126+
/// ===
127+
/// This was flawed as SAME HASHCODE DOESN'T MEAN SAME VALUE. Hash collisions would lead to
128+
/// wrongly reusing an instance instead of creating a new one (issue #48).
129+
/// It's now fixed as real identifier values are compared, not their hashes anymore.
125130
/// </summary>
126131
[Test]
127132
public void Can_Detect_Duplicate_Parent_Members_And_Properly_Instantiate_The_Object_Only_Once()

Slapper.AutoMapper.Tests/PerformanceTests.cs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -105,9 +105,9 @@ public void Complex_Performance_Test()
105105
{ "CustomerId", i },
106106
{ "FirstName", "Bob" },
107107
{ "LastName", "Smith" },
108-
{ "Orders_Id", i },
108+
{ "Orders_OrderId", i },
109109
{ "Orders_OrderTotal", 50.50m },
110-
{ "Orders_OrderDetails_Id", i },
110+
{ "Orders_OrderDetails_OrderDetailId", i },
111111
{ "Orders_OrderDetails_OrderDetailTotal", 50.50m },
112112
{ "Orders_OrderDetails_Product_Id", 546 },
113113
{ "Orders_OrderDetails_Product_ProductName", "Black Bookshelf" }

Slapper.AutoMapper/Slapper.AutoMapper.Cache.cs

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public static partial class AutoMapper
4343
/// <summary>
4444
/// Contains the methods and members responsible for this libraries caching concerns.
4545
/// </summary>
46-
public static class Cache
46+
internal static class Cache
4747
{
4848
/// <summary>
4949
/// The name of the instance cache stored in the logical call context.
@@ -115,13 +115,13 @@ public static void ClearInstanceCache()
115115
/// unique cache.
116116
/// </remarks>
117117
/// <returns>Instance Cache</returns>
118-
public static Dictionary<Tuple<int, int, object>, object> GetInstanceCache()
118+
public static Dictionary<InternalHelpers.InstanceKey,object> GetInstanceCache()
119119
{
120-
var instanceCache = InternalHelpers.ContextStorage.Get<Dictionary<Tuple<int, int, object>, object>>(InstanceCacheContextStorageKey);
120+
var instanceCache = InternalHelpers.ContextStorage.Get<Dictionary<InternalHelpers.InstanceKey, object>>(InstanceCacheContextStorageKey);
121121

122122
if (instanceCache == null)
123123
{
124-
instanceCache = new Dictionary<Tuple<int, int, object>, object>();
124+
instanceCache = new Dictionary<InternalHelpers.InstanceKey, object>();
125125

126126
InternalHelpers.ContextStorage.Store(InstanceCacheContextStorageKey, instanceCache);
127127
}

Slapper.AutoMapper/Slapper.AutoMapper.InternalHelpers.cs

Lines changed: 90 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,73 @@ public static partial class AutoMapper
4949
/// </summary>
5050
internal static class InternalHelpers
5151
{
52+
/// <summary>
53+
/// Combine several hashcodes into a single new one. This implementation was grabbed from http://stackoverflow.com/a/34229665 where it is introduced
54+
/// as MS implementation of GetHashCode() for strings.
55+
/// </summary>
56+
/// <param name="hashCodes">Hascodes to be combined.</param>
57+
/// <returns>A new Hascode value combining those passed as parameters.</returns>
58+
private static int CombineHashCodes(params int[] hashCodes)
59+
{
60+
int hash1 = (5381 << 16) + 5381;
61+
int hash2 = hash1;
62+
63+
int i = 0;
64+
foreach (var hashCode in hashCodes)
65+
{
66+
if (i % 2 == 0)
67+
hash1 = ((hash1 << 5) + hash1 + (hash1 >> 27)) ^ hashCode;
68+
else
69+
hash2 = ((hash2 << 5) + hash2 + (hash2 >> 27)) ^ hashCode;
70+
71+
++i;
72+
}
73+
74+
return hash1 + (hash2 * 1566083941);
75+
}
76+
77+
/// <summary>
78+
/// Defines the key for caching instances. Overrides Equality as to get unicity for a given set of identifiers values
79+
/// for a given type.
80+
/// </summary>
81+
public struct InstanceKey : IEquatable<InstanceKey>
82+
{
83+
public bool Equals(InstanceKey other) {
84+
return Equals(Type, other.Type)
85+
&& Equals(ParentInstance, other.ParentInstance)
86+
&& StructuralComparisons.StructuralEqualityComparer.Equals(IdentifierValues, other.IdentifierValues);
87+
}
88+
89+
public override bool Equals(object obj)
90+
{
91+
if (ReferenceEquals(null, obj)) return false;
92+
return obj is InstanceKey && Equals((InstanceKey) obj);
93+
}
94+
95+
public override int GetHashCode()
96+
{
97+
unchecked
98+
{
99+
return CombineHashCodes(Type?.GetHashCode() ?? 0, StructuralComparisons.StructuralEqualityComparer.GetHashCode(IdentifierValues), ParentInstance?.GetHashCode() ?? 0);
100+
}
101+
}
102+
103+
public static bool operator ==(InstanceKey left, InstanceKey right) { return left.Equals(right); }
104+
105+
public static bool operator !=(InstanceKey left, InstanceKey right) { return !left.Equals(right); }
106+
107+
public InstanceKey(Type type, object[] identifierValues, object parentInstance)
108+
{
109+
Type = type;
110+
IdentifierValues = identifierValues;
111+
ParentInstance = parentInstance;
112+
}
113+
114+
public Type Type { get; }
115+
public object[] IdentifierValues { get; }
116+
public object ParentInstance { get; }
117+
}
118+
52119
/// <summary>
53120
/// Gets the identifiers for the given type. Returns NULL if not found.
54121
/// Results are cached for subsequent use and performance.
@@ -338,6 +405,28 @@ private static object GetMemberValue(object member, object obj)
338405
return value;
339406
}
340407

408+
/// <summary>
409+
/// Computes a key for storing and identifying an instance in the cache.
410+
/// </summary>
411+
/// <param name="type">Type of instance to get</param>
412+
/// <param name="properties">List of properties and values</param>
413+
/// <param name="parentInstance">Parent instance. Can be NULL if this is the root instance.</param>
414+
/// <returns>
415+
/// InstanceKey that will be unique for given set of identifiers values for the type. If the type isn't associated with any
416+
/// identifier, the return value is made unique by generating a Guid.
417+
/// ASSUMES GetIdentifiers(type) ALWAYS RETURN IDENTIFIERS IN THE SAME ORDER FOR A GIVEN TYPE.
418+
/// This is certainly the case as long as GetIdentifiers caches its result for a given type (which it does by 2016-11-25).
419+
/// </returns>
420+
private static InstanceKey GetCacheKey(Type type, IDictionary<string, object> properties, object parentInstance)
421+
{
422+
var identifierValues = GetIdentifiers(type)?.Select(id => properties[id]).DefaultIfEmpty(Guid.NewGuid()).ToArray()
423+
?? new object[] { Guid.NewGuid() };
424+
425+
var key = new InstanceKey(type, identifierValues, parentInstance);
426+
return key;
427+
}
428+
429+
341430
/// <summary>
342431
/// Gets a new or existing instance depending on whether an instance with the same identifiers already existing
343432
/// in the instance cache.
@@ -349,7 +438,7 @@ private static object GetMemberValue(object member, object obj)
349438
/// Tuple of bool, object, int where bool represents whether this is a newly created instance,
350439
/// object being an instance of the requested type and int being the instance's identifier hash.
351440
/// </returns>
352-
internal static Tuple<bool, object, Tuple<int, int, object>> GetInstance(Type type, IDictionary<string, object> properties, object parentInstance = null)
441+
internal static Tuple<bool, object, InstanceKey> GetInstance(Type type, IDictionary<string, object> properties, object parentInstance = null)
353442
{
354443
var key = GetCacheKey(type, properties, parentInstance);
355444

@@ -368,47 +457,6 @@ internal static Tuple<bool, object, Tuple<int, int, object>> GetInstance(Type ty
368457
return Tuple.Create(isNewlyCreatedInstance, instance, key);
369458
}
370459

371-
private static Tuple<int, int, object> GetCacheKey(Type type, IDictionary<string, object> properties, object parentInstance)
372-
{
373-
var identifierHash = GetIdentifierHash(type, properties);
374-
375-
var key = Tuple.Create(identifierHash, type.GetHashCode(), parentInstance);
376-
return key;
377-
}
378-
379-
private static int GetIdentifierHash(Type type, IDictionary<string, object> properties)
380-
{
381-
var identifiers = GetIdentifiers(type);
382-
383-
var identifierHash = 0;
384-
385-
if (identifiers != null)
386-
{
387-
foreach (var identifier in identifiers)
388-
{
389-
if (properties.ContainsKey(identifier))
390-
{
391-
var identifierValue = properties[identifier];
392-
if (identifierValue != null)
393-
{
394-
// Unchecked to avoid arithmetic overflow
395-
unchecked
396-
{
397-
// Include identifier hashcode to avoid collisions between e.g. multiple int IDs
398-
identifierHash += identifierValue.GetHashCode() + identifier.GetHashCode();
399-
}
400-
}
401-
}
402-
}
403-
}
404-
else
405-
{
406-
// If the type has no identifiers we must generate a unique hash for it.
407-
identifierHash = Guid.NewGuid().GetHashCode();
408-
}
409-
return identifierHash;
410-
}
411-
412460
/// <summary>
413461
/// Populates the given instance's properties where the IDictionary key property names
414462
/// match the type's property names case insensitively.

0 commit comments

Comments
 (0)