Orchard Core Deferred Tasks and After-Request Background Jobs
Orchard Core provides two complementary ways to run code after the main work
of a request, without adding a scheduled IBackgroundTask. Both live in the
OrchardCore.Abstractions package, so any module can use them.
ShellScope.AddDeferredTask — runs a delegate in a fresh isolated scope
once the current shell scope tears down, after the YesSql session has been
committed. Use it to react to committed data (indexing, cache busting,
sending a signal) and to see your own just-saved changes.
HttpBackgroundJob.ExecuteAfterEndOfRequestAsync — truly fire-and-forget.
It returns immediately, waits for the current HttpContext to be released
(end of the HTTP response), reloads the shell, restores the current user, and
runs the job in an isolated scope. Use it for longer work you do not want the
client to wait for.
When to use which
| Need |
Use |
| Run right after commit, still part of request teardown, client waits |
ShellScope.AddDeferredTask |
| Run after the response is sent, do not block the client |
HttpBackgroundJob.ExecuteAfterEndOfRequestAsync |
| Only send an invalidation signal after commit |
ShellScope.AddDeferredSignal |
| Run something as the scope disposes (no new scope) |
ShellScope.RegisterBeforeDispose |
Why "after the session is saved" works
The YesSql/document session commit is registered as a before-dispose
callback on the shell scope. During scope teardown Orchard Core runs, in order:
RegisterBeforeDispose callbacks — this is where IDocumentStore.CommitAsync()
commits the session.
- Deferred signals added via
AddDeferredSignal.
- Deferred tasks added via
AddDeferredTask, each in its own new scope
(with a fresh session) built from the reloaded shell.
Because the commit happens in step 1 and deferred tasks run in step 3, a
deferred task always observes committed data and never fights the request's
open transaction. Kicking off the background job from inside a deferred task
guarantees the job is only scheduled after a successful commit.
Fire-and-forget after the request
using OrchardCore.BackgroundJobs;
// Inside a controller action, driver, or handler:
await HttpBackgroundJob.ExecuteAfterEndOfRequestAsync(
"send-welcome-email",
async scope =>
{
// 'scope' is a fresh, isolated ShellScope. Resolve services from it;
// never capture request-scoped services from the outer scope.
var emailService = scope.ServiceProvider.GetRequiredService<IEmailService>();
await emailService.SendWelcomeAsync();
});
Pass captured state through the typed overloads instead of closures over
request-scoped services:
await HttpBackgroundJob.ExecuteAfterEndOfRequestAsync(
"reindex-item",
contentItemId,
static async (scope, id) =>
{
var manager = scope.ServiceProvider.GetRequiredService<IContentManager>();
var item = await manager.GetAsync(id);
// ... process the freshly loaded, committed item
});
Defer until after commit, then fire-and-forget
This is the recommended pattern when the background work must only happen if the
current unit of work is actually persisted. Register a deferred task; inside it,
schedule the after-request job.
using OrchardCore.BackgroundJobs;
using OrchardCore.Environment.Shell.Scope;
// e.g. inside a content handler or controller, after mutating data:
ShellScope.AddDeferredTask(async scope =>
{
// Runs in a NEW scope AFTER the session has been committed.
await HttpBackgroundJob.ExecuteAfterEndOfRequestAsync(
"process-order",
orderId,
static async (jobScope, id) =>
{
var orders = jobScope.ServiceProvider.GetRequiredService<IOrderService>();
await orders.ProcessAsync(id);
});
});
Guidelines
- Both APIs run outside the original request-scoped services. Resolve every
dependency from the provided
scope.ServiceProvider; do not close over the
ambient IServiceProvider, HttpContext, or request-scoped services.
- Pass identifiers or immutable values, then re-load entities inside the job
so you read committed state.
ExecuteAfterEndOfRequestAsync requires an active HttpContext; if there is
no HTTP context it logs a warning and does nothing. For non-HTTP flows
(background tasks, CLI, setup) use a deferred task or run the work directly.
- The after-request job waits up to 60 seconds for the current
HttpContext
to be released before running; keep controller work bounded.
- The after-request job restores the current user principal, so authorization
checks inside the job reflect the requesting user.
- Make the work idempotent. Fire-and-forget jobs are not retried and are not
durable across an app restart; for guaranteed delivery use a real queue.
- Exceptions inside either callback are caught and logged, not surfaced to the
client; add your own logging and compensation.
- No feature needs to be enabled — these are framework primitives in
OrchardCore.Abstractions, referenced transitively by modules.
- All C# classes in examples are sealed except View Models.
See references/deferred-tasks-examples.md for content-handler, controller, and
signal-invalidation examples.
1---2name: orchardcore-deferred-tasks3description: Skill for running fire-and-forget work after a request in Orchard Core using HttpBackgroundJob.ExecuteAfterEndOfRequestAsync and ShellScope.AddDeferredTask. Covers deferring work until the current YesSql session is committed, running in a fresh isolated ShellScope, restoring the current user, and choosing between deferred tasks and after-request background jobs. Use this skill when requests mention Orchard Core HttpBackgroundJob, ExecuteAfterEndOfRequestAsync, ShellScope.AddDeferredTask, AddDeferredSignal, RegisterBeforeDispose, running code after the session is saved, fire-and-forget after a request, or closely related Orchard Core setup or troubleshooting work. Strong matches include OrchardCore.BackgroundJobs, OrchardCore.Environment.Shell.Scope, OrchardCore.Abstractions, ShellScope, IShellHost, IDocumentStore, IHttpContextAccessor, and combining a deferred task with a background job so work runs only after the session commit.4license: Apache-2.05---67# Orchard Core Deferred Tasks and After-Request Background Jobs89Orchard Core provides two complementary ways to run code *after* the main work10of a request, without adding a scheduled `IBackgroundTask`. Both live in the11`OrchardCore.Abstractions` package, so any module can use them.1213- `ShellScope.AddDeferredTask` — runs a delegate in a **fresh isolated scope**14 once the current shell scope tears down, **after the YesSql session has been15 committed**. Use it to react to committed data (indexing, cache busting,16 sending a signal) and to see your own just-saved changes.17- `HttpBackgroundJob.ExecuteAfterEndOfRequestAsync` — truly **fire-and-forget**.18 It returns immediately, waits for the current `HttpContext` to be released19 (end of the HTTP response), reloads the shell, restores the current user, and20 runs the job in an isolated scope. Use it for longer work you do not want the21 client to wait for.2223## When to use which2425| Need | Use |26|---|---|27| Run right after commit, still part of request teardown, client waits | `ShellScope.AddDeferredTask` |28| Run after the response is sent, do not block the client | `HttpBackgroundJob.ExecuteAfterEndOfRequestAsync` |29| Only send an invalidation signal after commit | `ShellScope.AddDeferredSignal` |30| Run something as the scope disposes (no new scope) | `ShellScope.RegisterBeforeDispose` |3132## Why "after the session is saved" works3334The YesSql/document session commit is registered as a **before-dispose**35callback on the shell scope. During scope teardown Orchard Core runs, in order:36371. `RegisterBeforeDispose` callbacks — this is where `IDocumentStore.CommitAsync()`38 commits the session.392. Deferred **signals** added via `AddDeferredSignal`.403. Deferred **tasks** added via `AddDeferredTask`, each in its **own new scope**41 (with a fresh session) built from the reloaded shell.4243Because the commit happens in step 1 and deferred tasks run in step 3, a44deferred task always observes committed data and never fights the request's45open transaction. Kicking off the background job **from inside a deferred task**46guarantees the job is only scheduled after a successful commit.4748## Fire-and-forget after the request4950```csharp51using OrchardCore.BackgroundJobs;5253// Inside a controller action, driver, or handler:54await HttpBackgroundJob.ExecuteAfterEndOfRequestAsync(55 "send-welcome-email",56 async scope =>57 {58 // 'scope' is a fresh, isolated ShellScope. Resolve services from it;59 // never capture request-scoped services from the outer scope.60 var emailService = scope.ServiceProvider.GetRequiredService<IEmailService>();6162 await emailService.SendWelcomeAsync();63 });64```6566Pass captured state through the typed overloads instead of closures over67request-scoped services:6869```csharp70await HttpBackgroundJob.ExecuteAfterEndOfRequestAsync(71 "reindex-item",72 contentItemId,73 static async (scope, id) =>74 {75 var manager = scope.ServiceProvider.GetRequiredService<IContentManager>();76 var item = await manager.GetAsync(id);77 // ... process the freshly loaded, committed item78 });79```8081## Defer until after commit, then fire-and-forget8283This is the recommended pattern when the background work must only happen if the84current unit of work is actually persisted. Register a deferred task; inside it,85schedule the after-request job.8687```csharp88using OrchardCore.BackgroundJobs;89using OrchardCore.Environment.Shell.Scope;9091// e.g. inside a content handler or controller, after mutating data:92ShellScope.AddDeferredTask(async scope =>93{94 // Runs in a NEW scope AFTER the session has been committed.95 await HttpBackgroundJob.ExecuteAfterEndOfRequestAsync(96 "process-order",97 orderId,98 static async (jobScope, id) =>99 {100 var orders = jobScope.ServiceProvider.GetRequiredService<IOrderService>();101 await orders.ProcessAsync(id);102 });103});104```105106## Guidelines107108- Both APIs run outside the original request-scoped services. **Resolve every109 dependency from the provided `scope.ServiceProvider`**; do not close over the110 ambient `IServiceProvider`, `HttpContext`, or request-scoped services.111- Pass identifiers or immutable values, then **re-load entities** inside the job112 so you read committed state.113- `ExecuteAfterEndOfRequestAsync` requires an active `HttpContext`; if there is114 no HTTP context it logs a warning and does nothing. For non-HTTP flows115 (background tasks, CLI, setup) use a deferred task or run the work directly.116- The after-request job **waits up to 60 seconds** for the current `HttpContext`117 to be released before running; keep controller work bounded.118- The after-request job restores the **current user principal**, so authorization119 checks inside the job reflect the requesting user.120- Make the work **idempotent**. Fire-and-forget jobs are not retried and are not121 durable across an app restart; for guaranteed delivery use a real queue.122- Exceptions inside either callback are caught and logged, not surfaced to the123 client; add your own logging and compensation.124- No feature needs to be enabled — these are framework primitives in125 `OrchardCore.Abstractions`, referenced transitively by modules.126- All C# classes in examples are sealed except View Models.127128See `references/deferred-tasks-examples.md` for content-handler, controller, and129signal-invalidation examples.