IEnqueueContextAccessor

Exposes the data context an enqueue is committing on, so a train's OnQueue hook can make its side-effect part of the same transaction as the work queue row. The interface is defined in Trax.Effect.Data (namespace below); it is listed under the Mediator API because AddMediator() registers it, as a singleton, so a train registered with any lifetime may inject it.

Signature

namespace Trax.Effect.Data.Services.EnqueueContext;
 
public interface IEnqueueContextAccessor
{
    IDataContext? Current { get; }
    IDisposable Enter(IDataContext context);
    IDisposable Suppress();
}
MemberDescription
CurrentThe context the enqueue is committing on, or null when no enqueue is in progress on this async flow
Enter(context)Makes context current for this async flow until the returned scope is disposed, which restores whatever was current before. Called by the enqueue path; consumers read Current
Suppress()Makes Current null for this async flow until the returned scope is disposed, which restores whatever was current before. For the enqueue path, which has to hide an outer enqueue's context from a hook that must not join it

When Current is set

SituationCurrent
Inside OnQueue for a train that does not defer promotionThe enqueue's context. Writes tracked on it are saved and committed with the work queue row, and rolled back with it if the hook or the insert fails
Inside OnQueue for a train with DeferQueuePromotionNull. The entry is already committed before the hook runs, or, when the train is enqueued from inside another train's hook, is written in that enqueue's transaction, which the deferring hook is not meant to write into. Null in the nested case from Trax.Mediator 1.23.0; an older Trax.Mediator leaves the outer enqueue's context visible there
Anywhere elseNull. Fall back to your own context

The enqueue path enters a context only for trains that override OnQueue.

Flow and nesting

The value lives in a static AsyncLocal, so it follows the async call rather than the accessor instance or its DI scope. Every instance on the same async flow sees the same value, whichever scope or lifetime resolved it, including a singleton train's accessor or one resolved from another scope. Two enqueues running at once on one scope each see their own. An enqueue started from inside an OnQueue hook joins the outer enqueue's transaction (see OnQueue), so a nested train that does not defer promotion sees the outer enqueue's context in its own hook, and what it tracks there commits or rolls back with the outer entry. Disposing each scope restores what was current before it. Suppress() follows the same rules: it hides the context from the flow that calls it and the work that flow starts, never from a concurrent enqueue, and disposing it hands back what was there before.

Rules for the hook

  • Do not call SaveChanges or commit on Current. The enqueue owns the lifetime.
  • It covers only entities in Trax's own model. A hook writing through its own DbContext has its own connection and transaction, and gets no atomicity from this; use DeferQueuePromotion instead.

Example

public class ReserveSeatTrain(IEnqueueContextAccessor accessor)
    : ServiceTrain<ReserveSeatInput, Unit>, IReserveSeatTrain
{
    protected override async Task OnQueue(Metadata metadata, CancellationToken ct)
    {
        // No SaveChanges: the enqueue commits this with the work queue row.
        await accessor.Current!.Track(new SomeTraxEntity());
    }
}

Package

dotnet add package Trax.Effect.Data