How to store null key in Generic Dictionary<TKey, TValue>

By Varun Om | June 19, 2019

I was writing a cache optimized permit manager in ASPSecurityKit that stores entityIds as keys and list of permissions as values to reduce session size stored in Redis by 90%. In tests, I was using Nullable Guid? as the key type and it failed spectacularly with this exception:

System.ArgumentNullException : Value cannot be null.
Parameter name: key

Researching about it, I stumbled on this SO question and this MSDN spec for Dictionary<TKey, TValue>.

A key cannot be null, but a value can be

Ug! But null key is a non-negotiable requirement for permit manager as general permits (a permission granted to all entity instances of a given entity type) are created with null entityIds. The recommendation in Stackoverflow is to create a custom type, sort of a wrapper to hold the key which also manages null keys without ever being null itself. So here is an implementation of such a wrapper, inspired by this SO answer:

    public struct KeyWrapper<TKey>
        where TKey : struct
    {
        public static readonly KeyWrapper<TKey> NullKey = new KeyWrapper<TKey>(null);

        public TKey? Key { get; }
        public bool IsNull => !this.Key.HasValue;

        public KeyWrapper(TKey? key)
        {
            this.Key = key;
        }

        public static implicit operator TKey?(KeyWrapper<TKey> keyWrapper)
        {
            return keyWrapper.Key;
        }

        public static implicit operator KeyWrapper<TKey>(TKey? key)
        {
            return new KeyWrapper<TKey>(key);
        }

        public override string ToString()
        {
            return Key?.ToString() ?? string.Empty;
        }

        public override bool Equals(object obj)
        {
            if (obj == null)
                return this.IsNull;

            if (!(obj is KeyWrapper<TKey> that))
                return false;

            if (this.IsNull)
                return that.IsNull;

            if (that.IsNull)
                return this.IsNull;

            return this.Key.Value.Equals(that.Key.Value);
        }

        public override int GetHashCode()
        {
            if (this.IsNull)
                return 0;

            var result = this.Key.GetHashCode();

            // Increment so as to not conflict with zero reserved for null
            if (result >= 0)
                result++;

            return result;
        }
    }
Share on: