HangfireIO/Hangfire · error · FormatException

Unable to obtain resource identifier: {ex.Message}

Error message

Unable to obtain resource identifier: {ex.Message}

What it means

DisableConcurrentExecutionAttribute.GetResource (DisableConcurrentExecutionAttribute.cs:76) formats the Resource template using the job's arguments via String.Format(Resource, job.Args.ToArray()). If the Resource string contains format placeholders ({0}, {1}) that do not align with the argument count/types, or contains malformed braces, String.Format throws and the catch re-wraps it as a FormatException with the original message.

Source

Thrown at src/Hangfire.Core/DisableConcurrentExecutionAttribute.cs:76

            {
                throw new InvalidOperationException("Can not release a distributed lock: it was not acquired.");
            }

            var distributedLock = (IDisposable)value;
            distributedLock.Dispose();
        }

        private string GetResource(Job job)
        {
            if (!String.IsNullOrWhiteSpace(Resource))
            {
                try
                {
                    return String.Format(CultureInfo.InvariantCulture, Resource, job.Args.ToArray()).ToLowerInvariant();
                }
                catch (Exception ex)
                {
                    throw new FormatException($"Unable to obtain resource identifier: {ex.Message}");
                }
            }

            return $"{job.Type.ToGenericTypeString()}.{job.Method.Name}";
        }
    }
}

View on GitHub (pinned to c236dd0f93)

Solutions

  1. Ensure the Resource format string's placeholder indices are all within the bounds of the job method's actual parameters.
  2. Escape literal braces by doubling them (use '{{' and '}}') if you need literal curly braces in the resource name.
  3. If no interpolation is needed, omit placeholders entirely and use a plain string.

Example fix

// before: method has 1 arg but template references {1}
[DisableConcurrentExecution(Resource = "lock_{0}_{1}", timeoutSec = 30)]
public void Run(int orderId) { ... }

// after
[DisableConcurrentExecution(Resource = "lock_{0}", timeoutSec = 30)]
public void Run(int orderId) { ... }
Defensive patterns

Strategy: validation

Validate before calling

// Validate the Resource template against the job method's parameter count
static void ValidateResourceTemplate(string resource, int argCount)
{
    if (string.IsNullOrEmpty(resource)) return;
    try { string.Format(resource, new object[argCount]); }
    catch (FormatException ex)
    {
        throw new FormatException(
            $"Resource template '{resource}' is invalid for {argCount} args: {ex.Message}");
    }
}

ValidateResourceTemplate("lock_{0}_{1}", argCount: 1); // throws proactively

Prevention

When it happens

Trigger: Setting [DisableConcurrentExecution(Resource = "...", ...)] with a format template whose placeholders ({0}, {1}, etc.) reference indices beyond the job method's argument count, or contain unescaped braces like '{' or '}' that are not valid format items.

Common situations: Resource template was written for a different method signature with more arguments; typo in the format string (e.g. 'lock_{0}_{5}' when there are only 2 args); a literal brace in the resource name that String.Format interprets as a placeholder.

Related errors


AI-assisted analysis of HangfireIO/Hangfire@c236dd0f93 (2026-08-13). Data as JSON: /api/errors/2dfdd81f3e4441aa. Report an issue: GitHub.