tui-cs/Terminal.Gui · error · InvalidOperationException
Error cloning dictionary ({source}) (last key was "{lastKey}
Error message
Error cloning dictionary ({source}) (last key was "{lastKey}"). Ensure the source dictionary is not modified during cloning. What it means
Thrown by DeepCloner.CloneDictionary when an InvalidOperationException escapes the foreach over sourceDict.Keys. The catch at DeepCloner.cs:358-364 wraps it with the source's ToString and the last key being processed, because the canonical cause is concurrent modification of the source dictionary during enumeration. The inner exception carries the original failure (typically 'Collection was modified; enumeration operation may not execute').
Source
Thrown at Terminal.Gui/Configuration/DeepCloner.cs:361
schemeDict [(string)clonedKey!] = (Scheme?)clonedValue;
continue;
}
if (tempDict.Contains (clonedKey!))
{
tempDict [clonedKey!] = clonedValue;
}
else
{
tempDict.Add (clonedKey!, clonedValue);
}
}
}
catch (InvalidOperationException ex)
{
// Handle cases where the dictionary is modified during enumeration
throw new InvalidOperationException (
$"Error cloning dictionary ({source}) (last key was \"{lastKey}\"). Ensure the source dictionary is not modified during cloning.",
ex);
}
return tempDict;
}
[UnconditionalSuppressMessage ("Trimming", "IL2067", Justification = "Dictionary cloning only instantiates supported dictionary runtime types and falls back safely when comparer constructors are unavailable.")]
private static IDictionary CreateDictionaryInstance (Type dictType, object? comparer)
{
// Typed paths for dictionary types that require custom comparers.
if (dictType == typeof (ConcurrentDictionary<string, ThemeScope>))
{
if (comparer is IEqualityComparer<string> stringComparer)
{
return new ConcurrentDictionary<string, ThemeScope> (stringComparer);
}
View on GitHub (pinned to 2e47b11478)
Solutions
- Ensure the dictionary is not mutated during the clone: take a snapshot with ToArray()/new Dictionary(source) before calling DeepClone if concurrent mutation is possible.
- Use ConcurrentDictionary<,> for any configuration dictionary that may be read during a clone on another thread (it enumerates over a snapshot and does not throw on concurrent modification).
- Serialize access to the configuration scope with a lock around both mutation and Apply/clone operations.
- Check whether a ConfigProperty setter or value-cloning callback re-enters ConfigurationManager and remove the re-entrancy.
- Inspect the inner exception's stack trace to find the mutating call site.
Example fix
// before — clone a dictionary that another thread may mutate var clone = DeepCloner.DeepClone (sharedThemeDictionary); // after — snapshot first, then clone the snapshot var snapshot = new ConcurrentDictionary<string, ThemeScope> (sharedThemeDictionary); var clone = DeepCloner.DeepClone (snapshot);
Defensive patterns
Strategy: try-catch
Validate before calling
// Snapshot a plain Dictionary before cloning if concurrent access is possible
static Dictionary<TKey, TValue> SnapshotForClone<TKey, TValue> (Dictionary<TKey, TValue> source)
where TKey : notnull
{
lock (source)
{
return new Dictionary<TKey, TValue> (source, source.Comparer);
}
}
var safeCopy = SnapshotForClone (sharedDict);
var clone = DeepCloner.DeepClone (safeCopy); Type guard
// Prefer ConcurrentDictionary for any dictionary cloned under concurrency
static bool IsConcurrencySafeForClone (object? dict)
=> dict is System.Collections.Concurrent.ConcurrentDictionary<,>; Try / catch
try
{
var clone = DeepCloner.DeepClone (configScope);
}
catch (InvalidOperationException ex) when (ex.Message.Contains ("Ensure the source dictionary is not modified during cloning"))
{
// A concurrent modification happened. Snapshot the source and retry,
// or serialize Apply/clone with a lock against mutations.
var snapshot = sourceDict.ToArray ();
clone = DeepCloner.DeepClone (new Dictionary<,>(snapshot));
} Prevention
- Use ConcurrentDictionary<,> for configuration dictionaries accessed across threads.
- Lock around both mutation and Apply/clone when sharing a plain Dictionary.
- Snapshot (ToArray/new Dictionary) before cloning if you cannot guarantee quiescence.
- Avoid re-entrant ConfigurationManager calls from ConfigProperty setters.
When it happens
Trigger: DeepClone runs on a dictionary while another thread (or a re-entrant clone callback) mutates the same dictionary instance. Concretely: the sourceDict.Keys enumerator detects a version change mid-iteration, or DeepCloneInternal on a value triggers a setter that adds/removes a key from the same source dictionary.
Common situations: ConfigurationManager.Apply runs on one thread while a theme change or settings reload mutates the same SettingsScope/ThemeScope dictionary on another. A Scheme or ConfigProperty setter, invoked during cloning, re-enters and modifies the dictionary being cloned. ConcurrentDictionary sources are less likely to throw here (snapshot enumeration), so this most often involves plain Dictionary<,>.
Related errors
- Unsupported dictionary type: {type}. Only Dictionary<,> and
- Cloning of frozen or immutable dictionaries like {type.Name}
- Cannot create instance of type {type.FullName}. No parameter
- Cannot create instance of type {type.FullName} in AOT contex
- Cloning of immutable collections like {type.Name} is not sup
AI-assisted analysis of tui-cs/Terminal.Gui@2e47b11478 (2026-08-13).
Data as JSON: /api/errors/84bd36c965780aeb.
Report an issue: GitHub.