4. Runtime Pipeline
Read this chapter's framing note before the diagrams. This framework's own architecture audit (finding AP-01, see §20 — Appendix) found that the pipeline's own XML doc comment describes a richer, fully-staged execution sequence than the shipped code actually implements. This chapter documents both: what
IBackgroundJobExecutionPipelineactually does today (§4.2), and the fuller conceptual stage sequence a job author can manually compose around it (§4.3) — clearly labeled as manual composition, not automatic pipeline behavior, because presenting it otherwise would misrepresent the current source.
4.1 The concepts, in the order the user-facing stage sequence is usually described
Scheduler → Pipeline → Execution Context → Tenant Context → Distributed Lock → Executor → Adapter → Business Service → Result → Completion
Every one of these ten concepts is a real, working, tested component in this framework. What this section exists to make precise is which of these steps happen automatically inside the pipeline, and which ones a job author must wire up explicitly — because the framework's own pipeline does not chain all ten automatically.
4.2 What IBackgroundJobExecutionPipeline.RunAsync actually does (verified from source)
That is the entire body of the shipped BackgroundJobExecutionPipeline.RunAsync: a cancellation
fast-path, a lifetime timer that is started but whose elapsed value is never consumed by anything, an
invocation of the caller-supplied next delegate, and exception classification that computes a category and
then discards it before re-throwing. It does not itself resolve or call IBackgroundJobTenantContext,
IBackgroundJobDistributedLock, IBackgroundJobLoggingAdapter, or IBackgroundJobNotificationAdapter
anywhere. Those are genuine, working services — they simply are not automatically chained around every
execution by the pipeline itself.
4.3 The fuller conceptual sequence — manually composed by the caller, not automatic
What is genuinely automatic, for every job routed through the pipeline: context construction (by the bridge, always required), the pipeline's cancellation/classification/re-throw behavior, and the executor's call into the job adapter.
What is opt-in, composed manually per call site, and currently unused by every one of the six migrated
production jobs: tenant context resolution and distributed-lock acquisition. Both are fully working,
independently tested capabilities — they are simply not needed by any job migrated so far, since all six
operate host-level (tenantId: null) and rely on a business-layer atomic claim (ClaimAsync) rather than a
job-level lock (see §9.4). A
future job that genuinely needs per-tenant scoping or a coarse job-level mutual exclusion would compose these
around its own pipeline.RunAsync call explicitly, following the E2E test suite's own
RuntimeTestHelpers.RunPipelineAsync pattern as the reference example.
4.4 The result and completion model
BackgroundJobExecutionResult is a sealed record:
(bool Success, BackgroundJobStatus Status, DateTimeOffset CompletedOn, string? ErrorMessage = null, IReadOnlyDictionary<string,string>? Data = null), with static factories Succeeded(data?), Failed(msg),
Cancelled(), RetryRequired(reason), and PermanentFailure(msg).
RetryRequired/PermanentFailure exist as forward-looking hooks — no code in either repository currently
calls them outside unit tests, since Hangfire's own AutomaticRetry state machine is what actually decides
whether to retry, driven by whether an exception escaped, not by inspecting this result's Status.
4.5 Registration invariant that keeps this honest
IBackgroundJobExecutionPipeline, IBackgroundJobScheduler, IBackgroundJobTenantContext, and
IBackgroundJobDistributedLock are all registered in DI (AddBackgroundJobsFrameworkCore), so they are
always available to be composed. Whether a given job composes them is a decision made in that job's
*PipelineJob bridge class — never something the framework itself decides on the job's behalf. This is a
deliberate consequence of the framework owning generic capability, not job-specific policy — see
§1.4.