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();
}| Member | Description |
|---|---|
Current | The 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
| Situation | Current |
|---|---|
Inside OnQueue for a train that does not defer promotion | The 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 DeferQueuePromotion | Null. 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 else | Null. 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
SaveChangesor commit onCurrent. The enqueue owns the lifetime. - It covers only entities in Trax's own model. A hook writing through its own
DbContexthas its own connection and transaction, and gets no atomicity from this; useDeferQueuePromotioninstead.
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