Preliminary rant:
System.Collections.Generic.KeyNotFoundException : The given key was not present in the dictionary.
Oh. My.
So I finally got fed up with how default exception information is beyond useless.
For a part of the project I will mostly no longer use the this[TKey key]
property to access my Dictionary
ies but will use a custom extension method that actually tries to tell me WTF went wrong.
Preliminary rationale:
The system I have has a large "integration testing" suite in addition to more easily debuggable Unit Tests, and if things go southwards, it is hard to grok what went wrong, if all you have is a rather unspecific stack trace and a message telling you:
System.Collections.Generic.KeyNotFoundException : The given key was not present in the dictionary.
I would note that this system heavily relies on (string) mappings, where some kind of configuration error can easily trigger such errors in unexpected places.
Especially, there are quite a lot of places in the code, where different dictionaries will be consulted in the same code part / line, making the stacktrace basically worthless to pinpoint the exact problem. Think:
var attributeValue = model[Config.PartX].attributes[Config.Attribute42];
I will also note that I started out with hand coding the TryGetValue
stuff, but this got unreadable pretty fast, so I'm looking forward to trying a slightly less specific, but more hopefully usable, approach.
Requirements
"Key Not Found" Exception shall contain information pertaining to:
- local name of dictionary
- number of items in dictionary
- value of the key
The information shall be recorded by the logging framework (NLog) without further fiddling.
I feel I have to explicitly note this: The potential leaking of
key
value information is beyond irrelevant for this system. Please do not comment wrt. this aspect.
Code
If applicable, all my snippets basically fall under https://opensource.org/licenses/unlicense in the unlikely event anybody would like to copy this.
namespace MY.Project
{
public static class DictionaryHelper
{
/// <summary>
/// Same as `TValue this[TKey key] { get; }`, but with a better exception message
/// containing the dictionary "name" (needs to be provided), number of entries and key value
/// </summary>
/// <returns>Value if found, throws KeyNotFoundException otherwise</returns>
public static TValue GetExistingValue<TKey, TValue>(this IDictionary<TKey, TValue> dict, string nameOfDict, TKey key)
{
if (!dict.TryGetValue(key, out var val))
{
throw CreateKeyNotFoundException(dict, nameOfDict, dict.Count, key);
}
return val;
}
/// <see cref="GetExistingValue"/> for `IDictionary` above.
public static TValue GetExistingValue<TKey, TValue>(this IReadOnlyDictionary<TKey, TValue> dict, string nameOfDict, TKey key)
{
if (!dict.TryGetValue(key, out var val))
{
throw CreateKeyNotFoundException(dict, nameOfDict, dict.Count, key);
}
return val;
}
/// <summary>
/// Provide separate explicit overload for the `Dictionary` class because this class implements both
/// the IDict and IReadOnlyDict interface, making the overload ambiguous otherwise
/// </summary>
public static TValue GetExistingValue<TKey, TValue>(this Dictionary<TKey, TValue> dict, string nameOfDict, TKey key)
{
return GetExistingValue((IDictionary<TKey, TValue>)dict, nameOfDict, key);
}
private static KeyNotFoundException CreateKeyNotFoundException<T, TCount, TKey>(T dict, string nameOfDict, TCount count, TKey key)
{
return new KeyNotFoundException(
$"{nameOfDict} ({dict.GetType()}) (with #{count} entries) does not contain key <{key}>!"
);
}
}
Questions
Beyond general feedback:
- Is there any sane name for this extension method?
- Would you make this an extension method?
- Is there any other good way to capture the name of the dictionary variable?
- Order of parameters? How to prevent messing up order of
string nameOfDict
andkey
ifTKey == string
?