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

  1. Catch this InvalidOperationException from AddAsync/SaveAsync and convert to an update (GetAsync + SaveAsync) or inform the user the name is taken.
  2. For concurrency, serialize secret creation (idempotent 'add or get' logic) or use a transaction/unique-name upsert strategy.
  3. Ensure only one host writes secrets, or rely on the DB unique index plus this error path as the arbiter.
  4. 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

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


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)