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;
}
}