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

  1. 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.
  2. 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).
  3. Serialize access to the configuration scope with a lock around both mutation and Apply/clone operations.
  4. Check whether a ConfigProperty setter or value-cloning callback re-enters ConfigurationManager and remove the re-entrancy.
  5. 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

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


AI-assisted analysis of tui-cs/Terminal.Gui@2e47b11478 (2026-08-13). Data as JSON: /api/errors/84bd36c965780aeb. Report an issue: GitHub.