Cross-cutting concerns today mean either per-handler code (every handler restores its own tenant/correlation context, times itself, wraps its own transaction) or monitoring-event subscribers, which observe but cannot wrap. There is no seam to run logic around handler execution. Given the MediatR heritage of the dispatch API, pipeline behaviors are the natural shape.
Design sketch
IEverTaskBehavior<TTask> (name TBD), registered as open generics and resolved from the delivery's own service scope in WorkerExecutor.DoWorkCore, composed around Handle with the handler innermost. Registration order = execution order.
- Wraps
Handle only: not the lifecycle callbacks, not the rate-limit gate, not storage writes. The pipeline runs inside the existing execution boundary, so a behavior that throws follows the standard failure path — retry policy included — exactly as if the handler had thrown.
- A behavior can short-circuit by not calling
next (veto/kill-switch); semantics of a vetoed run to define (proposal: completes without executing, with a monitoring event saying which behavior vetoed).
- Applies to every delivery on the executing host: one-shot, recurring occurrences, recovered tasks.
Use cases
Tenant/correlation scope restore, timing and custom metrics in one place, unit-of-work/transaction around the handler, feature-flag kill-switch, exception enrichment. The OpenTelemetry execution span (see the OTel issue) is implemented as a built-in behavior on top of this.
Distribution compatibility
Neutral by construction: behaviors run wherever the handler runs, so #59 (single consumer) and #31 (leases) are unaffected.
Cross-cutting concerns today mean either per-handler code (every handler restores its own tenant/correlation context, times itself, wraps its own transaction) or monitoring-event subscribers, which observe but cannot wrap. There is no seam to run logic around handler execution. Given the MediatR heritage of the dispatch API, pipeline behaviors are the natural shape.
Design sketch
IEverTaskBehavior<TTask>(name TBD), registered as open generics and resolved from the delivery's own service scope inWorkerExecutor.DoWorkCore, composed aroundHandlewith the handler innermost. Registration order = execution order.Handleonly: not the lifecycle callbacks, not the rate-limit gate, not storage writes. The pipeline runs inside the existing execution boundary, so a behavior that throws follows the standard failure path — retry policy included — exactly as if the handler had thrown.next(veto/kill-switch); semantics of a vetoed run to define (proposal: completes without executing, with a monitoring event saying which behavior vetoed).Use cases
Tenant/correlation scope restore, timing and custom metrics in one place, unit-of-work/transaction around the handler, feature-flag kill-switch, exception enrichment. The OpenTelemetry execution span (see the OTel issue) is implemented as a built-in behavior on top of this.
Distribution compatibility
Neutral by construction: behaviors run wherever the handler runs, so #59 (single consumer) and #31 (leases) are unaffected.