-
-
Notifications
You must be signed in to change notification settings - Fork 105
Description
When formatting a template that references a key not present in the dictionary, DictionarySource.TryGetIDictionaryValue iterates all entries and returns false if no match is found. This causes FormattingException instead of outputting an empty string.
This is a problem for templates with optional fields - e.g. financial data where not every message contains all possible fields.
Reproduction:
using SmartFormat;
public enum Field { Price, Volume, Name }
var formatter = Smart.CreateDefaultSmartFormat();
var dict = new Dictionary<Field, object>
{
{ Field.Price, 100.5 },
// Volume is intentionally missing
{ Field.Name, "AAPL" },
};
var obj = new { Changes = (IDictionary<Field, object>)dict };
// Throws FormattingException:
// "No source extension could handle the selector named "Volume""
var result = formatter.Format("{Changes:{Price};{Volume};{Name}}", obj);
// Expected: "100.5;;AAPL"
Root cause:
DictionarySource.TryGetIDictionaryValue (line 64-80) iterates DictionaryEntry and compares entry.Key.ToString() to selector text. If no entry matches, it returns false, which eventually triggers the error.
However, the non-generic IDictionary indexer (dict[key]) returns null for missing keys rather than throwing. This would be the more graceful behavior - return true with result = null, letting the formatter output an empty string.
Suggested fix in TryGetIDictionaryValue:
private static bool TryGetIDictionaryValue(object current, string selectorText,
StringComparison comparison, out object? value)
{
if (current is IDictionary rawDict)
{
foreach (DictionaryEntry entry in rawDict)
{
var key = entry.Key as string ?? entry.Key.ToString()!;
if (!key.Equals(selectorText, comparison))
continue;
value = entry.Value;
return true;
}
// Key not found — return null instead of falling through to error.
// Non-generic IDictionary contract: indexer returns null for missing keys.
value = null;
return true;
}
value = null;
return false;
}
Impact: Any template referencing optional dictionary keys currently fails. This forces users to either guarantee all keys exist or implement a custom ISource workaround.
Our workaround:
We implemented a custom ISource that handles this by accessing the non-generic IDictionary indexer directly (dictionary[key]), which returns null for missing keys instead of throwing. It also uses type-aware key conversion (Convert(selectorText, keyType)) rather than ToString() comparison, making it work reliably with enum, int, long and other non-string key types.