Skip to content

DictionarySource throws on missing key instead of returning null for IDictionary with non-string keys #522

@mikasoukhov

Description

@mikasoukhov

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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions