{"record":{"id":"60472323df079967","repo":"elsa-workflows/elsa-core","slug":"a-secret-named-name-already-exists","errorCode":null,"errorMessage":"A secret named '{name}' already exists.","messagePattern":"A secret named '(.+?)' already exists\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.Secrets.Persistence.EFCore/Repositories/EFCoreSecretRepository.cs","lineNumber":134,"sourceCode":"        return dbContext.Secrets.AnyAsync(x => EF.Property<string>(x, SecretShadowPropertyNames.NormalizedName) == normalizedName, cancellationToken);\n    }\n\n    // The DbUpdateException-to-name-conflict translation below relies on the (TenantId, NormalizedName)\n    // unique index, which only covers rows with a non-null TenantId (SQL Server filters null rows out of the\n    // index; SQLite/PostgreSQL/MySQL treat nulls as distinct — Oracle alone rejects null-tenant duplicates).\n    // With multitenancy disabled nothing assigns a TenantId, so this backstop never fires there and\n    // uniqueness rests solely on the FindByNameAsync/ExistsByNormalizedNameAsync pre-checks — two concurrent\n    // creates racing past the pre-check both commit. See doc/migrations/secrets-tenancy.md.\n    private async Task SaveChangesAsync(SecretsElsaDbContext dbContext, string name, CancellationToken cancellationToken)\n    {\n        try\n        {\n            await dbContext.SaveChangesAsync(cancellationToken);\n        }\n        catch (DbUpdateException e)\n        {\n            if (await IsNameConflictAsync(name, cancellationToken))\n                throw new InvalidOperationException($\"A secret named '{name}' already exists.\", e);\n\n            throw;\n        }\n    }\n\n    private async Task<bool> TrySaveChangesAsync(SecretsElsaDbContext dbContext, string name, CancellationToken cancellationToken)\n    {\n        try\n        {\n            await dbContext.SaveChangesAsync(cancellationToken);\n            return true;\n        }\n        catch (DbUpdateException)\n        {\n            if (await IsNameConflictAsync(name, cancellationToken))\n                return false;\n\n            throw;","sourceCodeStart":116,"sourceCodeEnd":152,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.Secrets.Persistence.EFCore/Repositories/EFCoreSecretRepository.cs#L116-L152","documentation":"SaveChangesAsync catches DbUpdateException from EF Core's SaveChangesAsync and, when a name-conflict check confirms the failure was caused by a duplicate (unique index on the normalized name), throws this friendlier InvalidOperationException including the offending name, attaching the original DbUpdateException as the inner exception. If it is not a name conflict, the original exception is rethrown.","triggerScenarios":"Two concurrent AddAsync calls (or an Add/Save racing another host/instance) pass the ExistsByNormalizedNameAsync pre-check but the database's unique constraint rejects the second insert; or SaveAsync renames a secret to a name another row already uses.","commonSituations":"Multi-instance deployments where the in-process existence check cannot see another node's in-flight insert; retry pipelines replaying an Add after a timeout where the first insert actually committed; renaming a secret to a name already taken.","solutions":["Catch this InvalidOperationException from AddAsync/SaveAsync and convert to an update (GetAsync + SaveAsync) or inform the user the name is taken.","For concurrency, serialize secret creation (idempotent 'add or get' logic) or use a transaction/unique-name upsert strategy.","Ensure only one host writes secrets, or rely on the DB unique index plus this error path as the arbiter.","If the error recurs unexpectedly, inspect the inner DbUpdateException to confirm it is the unique-index violation and not another constraint."],"exampleFix":"// before\nawait repository.AddAsync(new Secret { Name = name }); // race: insert may conflict\n// after\ntry\n{\n    await repository.AddAsync(new Secret { Name = name });\n}\ncatch (InvalidOperationException) // duplicate name\n{\n    var existing = await repository.GetAsync(name, ct);\n    existing.Value = value;\n    await repository.SaveAsync(existing, ct);\n}","handlingStrategy":"try-catch","validationCode":null,"typeGuard":null,"tryCatchPattern":"try\n{\n    await repository.AddAsync(secret, cancellationToken);\n}\ncatch (InvalidOperationException ex)\n    when (ex.Message.StartsWith(\"A secret named '\") && ex.InnerException is Microsoft.EntityFrameworkCore.DbUpdateException)\n{\n    logger.LogWarning(ex, \"Concurrent creation of secret '{Name}' detected; falling back to update.\", secret.Name);\n    var existing = await repository.GetAsync(secret.Name, ct);\n    existing.Value = secret.Value;\n    await repository.SaveAsync(existing, ct);\n}","preventionTips":["Design add-or-update flows that tolerate the unique-index conflict as the arbiter.","Avoid retrying AddAsync blindly after timeouts; verify whether the insert committed.","Keep secrets writes on a single writer, or wrap in transactions where possible."],"tags":["duplicate","concurrency","efcore","secrets","conflict"],"backgroundTag":"database-write-failed","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}