Search code examples
c#dictionarygenericscase-insensitivevaluetuple

How can I create a generic case-insensitive multi-key dictionary using ValueTuples?


I know that I can define a dictionary with a System.ValueTuple key (based on this answer) as below:

var multiKeyDictionary = new Dictionary<(int key1, string key2), object>()
{
    {(1, "test"), new object() }
};

However, I want any string values present in the ValueTuple to be treated as case-insensitive using IEqualityComparer or something similarly idiomatic for dictionary access, similar to what is prescribed here.

The solutions I have thought of so far are to:

class InsensitiveValueTupleDictionary
{
    private Dictionary<(int key1, string key2), object> _multiKeyDictionary = new Dictionary<(int key1, string key2), object>();

    public void Add((int key1, string key2) key, object value)
    {
        _multiKeyDictionary.Add((key.key1, key.key2.ToLower()), value);
    }

    public bool ContainsKey((int key1, string key2) key)
    {
        return _multiKeyDictionary.ContainsKey((key.key1, key.key2.ToLower()));
    }

    // ... and so on
}

Both of these strategies will require setup code for every unique combination of ValueTuple e.g., (int, string), (string, int), (string, string, int), and so on.

Is there another method that will allow case-insensitive key equality for arbitrary ValueTuple combinations?


Solution

  • You're painting yourself into that corner by using a tuple. Tuples are meant to represent a bag of values, not an entity.

    You can implement your own key type in a tuple friendly way:

    public struct Key : IEquatable<Key>
    {
        private readonly int hashCode;
    
        public Key(int key1, string key2)
        {
            this.Key1 = key1;
            this.Key2 = key2;
            this.hashCode = HashCode.Combine(key1, StringComparer.OrdinalIgnoreCase.GetHashCode(Key2));
        }
    
        public int Key1 { get; }
        public string Key2 { get; }
    
        public bool Equals(Key other)
            => this.hashCode == other.hashCode
                && this.Key1 == other.Key1
                && string.Equals(this.Key2, other.Key2, StringComparison.OrdinalIgnoreCase);
    
        public override bool Equals(object obj)
            => obj is Key key && this.Equals(key);
    
        public override int GetHashCode() => this.hashCode;
        
        public static implicit operator (int key1, string key2)(Key key)
            => (key1: key.Key1, key2: key.Key2);
        
        public static implicit operator Key((int key1, string key2) key)
            => new Key(key.key1, key.key2);
        
        public void Deconstruct(out int key1, out string key2)
            => (key1, key2) = (this.Key1, this.Key2);
    }
    

    You can even use it with tuples or "like" tuples:

    var key = new Key(1, "one");
    var (k1, k2) = key;
    (int key1, string key2) t = key;
    t.key1 = 2;
    t.key2 = "two";
    key = t;
    

    If you really want to stay with tuples, define your own comparer:

    public class MyTupleComparer : IEqualityComparer<(int key1, string key2)>
    {
        public bool Equals((int key1, string key2) x, (int key1, string key2) y)
            => x.key1 == y.key1
                && string.Equals(x.key2, y.key2, StringComparison.OrdinalIgnoreCase);
    
        public int GetHashCode((int key1, string key2) obj)
        => HashCode.Combine(obj.key1, StringComparer.OrdinalIgnoreCase.GetHashCode(obj.key2));
    }