{"record":{"id":"f3a3551aa98094ab","repo":"elsa-workflows/elsa-core","slug":"cannot-overwrite-an-ai-conversation-that-belongs-to-another-f3a355","errorCode":null,"errorMessage":"Cannot overwrite an AI conversation that belongs to another tenant.","messagePattern":"Cannot overwrite an AI conversation that belongs to another tenant\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/modules/Elsa.AI.Persistence.EFCore/Stores/EFCoreAIConversationStore.cs","lineNumber":42,"sourceCode":"            return conversation;\n\n        return null;\n    }\n\n    public async ValueTask SaveAsync(AIConversation conversation, CancellationToken cancellationToken = default)\n    {\n        Validate(conversation);\n        var isNew = false;\n        var record = await dbContext.Conversations.FindAsync([conversation.Id], cancellationToken);\n        if (record == null)\n        {\n            record = new AIConversationRecord { Id = conversation.Id };\n            dbContext.Conversations.Add(record);\n            isNew = true;\n        }\n        else if (!BelongsToTenant(record.TenantId, conversation.TenantId))\n        {\n            throw new InvalidOperationException(\"Cannot overwrite an AI conversation that belongs to another tenant.\");\n        }\n        else\n        {\n            ValidateUserOwnership(record, conversation);\n        }\n\n        Map(conversation, record);\n\n        try\n        {\n            await dbContext.SaveChangesAsync(cancellationToken);\n        }\n        catch (DbUpdateException e) when (isNew)\n        {\n            await RetryAsUpdateAsync(conversation, e, cancellationToken);\n        }\n    }\n","sourceCodeStart":24,"sourceCodeEnd":60,"githubUrl":"https://github.com/elsa-workflows/elsa-core/blob/fe9217bdfa0e27f0e09e45006eb6898f616e513d/src/modules/Elsa.AI.Persistence.EFCore/Stores/EFCoreAIConversationStore.cs#L24-L60","documentation":"EFCoreAIConversationStore.SaveAsync refuses to update an existing AIConversationRecord whose TenantId differs from the incoming conversation (using BelongsToTenant with null-to-default normalization). This blocks cross-tenant overwrites at the persistence layer. Thrown as InvalidOperationException from the public SaveAsync method.","triggerScenarios":"Saving a conversation whose Id exists in the database but whose TenantId differs from the stored record's TenantId (including null vs set values).","commonSituations":"Lost or default tenant context in background jobs; reusing conversation IDs across tenants in seeds/tests; tenant ID casing or normalization differences upstream.","solutions":["Match the stored TenantId when saving an existing conversation Id.","Generate a new conversation Id when the tenant changes.","Delete the old record first if a tenant change is genuinely intended.","Confirm tenant resolution in the app supplies the same TenantId used at creation."],"exampleFix":"// before\nawait store.SaveAsync(new AIConversation { Id = id, TenantId = \"tenant-b\" });\n// after\nawait store.SaveAsync(new AIConversation { Id = id, TenantId = \"tenant-a\" });","handlingStrategy":"validation","validationCode":"var existing = await dbContext.Conversations.AsNoTracking().FirstOrDefaultAsync(c => c.Id == conversation.Id);\nif (existing is not null && !Equals(existing.TenantId ?? \"\", conversation.TenantId ?? \"\"))\n    throw new InvalidOperationException(\"Tenant mismatch for \" + conversation.Id);","typeGuard":"bool BelongsToTargetTenant(AIConversationRecord? existing, AIConversation c) => existing is null || Equals(existing.TenantId ?? \"\", c.TenantId ?? \"\");","tryCatchPattern":"try { await store.SaveAsync(conversation); } catch (InvalidOperationException ex) when (ex.Message.Contains(\"another tenant\")) { logger.LogWarning(ex, \"Cross-tenant EF save blocked for {Id}\", conversation.Id); }","preventionTips":["Persist and reapply TenantId from ambient context on every save.","Generate fresh Ids when data moves between tenants.","Cover cross-tenant save scenarios in integration tests."],"tags":["dotnet","ef-core","multi-tenancy","authorization"],"backgroundTag":"permission-denied","analyzedSha":"fe9217bdfa0e27f0e09e45006eb6898f616e513d","analyzedAt":"2026-09-13T20:32:34.702Z","contentChangedAt":"2026-09-13T20:32:34.702Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}