elsa-workflows/elsa-core · error · InvalidOperationException

Output converter registration

Error message

Output converter registration '{id}' must use the Converter ID as its service key.

What it means

Thrown by OutputConverterRegistry.ValidateRegistrations at startup when an output converter is registered with a service key that differs from its descriptor's Converter ID. The registry resolves converters by keyed services, so the DI service key must exactly (ordinal) match the descriptor Id.

Solutions

  1. Make the DI service key identical to descriptor.Id (use the same constant in both places).
  2. Derive the service key from the descriptor when registering: AddKeyedSingleton(descriptor.Id, implementation).
  3. If using Elsa's registration helpers, pass the descriptor itself so key and Id stay in sync.

Example fix

// before
services.AddKeyedSingleton<IOutputConverter, IntToStringConverter>("int-to-str");
descriptor.Id = "int-to-string";

// after
const string ConverterId = "int-to-string";
descriptor.Id = ConverterId;
services.AddKeyedSingleton<IOutputConverter, IntToStringConverter>(ConverterId);
Defensive patterns

Strategy: validation

Validate before calling

if (!string.Equals(descriptor.Id, serviceKey, StringComparison.Ordinal))
    throw new InvalidOperationException($"Service key '{serviceKey}' must equal converter Id '{descriptor.Id}'.");

Try / catch

try
{
    services.AddOutputConverters(...); // triggers ValidateRegistrations
}
catch (InvalidOperationException ex) when (ex.Message.Contains("must use the Converter ID as its service key"))
{
    logger.LogCritical(ex, "Output converter registration key mismatch");
    throw; // fail fast: misconfiguration cannot be recovered at runtime
}

Prevention

When it happens

Trigger: Registering an OutputConverterDescriptor via DI with a key such as AddKeyedSingleton(key, ...) where key != descriptor.Id; renaming the descriptor Id without updating the registration key; convention-based registration helpers producing mismatched keys.

Common situations: Copy/pasted converter registrations where the descriptor Id was changed, plugin/module registrations assembled dynamically, and refactoring after renaming a converter class.

Understand the failure class

Background: "Invalid value" and "allowed values are" config errors: what your library rejected and how to fix it — this error's family across 41 libraries.

Related errors


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

Appendix: source

Thrown at src/modules/Elsa.Workflows.Core/Services/OutputConverterRegistry.cs:53

    internal static bool IsAssignableToDestination(Type valueType, Type destinationType)
    {
        if (destinationType.IsAssignableFrom(valueType))
            return true;

        var underlyingDestinationType = Nullable.GetUnderlyingType(destinationType);
        return underlyingDestinationType?.IsAssignableFrom(valueType) == true;
    }

    private static void ValidateRegistrations(IReadOnlyCollection<OutputConverterRegistration> registrations)
    {
        foreach (var registration in registrations)
        {
            ValidateDescriptor(registration.Descriptor);
            var id = registration.Descriptor.Id;

            if (!string.Equals(id, registration.ServiceKey, StringComparison.Ordinal))
                throw new InvalidOperationException($"Output converter registration '{id}' must use the Converter ID as its service key.");
        }

        var duplicate = registrations
            .GroupBy(x => x.Descriptor.Id, StringComparer.OrdinalIgnoreCase)
            .FirstOrDefault(x => x.Count() > 1);

        if (duplicate != null)
            throw new InvalidOperationException($"Output converter ID '{duplicate.Key}' is registered more than once or differs from another registration only by case.");
    }

    internal static void ValidateDescriptor(OutputConverterDescriptor descriptor)
    {
        if (string.IsNullOrWhiteSpace(descriptor.Id))
            throw new InvalidOperationException("Output converter IDs cannot be empty.");

        if (descriptor.SourceType.ContainsGenericParameters || descriptor.ResultType.ContainsGenericParameters)
            throw new InvalidOperationException($"Output converter '{descriptor.Id}' cannot use open-generic source or result types.");
    }

View on GitHub (pinned to fe9217bdfa)