microsoft/aspire · error · InvalidOperationException

Toolbox ' ' changed concurrently: target version ' ' is no…

Error message

Toolbox '{name}' changed concurrently: target version '{version}' is no longer visible. No further changes were made.

What it means

ValidatePromotionTarget confirms, before validating ownership, that the version Aspire just promoted still exists in the Toolbox's version list when the post-promotion state is read. If the target version was deleted concurrently, the reconciler throws this error and makes no further changes. It prevents Aspire from validating or reporting success for a version that no longer exists.

Solutions

  1. Re-run the deploy so Aspire re-publishes the version and completes the promotion
  2. Check whether a cleanup/retention job deletes Toolbox versions during deploys and reschedule or exclude Aspire-managed versions
  3. Coordinate with team members to avoid deleting versions while a deployment is running
  4. Serialize reconciles per environment with a pipeline concurrency group

Example fix

// before: cleanup job deletes versions older than 1 day, deploy promotes 1.3.0 published moments ago

// after: exclude the version being deployed from cleanup
// cleanup filter: delete versions NOT matching current AppHost-declared version
#pragma warning disable ASPIREFOUNDRY001
cbuilder.AddFoundryToolbox("my-toolbox", version: "1.3.0");
Defensive patterns

Strategy: retry

Validate before calling

// Confirm the target version exists before deploying
var state = await administration.GetAsync(definition.Name, ct);
bool versionVisible = state?.Versions.Any(v => v.Version == definition.Version) == true;
if (state is not null && !versionVisible) { /* version was deleted; let reconcile republish */ }

Type guard

bool VersionExists(FoundryToolboxState? state, string version) =>
    state?.Versions.Any(v => string.Equals(v.Version, version, StringComparison.Ordinal)) == true;

Try / catch

try
{
    await reconciler.ReconcileAsync(definition, ct);
}
catch (ConcurrentChangeException)
{
    // Target version was deleted mid-flight; safe to re-run so it is republished.
    await reconciler.ReconcileAsync(definition, ct);
}

Prevention

When it happens

Trigger: PromoteOwnedVersionAsync calls ValidatePromotionTarget with the promoted version string, and state.Versions contains no entry whose Version matches — i.e., the version was deleted by another actor between promotion and the post-promotion GetAsync read.

Common situations: A retention/cleanup policy or pipeline deletes old Toolbox versions while a deploy promotes one; another developer removes the version in the portal mid-deploy; a racing Aspire reconcile deletes the version it previously published.

Related errors


AI-assisted analysis of microsoft/aspire@25830f84bd (2026-09-16). Data as JSON: /api/errors/461f3f935c25f723. Report an issue: GitHub.

Appendix: source

Thrown at src/Aspire.Hosting.Foundry/Toolbox/FoundryToolboxReconciler.cs:492

            throw CreateConcurrentChangeException(
                name,
                $"default version '{state.DefaultVersion}' is not managed by Aspire");
        }

        throw new InvalidOperationException(
            $"Toolbox '{name}' already exists but is not managed by Aspire. No changes were made.");
    }

    private static void ValidatePromotionTarget(
        FoundryToolboxDeploymentDefinition definition,
        FoundryToolboxState state,
        string version)
    {
        var target = state.Versions.FirstOrDefault(candidate =>
            string.Equals(candidate.Version, version, StringComparison.Ordinal));
        if (target is null)
        {
            throw CreateConcurrentChangeException(
                definition.Name,
                $"target version '{version}' is no longer visible");
        }

        if (!IsAspireManaged(target) ||
            !target.Metadata.TryGetValue(
                FoundryToolboxDeploymentDefinition.ConfigurationHashMetadataKey,
                out var configurationHash) ||
            !string.Equals(configurationHash, definition.ConfigurationHash, StringComparison.Ordinal))
        {
            throw CreateConcurrentChangeException(
                definition.Name,
                $"target version '{version}' no longer has the expected Aspire ownership and configuration");
        }
    }

    private static bool IsAspireManaged(FoundryToolboxVersionState version) =>
        version.Metadata.TryGetValue(

View on GitHub (pinned to 25830f84bd)