elsa-workflows/elsa-core · error · InvalidOperationException

A secret named ' ' already exists.

Error message

A secret named '{secret.Name}' already exists.

What it means

EFCoreSecretRepository.AddAsync normalizes the incoming secret's name and checks whether a secret with that normalized name already exists. If so, it throws this InvalidOperationException before touching the database, giving a friendly duplicate-name error (case-insensitive, per the validator's normalization).

Solutions

  1. Check existence first (ListAsync/GetAsync by name) and use SaveAsync/update instead of AddAsync when the secret already exists.
  2. Remove the existing secret if the new one should replace it, then AddAsync again.
  3. Pick a unique name for the new secret.
  4. Race conditions: catch InvalidOperationException from AddAsync/SaveChangesAsync and fall back to update semantics.

Example fix

// before
await repository.AddAsync(new Secret { Name = existingName });
// after
var existing = await repository.GetAsync(existingName, cancellationToken);
if (existing is null)
    await repository.AddAsync(new Secret { Name = existingName });
else
    existing.Value = newValue;
    await repository.SaveAsync(existing, cancellationToken);
Defensive patterns

Strategy: try-catch

Validate before calling

var existing = await repository.GetAsync(normalizedName, cancellationToken);
if (existing is not null)
    throw new InvalidOperationException($"Secret '{normalizedName}' already exists; use SaveAsync to update.");

Try / catch

try
{
    await repository.AddAsync(secret, cancellationToken);
}
catch (InvalidOperationException ex) when (ex.Message.StartsWith("A secret named '"))
{
    logger.LogWarning(ex, "Secret '{Name}' already exists.", secret.Name);
    // switch to update path or surface a duplicate-name message to the user
}

Prevention

When it happens

Trigger: Calling AddAsync with a secret whose Name matches (case-insensitively, after normalization) an existing secret row — e.g. adding "ApiKey" when "apikey" already exists.

Common situations: Retry logic re-adding a secret after a transient failure; seeding scripts that run twice; users typing a name that differs only by case from an existing secret; concurrent creation where both passed the check but only one insert wins (the other surfaces via SaveChangesAsync's variant of this error).

Understand the failure class

Background: "already exists" / EEXIST / FileAlreadyExistsException: what the 'file already exists' error means and how to fix it — this error's family across 37 libraries.

Related errors


AI-assisted analysis of elsa-workflows/elsa-core@fe9217bdfa (2026-09-13). Data as JSON: /api/errors/12f5d9fb70f213df. Report an issue: GitHub.

Appendix: source

Thrown at src/modules/Elsa.Secrets.Persistence.EFCore/Repositories/EFCoreSecretRepository.cs:34

    }

    public async Task<IReadOnlyCollection<Secret>> ListAsync(CancellationToken cancellationToken = default)
    {
        await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
        var secrets = await dbContext.Secrets.ToListAsync(cancellationToken);

        foreach (var secret in secrets)
            SecretSerialization.LoadSerializedProperties(dbContext, secret);

        return secrets;
    }

    public async Task AddAsync(Secret secret, CancellationToken cancellationToken = default)
    {
        await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
        var normalizedName = secretNameValidator.Normalize(secret.Name);
        if (await ExistsByNormalizedNameAsync(dbContext, normalizedName, cancellationToken))
            throw new InvalidOperationException($"A secret named '{secret.Name}' already exists.");

        await dbContext.Secrets.AddAsync(secret, cancellationToken);
        SetNormalizedName(dbContext, secret);
        SecretSerialization.StoreSerializedProperties(dbContext, secret);
        await SaveChangesAsync(dbContext, secret.Name, cancellationToken);
    }

    public async Task<bool> TryAddOrReplaceDeletedAsync(Secret secret, CancellationToken cancellationToken = default)
    {
        await using var dbContext = await store.CreateDbContextAsync(cancellationToken);
        var existingSecret = await FindByNameAsync(dbContext, secret.Name, cancellationToken);

        if (existingSecret == null)
        {
            await dbContext.Secrets.AddAsync(secret, cancellationToken);
            SetNormalizedName(dbContext, secret);
            SecretSerialization.StoreSerializedProperties(dbContext, secret);
            return await TrySaveChangesAsync(dbContext, secret.Name, cancellationToken);

View on GitHub (pinned to fe9217bdfa)