Skip to content

Commit

Permalink
Create custom comprarer to handle containers byte arrays.
Browse files Browse the repository at this point in the history
In C# byte array comparisons are by reference. This will likely cause
unexpected behavior to end-users. To make things do what we mean, we
provide a structural comprarer that works on the contents as opposed
to the array reference.

We also provide a `DictionaryEquals` extension that parallels the C#
library's `SetEquals` method. We have to do this because, even if we
provide a structural comparer as described above, this only operates
on the keys, not the values.
  • Loading branch information
malandis committed Jul 28, 2022
1 parent 1181245 commit c5734b0
Show file tree
Hide file tree
Showing 3 changed files with 93 additions and 18 deletions.
47 changes: 31 additions & 16 deletions IncubatingIntegrationTest/DictionaryTest.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
using MomentoSdk.Internal.ExtensionMethods;
using MomentoSdk.Responses;

namespace IncubatingIntegrationTest;
Expand Down Expand Up @@ -874,22 +875,29 @@ public void DictionaryGetAll_HasContentString_HappyPath()
public void DictionaryGetAll_HasContentBytes_HappyPath()
{
var dictionaryName = Utils.GuidString();
var field1 = Utils.GuidString();
var value1 = Utils.GuidString();
var field2 = Utils.GuidString();
var value2 = Utils.GuidString();
var contentDictionary = new Dictionary<string, string>() {
var field1 = Utils.GuidBytes();
var value1 = Utils.GuidBytes();
var field2 = Utils.GuidBytes();
var value2 = Utils.GuidBytes();
var contentDictionary = new Dictionary<byte[], byte[]>() {
{field1, value1},
{field2, value2}
};

client.DictionarySet(cacheName, dictionaryName, Utils.Utf8ToBytes(field1), Utils.Utf8ToBytes(value1), true, ttlSeconds: 10);
client.DictionarySet(cacheName, dictionaryName, Utils.Utf8ToBytes(field2), Utils.Utf8ToBytes(value2), true, ttlSeconds: 10);
client.DictionarySet(cacheName, dictionaryName, field1, value1, true, ttlSeconds: 10);
client.DictionarySet(cacheName, dictionaryName, field2, value2, true, ttlSeconds: 10);

var getAllResponse = client.DictionaryGetAll(cacheName, dictionaryName);

Assert.Equal(CacheGetStatus.HIT, getAllResponse.Status);
Assert.Equal(getAllResponse.StringDictionary(), contentDictionary);

// Exercise byte array dictionary structural equality comparer
Assert.True(getAllResponse.ByteArrayDictionary!.ContainsKey(field1));
Assert.True(getAllResponse.ByteArrayDictionary!.ContainsKey(field2));
Assert.Equal(2, getAllResponse.ByteArrayDictionary!.Count);

// Exercise DictionaryEquals extension
Assert.True(getAllResponse.ByteArrayDictionary!.DictionaryEquals(contentDictionary));
}

[Theory]
Expand Down Expand Up @@ -934,22 +942,29 @@ public async void DictionaryGetAllAsync_HasContentString_HappyPath()
public async void DictionaryGetAllAsync_HasContentBytes_HappyPath()
{
var dictionaryName = Utils.GuidString();
var field1 = Utils.GuidString();
var value1 = Utils.GuidString();
var field2 = Utils.GuidString();
var value2 = Utils.GuidString();
var contentDictionary = new Dictionary<string, string>() {
var field1 = Utils.GuidBytes();
var value1 = Utils.GuidBytes();
var field2 = Utils.GuidBytes();
var value2 = Utils.GuidBytes();
var contentDictionary = new Dictionary<byte[], byte[]>() {
{field1, value1},
{field2, value2}
};

await client.DictionarySetAsync(cacheName, dictionaryName, Utils.Utf8ToBytes(field1), Utils.Utf8ToBytes(value1), true, ttlSeconds: 10);
await client.DictionarySetAsync(cacheName, dictionaryName, Utils.Utf8ToBytes(field2), Utils.Utf8ToBytes(value2), true, ttlSeconds: 10);
await client.DictionarySetAsync(cacheName, dictionaryName, field1, value1, true, ttlSeconds: 10);
await client.DictionarySetAsync(cacheName, dictionaryName, field2, value2, true, ttlSeconds: 10);

var getAllResponse = await client.DictionaryGetAllAsync(cacheName, dictionaryName);

Assert.Equal(CacheGetStatus.HIT, getAllResponse.Status);
Assert.Equal(getAllResponse.StringDictionary(), contentDictionary);

// Exercise byte array dictionary structural equality comparer
Assert.True(getAllResponse.ByteArrayDictionary!.ContainsKey(field1));
Assert.True(getAllResponse.ByteArrayDictionary!.ContainsKey(field2));
Assert.Equal(2, getAllResponse.ByteArrayDictionary!.Count);

// Exercise DictionaryEquals extension
Assert.True(getAllResponse.ByteArrayDictionary!.DictionaryEquals(contentDictionary));
}

[Theory]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using System.Text;
using CacheClient;
using MomentoSdk.Responses;
using MomentoSdk.Internal;

namespace MomentoSdk.Incubating.Responses;

Expand All @@ -23,7 +24,8 @@ public CacheDictionaryGetAllResponse(_DictionaryGetAllResponse response)
ByteArrayDictionary = new Dictionary<byte[], byte[]>(
response.DictionaryBody.Select(
kv => new KeyValuePair<byte[], byte[]>(
kv.Key.ToByteArray(), kv.Value.ToByteArray())));
kv.Key.ToByteArray(), kv.Value.ToByteArray())),
Utils.ByteArrayComparer);
}
}

Expand Down
60 changes: 59 additions & 1 deletion Momento/Internal/Utils.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using System;
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using Google.Protobuf;
Expand Down Expand Up @@ -82,6 +83,29 @@ public static void ElementsNotNull<T>(IEnumerable<T> argument, string paramName)
throw new ArgumentNullException(paramName, "Each value must be non-null");
}
}

/// <summary>
/// Defines methods to support comparing containers of reference items by their
/// contents (structure) instead of by reference.
/// </summary>
public class StructuralEqualityComparer<T> : IEqualityComparer<T>
{
public bool Equals(T x, T y)
{
return StructuralComparisons.StructuralEqualityComparer.Equals(x, y);
}

public int GetHashCode(T obj)
{
return StructuralComparisons.StructuralEqualityComparer.GetHashCode(obj);
}
}

/// <summary>
/// Comprarer to use in byte array containers (Set, Dictionary, List)
/// so comparisons operate on byte-array content instead of reference.
/// </summary>
public static StructuralEqualityComparer<byte[]> ByteArrayComparer = new();
}

namespace ExtensionMethods
Expand All @@ -108,5 +132,39 @@ public static ByteString ToByteString(this string str)
return ByteString.CopyFromUtf8(str);
}
}

public static class ByteArrayDictionaryExtensions
{
/// <summary>
/// DWIM equality implementation for dictionaries. cf `SetEquals`.
/// https://docs.microsoft.com/en-us/dotnet/api/system.collections.generic.hashset-1.setequals?view=net-6.0
///
/// Tests whether the dictionaries contain the same content as opposed to the same
/// references.
/// </summary>
/// <param name="dictionary">LHS to compare</param>
/// <param name="other">RHS to compare</param>
/// <returns>`true` if the dictionaries contain the same content.</returns>
public static bool DictionaryEquals(this Dictionary<byte[], byte[]> dictionary, Dictionary<byte[], byte[]> other)
{
if (dictionary == null && other == null)
{
return true;
}

if (dictionary == null || other == null)
{
return false;
}

if (dictionary.Count != other.Count)
{
return false;
}

var keySet = new HashSet<byte[]>(dictionary.Keys, Utils.ByteArrayComparer);
return other.All(kv => keySet.Contains(kv.Key) && dictionary[kv.Key].SequenceEqual(kv.Value));
}
}
}
}
}

0 comments on commit c5734b0

Please sign in to comment.