{"record":{"id":"f38c0d1affc9ca82","repo":"elsa-workflows/elsa-core","slug":"an-alteration-plan-with-id-plan-id-already-exists-and-is-not","errorCode":null,"errorMessage":"An alteration plan with ID '{plan.Id}' already exists and is not visible to the current tenant.","messagePattern":"An alteration plan with ID '(.+?)' already exists and is not visible to the current tenant\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.Alterations.Core/Stores/MemoryAlterationPlanStore.cs","lineNumber":76,"sourceCode":"\n    /// <remarks>\n    /// Ambient tenant is applied here rather than in <see cref=\"AlterationPlanFilter.Apply\"/>.\n    /// EF owns that via <c>SetTenantIdFilter</c>; Memory must compensate.\n    /// </remarks>\n    private IQueryable<AlterationPlan> Filter(IQueryable<AlterationPlan> query, AlterationPlanFilter filter) =>\n        filter.Apply(query.WhereVisibleToTenant(CurrentTenantId));\n\n    private string CurrentTenantId => _tenantAccessor?.TenantId ?? Tenant.DefaultTenantId;\n\n    private bool IsVisible(Entity entity) => TenantVisibility.IsVisible(entity.TenantId, CurrentTenantId);\n\n    private void EnsureIdAvailable(AlterationPlan plan)\n    {\n        var existing = _store.Find(x => x.Id == plan.Id);\n\n        if (existing is not null && !CanReplace(existing))\n        {\n            throw new InvalidOperationException(\n                $\"An alteration plan with ID '{plan.Id}' already exists and is not visible to the current tenant.\");\n        }\n    }\n\n    /// <summary>\n    /// <c>*</c> is visible to every tenant, but only an agnostic writer may replace it.\n    /// Named tenants may upsert their own visible rows.\n    /// </summary>\n    private bool CanReplace(Entity existing) =>\n        existing.TenantId == Tenant.AgnosticTenantId\n            ? CurrentTenantId == Tenant.AgnosticTenantId\n            : IsVisible(existing);\n\n    private void ApplyCurrentTenant(Entity entity)\n    {\n        if (entity.TenantId == Tenant.AgnosticTenantId || _tenantAccessor is null)\n            return;\n","sourceCodeStart":58,"sourceCodeEnd":94,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.Alterations.Core/Stores/MemoryAlterationPlanStore.cs#L58-L94","documentation":"MemoryAlterationPlanStore.EnsureIdAvailable throws when saving an alteration plan whose ID already exists in the in-memory store but is not replaceable by the current writer's tenant context. Tenant-agnostic entries ('*') are visible to all tenants but can only be replaced by an agnostic writer, so any cross-tenant or tenant-vs-agnostic collision raises this InvalidOperationException from SaveAsync.","triggerScenarios":"Calling SaveAsync with an AlterationPlan whose Id matches an existing plan where CanReplace(existing) is false (plan owned by a different tenant, or an agnostic plan being written by a tenant-scoped writer).","commonSituations":"Importing plans with hardcoded or client-provided IDs into a multi-tenant environment; retrying a save under a different tenant scope; test fixtures reusing the same plan ID across tenants.","solutions":["Use a fresh unique plan ID for the new plan.","Align the current tenant context with the existing plan's tenant to perform a legitimate update.","Write with a tenant-agnostic context if intentionally replacing an agnostic ('*') plan."],"exampleFix":"// before\nvar plan = new AlterationPlan { Id = existingId, TenantId = currentTenant };\nawait planStore.SaveAsync(plan, ct);\n\n// after\nvar plan = new AlterationPlan { Id = Guid.NewGuid().ToString(), TenantId = currentTenant };\nawait planStore.SaveAsync(plan, ct);","handlingStrategy":"try-catch","validationCode":"var existing = await planStore.FindAsync(new AlterationPlanFilter { Id = plan.Id }, ct);\nif (existing is not null && existing.TenantId != plan.TenantId)\n    throw new InvalidOperationException($\"Plan ID '{plan.Id}' already exists in another tenant scope; use a new ID.\");","typeGuard":null,"tryCatchPattern":"try\n{\n    await planStore.SaveAsync(plan, ct);\n}\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"already exists\"))\n{\n    plan.Id = Guid.NewGuid().ToString();\n    await planStore.SaveAsync(plan, ct);\n}","preventionTips":["Use auto-generated plan IDs unless importing with a deliberate, conflict-checked ID.","Verify the tenant context before saving plans in multi-tenant deployments.","For imports, check for existing IDs first and choose update-vs-create explicitly."],"tags":["in-memory-store","tenancy","id-conflict","alterations"],"backgroundTag":"file-already-exists","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-15T23:17:13.987Z"}