Skip to content

[Context]: add ExecuteAllInContext fork-join batch runner #16

Description

@rian-be

Summary

Add ExecuteAllInContext — an IContextManager extension that runs a list of async delegates inside one logical context scope, with per-task fork-join isolation and correct ExecutionContext/AsyncLocal flow.

Goal

Provide a safe, first-party replacement for a hand-rolled array of tasks + Task.WhenAll, so context metadata (trace/tenant/correlation) never leaks or is lost across parallel branches.

Problem

Bare Task.WhenAll does not guarantee that tasks launched in a loop keep the library's ambient context, especially under SynchronizationContext configurations. The current ContextManager only executes single actions, so there is no library-level fork-join primitive.

Scope

  • IContextManager<C>.ExecuteAllInContext(C ctx, params Func<Task>[] work) extension.
  • Per-delegate snapshotting: each task starts from its own clone of the entering context (Isolation by default, No Ambient Write).
  • Launches each delegate through manager.ExecuteInContext(snapshot, work) with a correct ExecutionContext.
  • Waits via Task.WhenAll; task scopes disposed deterministically when the batch completes.
  • Emit ContextLifecycleEventKind.Started/Completed around each branch (per [Context]: add ActivityObserver for tracing context execution #11 event contract).

Internal Loop Reference

var tasks = delegates.Select(work => Task.Run(async () =>
{
    using var scope = manager.BeginContext(snapshot);
    try { await work(); }
    catch (Exception ex) { Log(ex, snapshot.CorrelationId); }
}));
await Task.WhenAll(tasks);

Design Expectations

  • Isolation by default: child mutations never leak to siblings or the parent thread.
  • Deterministic lifetime tied to Task.WhenAll duration.
  • No global lock: the method owns no shared/global semaphore (resource control lives in the concurrency issue).
  • Resource ownership stays with the caller.
  • Child writes never propagate back unless an explicit aggregating adapter is used (Join discards).

Acceptance Criteria

  • Context flows to every branch; each branch has an isolated snapshot.
  • Child context changes do not affect siblings or the caller afterward.
  • Scopes are disposed once the batch completes.
  • Events emitted per task and testable via the lifecycle observer.
  • No behavior change for a single delegate.

Non-Goals

  • No MaxConcurrency in this issue — that is the separate ExecutionOptions concurrency issue.
  • No ordered-result preservation, no partial-success (TryEach), no graph orchestration here — separate issues.
  • No blocking/Parallel.ForEach — async-only.

Metadata

Metadata

Assignees

No one assigned

    Labels

    concurrencyParallel / batch / async executionenhancementNew feature or requestextensionExtension behaviors / helpers

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions