'Is this custom Composite Key type sound and performant?
I need to model a composite key type that could have any number of key / value pairs.
I need the key to be convertible into a hashed string value that can be stored in a database for querying. As part of this, I need the key to be able to be recreated at any time, with the possibility the key value pairs will be added in a different order. In my implementation I've figured using a SortedDictionary<string, string>
will prevent issues with hashing and ordering.
I also need the type to have sound equality comparison in code.
I don't often need something like this, so I've created the below as a quick approach that could work.
I'm not sure if I'm overcooking this, and whether there's a simpler and/or more performant approach?
public class KeyDictionary
{
public KeyDictionary()
{
Items = new SortedDictionary<string, string>();
}
public SortedDictionary<string, string> Items { get; init; }
}
class CompositeKey
{
private readonly KeyDictionary keyDictionary;
public CompositeKey()
{
keyDictionary = new KeyDictionary();
}
public CompositeKey(string base64)
{
var raw = Base64Decode(base64);
var deserialized = JsonSerializer.Deserialize<KeyDictionary>(raw);
if (deserialized == null)
{
throw new InvalidOperationException($"Attempted to deserialize a key into a sorted dictionary and got null. Key in base64 is {base64}");
}
keyDictionary = deserialized;
}
public void Add(string key, string value)
{
if (keyDictionary.Items.ContainsKey(key))
{
throw new InvalidOperationException($"Key {key} cannot be added more than once");
}
keyDictionary.Items.Add(key, value);
}
private static string Base64Decode(string base64EncodedData)
{
var base64EncodedBytes = Convert.FromBase64String(base64EncodedData);
return System.Text.Encoding.UTF8.GetString(base64EncodedBytes);
}
private static string Base64Encode(string plainText)
{
var plainTextBytes = System.Text.Encoding.UTF8.GetBytes(plainText);
return Convert.ToBase64String(plainTextBytes);
}
public override string ToString()
{
return Base64Encode(JsonSerializer.Serialize(keyDictionary));
}
public override bool Equals(object? obj)
{
if (obj is not CompositeKey item)
{
return false;
}
return item.ToString() == ToString();
}
public override int GetHashCode()
{
return ToString().GetHashCode();
}
}
In Use
// Creation of a key
var originalKey = new CompositeKey();
originalKey.Add("seasonId", "45045054");
originalKey.Add("matchId", "66724565");
// Hashed value of the key
var hasOfOriginalKey = originalKey.ToString();
Console.WriteLine(hasOfOriginalKey); // eyJJdGVtcyI6eyJhZHZhbnRhZ2VJZCI6IjY2NzI0NTY1Iiwic2Vhc29uSWQiOiI0NTA0NTA1NCJ9fQ==
// De hashing of the key
var dehashedKey = new CompositeKey(hasOfOriginalKey);
Console.WriteLine(originalKey.Equals(dehashedKey)); // True
// Creating the same key independently (with keys in different order).
var remadeKey = new CompositeKey();
remadeKey.Add("matchId", "66724565");
remadeKey.Add("seasonId", "45045054");
Console.WriteLine(originalKey.Equals(remadeKey)); // True
// This should not evaluate as equal to the original key
var differentKey = new CompositeKey();
differentKey.Add("matchId", "7767676");
differentKey.Add("seasonId", "45045054");
Console.WriteLine(originalKey.Equals(differentKey)); // False
Solution 1:[1]
I think you've overcooked it. I think you could just replace everything with a regular SortedDictionary<string, string>
and some utility code that can do a few things like:
- Determine if two
SortedDictionary<string, string>
are equal - Serialize/deserialize
SortedDictionary<string, string>
<-> JSON - Encode/decode JSON <-> Base64
Significantly, #2 and #3 are already done for you with the JsonSerializer
and Convert
classes respectively.
So the only thing left for you to do is compare two SortedDictionary<string, string>
for equality. And that can be done much faster than serializing the whole shebang into JSON/Base64 strings. For example:
static bool AreEqual(IReadOnlyDictionary<string, string> one, IReadOnlyDictionary<string, string> two)
{
if (one.Count != two.Count)
return false;
return Enumerable
.Zip(
one,
two,
(kvp1, kvp2) => kvp1.Key == kvp2.Key && kvp1.Value == kvp2.Value
)
.All(x => x);
}
With (a working form of) the above function, your code becomes something like this:
string Serialize(object obj)
{
var json = JsonSerializer.Serialize(obj);
var utf8Bytes = System.Text.Encoding.UTF8.GetBytes(json);
return Convert.ToBase64String(utf8Bytes);
}
SortedDictionary<string, string> Deserialize(string base64)
{
var utf8Bytes = Convert.FromBase64String(base64);
var json = System.Text.Encoding.UTF8.GetString(utf8Bytes);
return JsonSerializer.Deserialize<SortedDictionary<string, string>>(json);
}
// Creation of a key
var originalKey = new SortedDictionary<string, string>();
originalKey["seasonId"] = "45045054";
originalKey["matchId"] = "66724565";
// Hashed value of the key
var hashOfOriginalKey = Serialize(originalKey);
Console.WriteLine(hashOfOriginalKey); // eyJJdGVtcyI6eyJhZHZhbnRhZ2VJZCI6IjY2NzI0NTY1Iiwic2Vhc29uSWQiOiI0NTA0NTA1NCJ9fQ==
// De hashing of the key
var dehashedKey = Deserialize(hashOfOriginalKey);
Console.WriteLine(AreEqual(dehashedKey, originalKey)); // True
// Creating the same key independently (with keys in different order).
var remadeKey = new SortedDictionary<string, string>();
remadeKey["matchId"] = "66724565";
remadeKey["seasonId"] = "45045054";
Console.WriteLine(AreEqual(originalKey, remadeKey)); // True
// This should not evaluate as equal to the original key
var differentKey = new SortedDictionary<string, string>();
differentKey["matchId"] = "7767676";
differentKey["seasonId"] = "45045054";
Console.WriteLine(AreEqual(originalKey, differentKey)); // False
Note: I'm aware that there will be a difference in the JSON produced by this code versus your code. Your code will produce JSON like this:
{
"Items": {
"key": "value"
}
}
...but my code will produce JSON like this:
{
"key": "value"
}
You can make that difference go away with sufficient hand-waving over my Serialize
and Deserialize
methods.
The above is just inspirational pseudocode written off the cuff. So don't be surprised if it needs some tweaks to compile ;)
Sources
This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.
Source: Stack Overflow
Solution | Source |
---|---|
Solution 1 | Matt Thomas |