stride3d/stride · error · TransactionException

This transaction has already been completed.

Error message

This transaction has already been completed.

What it means

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.

Solutions

  1. Dispose each transaction exactly once
  2. Track completion with isCompleted before calling Dispose again
  3. Wrap Dispose in a guard that checks/sets a disposed flag
  4. Prefer a single using statement over manual Dispose calls

Example fix

// before
transaction.Dispose();
transaction.Dispose(); // throws TransactionException
// after
if (transaction is DummyTransaction dt && !dt.IsCompleted) transaction.Dispose();
Defensive patterns

Strategy: validation

Validate before calling

if (transaction is DummyTransaction dt && dt.IsCompletedForTest) return; // skip double dispose
transaction?.Dispose();

Type guard

static bool CanDispose(DummyTransaction t) => !t.IsCompleted;

Try / catch

try { transaction.Dispose(); }
catch (TransactionException) { /* already completed — treat as success */ }

Prevention

When it happens

Trigger: 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.

Common situations: 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.

Understand the failure class

Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.

Related errors


AI-assisted analysis of stride3d/stride@96fad776d2 (2026-09-14). Data as JSON: /api/errors/7b9d054dd913aba7. Report an issue: GitHub.

Appendix: source

Thrown at sources/presentation/Stride.Core.Presentation/Services/DummyTransaction.cs:27

/// A dummy transaction created when <see cref="IUndoRedoService.UndoRedoInProgress"/> is true and a new transaction is requested.
/// Any operation pushed during this transaction will throw.
/// </summary>
internal class DummyTransaction : ITransaction, IReadOnlyTransaction
{
    private bool isCompleted;

    public Guid Id { get; } = Guid.NewGuid();

    public IReadOnlyList<Operation> Operations { get; } = [];

    public bool IsEmpty => true;

    public TransactionFlags Flags => TransactionFlags.None;

    public void Dispose()
    {
        if (isCompleted)
            throw new TransactionException("This transaction has already been completed.");

        Complete();
    }

    public void Continue()
    {
    }

    public void Complete()
    {
        if (isCompleted)
            throw new TransactionException("This transaction has already been completed.");

        isCompleted = true;
    }

    public void AddReference()
    {

View on GitHub (pinned to 96fad776d2)