mRemoteNG/mRemoteNG · error · InvalidOperationException

An option with key '{option.Key}' already exists.

Error message

An option with key '{option.Key}' already exists.

What it means

OptionsStore.AddOptionAsync throws InvalidOperationException when OptionKeyExists returns true for the given key. The options table enforces a UNIQUE constraint on key; this explicit pre-check provides a clearer, domain-specific message than the raw SQLite constraint violation. It indicates a duplicate insert attempt, not a malformed argument.

Source

Thrown at mRemoteNG/Config/Settings/Store/OptionsStore.cs:238

        /// <summary>
        /// Adds a new option to the store.
        /// </summary>
        public async Task<OptionInfo> AddOptionAsync(OptionInfo option)
        {
            if (option == null)
                throw new ArgumentNullException(nameof(option));
            if (string.IsNullOrWhiteSpace(option.Key))
                throw new ArgumentException("Option key cannot be null or whitespace.", nameof(option));

            EnsureReady();

            return await Task.Run(() =>
            {
                // Check if option key already exists
                if (OptionKeyExists(option.Key))
                {
                    throw new InvalidOperationException($"An option with key '{option.Key}' already exists.");
                }

                const string sql = """
                    INSERT INTO options (key, value, category, description, option_type, created_at, modified_at)
                    VALUES (@key, @value, @category, @description, @option_type, @created_at, @modified_at);
                    SELECT last_insert_rowid();
                    """;

                using SqliteCommand cmd = _connection.CreateCommand();
                cmd.CommandText = sql;
                cmd.Parameters.AddWithValue("@key", option.Key);
                cmd.Parameters.AddWithValue("@value", option.Value ?? (object)DBNull.Value);
                cmd.Parameters.AddWithValue("@category", option.Category ?? (object)DBNull.Value);
                cmd.Parameters.AddWithValue("@description", option.Description ?? (object)DBNull.Value);
                cmd.Parameters.AddWithValue("@option_type", option.OptionType ?? "string");
                cmd.Parameters.AddWithValue("@created_at", DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture));
                cmd.Parameters.AddWithValue("@modified_at", DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture));

View on GitHub (pinned to 9211babf35)

Solutions

  1. Call OptionExistsAsync(key) first and branch to UpdateOptionAsync when it returns true (upsert pattern).
  2. Catch InvalidOperationException and fall back to GetOptionByKeyAsync + UpdateOptionAsync.
  3. Clear or deduplicate data before bulk import.
  4. For concurrent access, serialize writes through a lock or single writer task — the check-then-insert is not atomic.

Example fix

// before
await store.AddOptionAsync(opt); // throws if key exists

// after
if (await store.OptionExistsAsync(opt.Key))
{
    var existing = await store.GetOptionByKeyAsync(opt.Key);
    opt.Id = existing.Id;
    await store.UpdateOptionAsync(opt);
}
else
{
    await store.AddOptionAsync(opt);
}
Defensive patterns

Strategy: validation

Validate before calling

if (await store.OptionExistsAsync(option.Key))
{
    var existing = await store.GetOptionByKeyAsync(option.Key);
    option.Id = existing.Id;
    await store.UpdateOptionAsync(option);
}
else
{
    await store.AddOptionAsync(option);
}

Try / catch

try { await store.AddOptionAsync(option); }
catch (InvalidOperationException ex) when (ex.Message.Contains("already exists"))
{ /* fall back to update — see validationCode */ }

Prevention

When it happens

Trigger: Calling AddOptionAsync twice with the same Key; importing options that already exist; a check-then-insert sequence where another caller inserted the key between the OptionKeyExists check and the INSERT (the check and insert are not atomic — no transaction or lock wraps them).

Common situations: Re-running a data-seeding routine without clearing first; importing a schema export on top of existing data; concurrent calls from multiple threads/tasks hitting the same SQLite connection.

Related errors


AI-assisted analysis of mRemoteNG/mRemoteNG@9211babf35 (2026-08-13). Data as JSON: /api/errors/42cbd028d2f62df1. Report an issue: GitHub.