dotnet/BenchmarkDotNet · error · InvalidOperationException
The current object {this} is frozen. Create a copy to modify
Error message
The current object {this} is frozen. Create a copy to modify. What it means
CharacteristicObject is an immutable-on-freeze tree: once Frozen is true (propagated through the Owner chain), no mutations are permitted. AssertNotFrozen guards every setter, so attempting to change a value on a Job/config that has already been frozen raises this. Freezing happens when a CharacteristicObject is attached as a child of another or when the root is explicitly frozen for use.
Source
Thrown at src/BenchmarkDotNet/Characteristics/CharacteristicObject.cs:60
sharedValues = [];
}
protected CharacteristicObject(string id) : this()
{
if (id.IsNotBlank())
{
IdCharacteristic[this] = id;
}
}
#endregion
#region Assertions
private void AssertNotFrozen()
{
if (Frozen)
{
throw new InvalidOperationException($"The current object {this} is frozen. Create a copy to modify.");
}
}
private void AssertIsRoot()
{
if (Owner != null)
{
throw new InvalidOperationException(
"The current operation allowed for root nodes only, " +
$"but the value {this} is attached to another node, {Owner}.");
}
}
private void AssertIsNonFrozenRoot()
{
AssertNotFrozen();
AssertIsRoot();
}View on GitHub (pinned to b515068b61)
Solutions
- Create a copy before modifying: BDN APIs return new unfrozen instances from With*() builder methods, so chain those instead of mutating in place.
- Capture the Job before it is added to a Config and only modify that pre-freeze reference.
- Never mutate shared/default instances (Job.Default, default configs); derive a new one from them.
- If you hold a frozen object you must change, clone it via its copy/build API first.
Example fix
// before - 'job' was already added to a frozen Config job.WithRuntime(newRuntime); // throws // after - builder methods produce a fresh, unfrozen instance var job = originalJob.WithRuntime(newRuntime);
Defensive patterns
Strategy: validation
Validate before calling
if (job.Frozen)
job = job.WithRuntime(newRuntime); // builder returns a fresh unfrozen instance
else
job.Runtime = newRuntime; Type guard
static bool IsMutable(CharacteristicObject o) => !o.Frozen;
Try / catch
try { obj.SomeSetter(value); }
catch (InvalidOperationException ex) when (ex.Message.Contains("frozen"))
{
var copy = obj.Clone(); // or rebuild via With*
copy.SomeSetter(value);
} Prevention
- Treat Job/Config objects as immutable; always use the With*() builder methods that return new instances.
- Never mutate shared/default instances (Job.Default) across runs.
- Build configs fresh per run rather than reusing a frozen one.
When it happens
Trigger: Mutating a Job instance after it has been added to a Config and the Config/runner has frozen it, editing the shared DefaultJobInstance or a resolver-provided template, or reusing a benchmark case's Job object to apply another With(...) modification.
Common situations: Calling job.WithRuntime(...) on a Job pulled from an already-built Config, sharing a static Job field across runs and modifying it between runs, or programmatically cloning a config and editing the original.
Related errors
- There is no default resolver for {characteristic.FullId}
- The current operation allowed for root nodes only, but the v
- There is no default resolver for {characteristic.FullId}
- The value {value} is not assignable to {characteristic} prop
- The argument must be an array
AI-assisted analysis of dotnet/BenchmarkDotNet@b515068b61 (2026-08-13).
Data as JSON: /api/errors/cce8776858f8322e.
Report an issue: GitHub.