elsa-workflows/elsa-core · error · InvalidOperationException
A secret named ' ' already exists.
Error message
A secret named '{name}' already exists. What it means
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.
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.
Example fix
// before
await repository.AddAsync(new Secret { Name = name }); // race: insert may conflict
// after
try
{
await repository.AddAsync(new Secret { Name = name });
}
catch (InvalidOperationException) // duplicate name
{
var existing = await repository.GetAsync(name, ct);
existing.Value = value;
await repository.SaveAsync(existing, ct);
} Defensive patterns
Strategy: try-catch
Try / catch
try
{
await repository.AddAsync(secret, cancellationToken);
}
catch (InvalidOperationException ex)
when (ex.Message.StartsWith("A secret named '") && ex.InnerException is Microsoft.EntityFrameworkCore.DbUpdateException)
{
logger.LogWarning(ex, "Concurrent creation of secret '{Name}' detected; falling back to update.", secret.Name);
var existing = await repository.GetAsync(secret.Name, ct);
existing.Value = secret.Value;
await repository.SaveAsync(existing, ct);
} Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- A secret named ' ' already exists.
- A secret named ' ' already exists.
- A secret named ' ' already exists.
- A secret named ' ' already exists.
- A secret named ' ' already exists.
AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13).
Data as JSON: /api/errors/60472323df079967.
Report an issue: GitHub.
Appendix: source
Thrown at src/modules/Elsa.Secrets.Persistence.EFCore/Repositories/EFCoreSecretRepository.cs:134
return dbContext.Secrets.AnyAsync(x => EF.Property<string>(x, SecretShadowPropertyNames.NormalizedName) == normalizedName, cancellationToken);
}
// The DbUpdateException-to-name-conflict translation below relies on the (TenantId, NormalizedName)
// unique index, which only covers rows with a non-null TenantId (SQL Server filters null rows out of the
// index; SQLite/PostgreSQL/MySQL treat nulls as distinct — Oracle alone rejects null-tenant duplicates).
// With multitenancy disabled nothing assigns a TenantId, so this backstop never fires there and
// uniqueness rests solely on the FindByNameAsync/ExistsByNormalizedNameAsync pre-checks — two concurrent
// creates racing past the pre-check both commit. See doc/migrations/secrets-tenancy.md.
private async Task SaveChangesAsync(SecretsElsaDbContext dbContext, string name, CancellationToken cancellationToken)
{
try
{
await dbContext.SaveChangesAsync(cancellationToken);
}
catch (DbUpdateException e)
{
if (await IsNameConflictAsync(name, cancellationToken))
throw new InvalidOperationException($"A secret named '{name}' already exists.", e);
throw;
}
}
private async Task<bool> TrySaveChangesAsync(SecretsElsaDbContext dbContext, string name, CancellationToken cancellationToken)
{
try
{
await dbContext.SaveChangesAsync(cancellationToken);
return true;
}
catch (DbUpdateException)
{
if (await IsNameConflictAsync(name, cancellationToken))
return false;
throw;View on GitHub (pinned to fe9217bdfa)