{"record":{"id":"42cbd028d2f62df1","repo":"mRemoteNG/mRemoteNG","slug":"an-option-with-key-option-key-already-exists-42cbd0","errorCode":null,"errorMessage":"An option with key '{option.Key}' already exists.","messagePattern":"An option with key '(.+?)' already exists\\.","errorType":"validation","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"mRemoteNG/Config/Settings/Store/OptionsStore.cs","lineNumber":238,"sourceCode":"\n        /// <summary>\n        /// Adds a new option to the store.\n        /// </summary>\n        public async Task<OptionInfo> AddOptionAsync(OptionInfo option)\n        {\n            if (option == null)\n                throw new ArgumentNullException(nameof(option));\n            if (string.IsNullOrWhiteSpace(option.Key))\n                throw new ArgumentException(\"Option key cannot be null or whitespace.\", nameof(option));\n\n            EnsureReady();\n\n            return await Task.Run(() =>\n            {\n                // Check if option key already exists\n                if (OptionKeyExists(option.Key))\n                {\n                    throw new InvalidOperationException($\"An option with key '{option.Key}' already exists.\");\n                }\n\n                const string sql = \"\"\"\n                    INSERT INTO options (key, value, category, description, option_type, created_at, modified_at)\n                    VALUES (@key, @value, @category, @description, @option_type, @created_at, @modified_at);\n                    SELECT last_insert_rowid();\n                    \"\"\";\n\n                using SqliteCommand cmd = _connection.CreateCommand();\n                cmd.CommandText = sql;\n                cmd.Parameters.AddWithValue(\"@key\", option.Key);\n                cmd.Parameters.AddWithValue(\"@value\", option.Value ?? (object)DBNull.Value);\n                cmd.Parameters.AddWithValue(\"@category\", option.Category ?? (object)DBNull.Value);\n                cmd.Parameters.AddWithValue(\"@description\", option.Description ?? (object)DBNull.Value);\n                cmd.Parameters.AddWithValue(\"@option_type\", option.OptionType ?? \"string\");\n                cmd.Parameters.AddWithValue(\"@created_at\", DateTime.UtcNow.ToString(\"o\", CultureInfo.InvariantCulture));\n                cmd.Parameters.AddWithValue(\"@modified_at\", DateTime.UtcNow.ToString(\"o\", CultureInfo.InvariantCulture));\n","sourceCodeStart":220,"sourceCodeEnd":256,"githubUrl":"https://github.com/mRemoteNG/mRemoteNG/blob/9211babf35209d6171c8ff6e73bd856f11df21a5/mRemoteNG/Config/Settings/Store/OptionsStore.cs#L220-L256","documentation":"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.","triggerScenarios":"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).","commonSituations":"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.","solutions":["Call OptionExistsAsync(key) first and branch to UpdateOptionAsync when it returns true (upsert pattern).","Catch InvalidOperationException and fall back to GetOptionByKeyAsync + UpdateOptionAsync.","Clear or deduplicate data before bulk import.","For concurrent access, serialize writes through a lock or single writer task — the check-then-insert is not atomic."],"exampleFix":"// before\nawait store.AddOptionAsync(opt); // throws if key exists\n\n// after\nif (await store.OptionExistsAsync(opt.Key))\n{\n    var existing = await store.GetOptionByKeyAsync(opt.Key);\n    opt.Id = existing.Id;\n    await store.UpdateOptionAsync(opt);\n}\nelse\n{\n    await store.AddOptionAsync(opt);\n}","handlingStrategy":"validation","validationCode":"if (await store.OptionExistsAsync(option.Key))\n{\n    var existing = await store.GetOptionByKeyAsync(option.Key);\n    option.Id = existing.Id;\n    await store.UpdateOptionAsync(option);\n}\nelse\n{\n    await store.AddOptionAsync(option);\n}","typeGuard":null,"tryCatchPattern":"try { await store.AddOptionAsync(option); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"already exists\"))\n{ /* fall back to update — see validationCode */ }","preventionTips":["Implement an upsert helper that checks existence before insert.","For concurrent writers, serialize access with a lock — the check-then-insert is not atomic.","Catch InvalidOperationException specifically, not the base Exception."],"tags":["duplicate-key","sqlite","concurrency","options-store","upsert"],"backgroundTag":null,"analyzedSha":"9211babf35209d6171c8ff6e73bd856f11df21a5","analyzedAt":"2026-08-13T19:31:27.817Z","schemaVersion":2},"datasetVersion":"2026-08-14T00:17:13.853Z"}