{"record":{"id":"40cb9902db011794","repo":"stride3d/stride","slug":"this-transaction-has-already-been-completed","errorCode":null,"errorMessage":"This transaction has already been completed.","messagePattern":"This transaction has already been completed\\.","errorType":"exception","errorClass":"TransactionException","httpStatus":null,"severity":"error","filePath":"sources/core/Stride.Core.Design/Transactions/Transaction.cs","lineNumber":64,"sourceCode":"    }\n\n    /// <inheritdoc/>\n    public void Continue()\n    {\n        synchronizationContext = SynchronizationContext.Current;\n    }\n\n    /// <inheritdoc/>\n    public void AddReference()\n    {\n        referenceCount++;\n    }\n\n    /// <inheritdoc/>\n    public void Complete()\n    {\n        if (referenceCount == 0)\n            throw new TransactionException(\"This transaction has already been completed.\");\n\n        // Transaction might be kept alive by others, only process it if last reference\n        // Note: this KeepAlive() and Complete() are not thread-safe, no need to use interlocked\n        if (referenceCount == 1)\n        {\n            // Disabling synchronization context check: when we await for dispatcher task we always resume in a different SC so it makes it difficult to enforce this rule.\n            //if (synchronizationContext != SynchronizationContext.Current)\n            //    throw new TransactionException(\"This transaction is being completed in a different synchronization context.\");\n\n            TryMergeOperations();\n            transactionStack.CompleteTransaction(this);\n            // Don't keep reference to synchronization context after completion\n            synchronizationContext = null;\n        }\n\n        --referenceCount;\n    }\n","sourceCodeStart":46,"sourceCodeEnd":82,"githubUrl":"https://github.com/stride3d/stride/blob/96fad776d210c221682aac1ccdf4c79dc046fc38/sources/core/Stride.Core.Design/Transactions/Transaction.cs#L46-L82","documentation":"Thrown by Transaction.Complete when the transaction's referenceCount has already reached zero, i.e. the transaction was already completed. Complete decrements the reference count and finishes the transaction when the last reference is released.","triggerScenarios":"Calling Transaction.Complete() twice on the same transaction — commonly calling both Dispose() and Complete(), or keeping shared references and completing from multiple code paths.","commonSituations":"Using statements combined with explicit Complete() calls, transaction objects shared between services that each call Complete.","solutions":["Call Complete() exactly once per transaction; do not also call it from Dispose paths you control","Track completion with a bool flag before calling Complete again","If sharing, have a single owner responsible for completing the transaction"],"exampleFix":"// before\nusing var transaction = stack.CreateTransaction();\ntransaction.Complete(); // then Dispose path may complete again\n// after\nvar transaction = stack.CreateTransaction();\ntry { /* edits */ }\nfinally { transaction.Dispose(); } // single completion path","handlingStrategy":"try-catch","validationCode":"if (completed) return;\ncompleted = true;\ntransaction.Complete();","typeGuard":"bool CanComplete(Transaction t) => !completedFlags.GetValueOrDefault(t, false);","tryCatchPattern":"try { transaction.Complete(); }\ncatch (TransactionException ex) when (ex.Message == \"This transaction has already been completed.\")\n{\n    // already completed elsewhere; treat as no-op\n}","preventionTips":["Call Complete() exactly once per transaction; prefer a single Dispose-based completion path","Designate one owner responsible for completing shared transactions","Track completion state locally before calling Complete"],"tags":["transaction","complete","double-dispose"],"backgroundTag":"invalid-state-transition","analyzedSha":"96fad776d210c221682aac1ccdf4c79dc046fc38","analyzedAt":"2026-09-14T02:59:31.279Z","contentChangedAt":"2026-09-14T02:59:31.279Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}