{"record":{"id":"7b9d054dd913aba7","repo":"stride3d/stride","slug":"this-transaction-has-already-been-completed-dummytransaction","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/presentation/Stride.Core.Presentation/Services/DummyTransaction.cs","lineNumber":27,"sourceCode":"/// A dummy transaction created when <see cref=\"IUndoRedoService.UndoRedoInProgress\"/> is true and a new transaction is requested.\n/// Any operation pushed during this transaction will throw.\n/// </summary>\ninternal class DummyTransaction : ITransaction, IReadOnlyTransaction\n{\n    private bool isCompleted;\n\n    public Guid Id { get; } = Guid.NewGuid();\n\n    public IReadOnlyList<Operation> Operations { get; } = [];\n\n    public bool IsEmpty => true;\n\n    public TransactionFlags Flags => TransactionFlags.None;\n\n    public void Dispose()\n    {\n        if (isCompleted)\n            throw new TransactionException(\"This transaction has already been completed.\");\n\n        Complete();\n    }\n\n    public void Continue()\n    {\n    }\n\n    public void Complete()\n    {\n        if (isCompleted)\n            throw new TransactionException(\"This transaction has already been completed.\");\n\n        isCompleted = true;\n    }\n\n    public void AddReference()\n    {","sourceCodeStart":9,"sourceCodeEnd":45,"githubUrl":"https://github.com/stride3d/stride/blob/96fad776d210c221682aac1ccdf4c79dc046fc38/sources/presentation/Stride.Core.Presentation/Services/DummyTransaction.cs#L9-L45","documentation":"DummyTransaction.Dispose completes the transaction, but a transaction can only be completed once. If isCompleted is already true, Dispose throws TransactionException instead of silently re-completing. This guards the invariant that each transaction lifecycle ends exactly once.","triggerScenarios":"Calling Dispose twice on the same DummyTransaction (e.g. 'using' plus an explicit Dispose call, or two nested using blocks over the same instance). The test Dispose_CalledMultipleTimes_DoesNotThrow exercises this path.","commonSituations":"Double-dispose patterns: wrapping an already-managed transaction in another using, disposing in both a finally block and a using, or re-disposing a cached transaction object.","solutions":["Dispose each transaction exactly once","Track completion with isCompleted before calling Dispose again","Wrap Dispose in a guard that checks/sets a disposed flag","Prefer a single using statement over manual Dispose calls"],"exampleFix":"// before\ntransaction.Dispose();\ntransaction.Dispose(); // throws TransactionException\n// after\nif (transaction is DummyTransaction dt && !dt.IsCompleted) transaction.Dispose();","handlingStrategy":"validation","validationCode":"if (transaction is DummyTransaction dt && dt.IsCompletedForTest) return; // skip double dispose\ntransaction?.Dispose();","typeGuard":"static bool CanDispose(DummyTransaction t) => !t.IsCompleted;","tryCatchPattern":"try { transaction.Dispose(); }\ncatch (TransactionException) { /* already completed — treat as success */ }","preventionTips":["Use a single using statement per transaction","Never mix manual Complete() with using/Dispose","Keep transaction ownership in one code path"],"tags":["transaction","dispose","double-dispose","lifecycle"],"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"}