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.05metadata:6 author: CrestApps Team7 version: "1.0"8---910# Orchard Core Deferred Tasks and After-Request Background Jobs1112Orchard Core provides two complementary ways to run code *after* the main work13of a request, without adding a scheduled `IBackgroundTask`. Both live in the14`OrchardCore.Abstractions` package, so any module can use them.1516- `ShellScope.AddDeferredTask` — runs a delegate in a **fresh isolated scope**17 once the current shell scope tears down, **after the YesSql session has been18 committed**. Use it to react to committed data (indexing, cache busting,19 sending a signal) and to see your own just-saved changes.20- `HttpBackgroundJob.ExecuteAfterEndOfRequestAsync` — truly **fire-and-forget**.21 It returns immediately, waits for the current `HttpContext` to be released22 (end of the HTTP response), reloads the shell, restores the current user, and23 runs the job in an isolated scope. Use it for longer work you do not want the24 client to wait for.2526## When to use which2728| Need | Use |29|---|---|30| Run right after commit, still part of request teardown, client waits | `ShellScope.AddDeferredTask` |31| Run after the response is sent, do not block the client | `HttpBackgroundJob.ExecuteAfterEndOfRequestAsync` |32| Only send an invalidation signal after commit | `ShellScope.AddDeferredSignal` |33| Run something as the scope disposes (no new scope) | `ShellScope.RegisterBeforeDispose` |3435## Why "after the session is saved" works3637The YesSql/document session commit is registered as a **before-dispose**38callback on the shell scope. During scope teardown Orchard Core runs, in order:39401. `RegisterBeforeDispose` callbacks — this is where `IDocumentStore.CommitAsync()`41 commits the session.422. Deferred **signals** added via `AddDeferredSignal`.433. Deferred **tasks** added via `AddDeferredTask`, each in its **own new scope**44 (with a fresh session) built from the reloaded shell.4546Because the commit happens in step 1 and deferred tasks run in step 3, a47deferred task always observes committed data and never fights the request's48open transaction. Kicking off the background job **from inside a deferred task**49guarantees the job is only scheduled after a successful commit.5051## Fire-and-forget after the request5253```csharp54using OrchardCore.BackgroundJobs;5556// Inside a controller action, driver, or handler:57await HttpBackgroundJob.ExecuteAfterEndOfRequestAsync(58 "send-welcome-email",59 async scope =>60 {61 // 'scope' is a fresh, isolated ShellScope. Resolve services from it;62 // never capture request-scoped services from the outer scope.63 var emailService = scope.ServiceProvider.GetRequiredService<IEmailService>();6465 await emailService.SendWelcomeAsync();66 });67```6869Pass captured state through the typed overloads instead of closures over70request-scoped services:7172```csharp73await HttpBackgroundJob.ExecuteAfterEndOfRequestAsync(74 "reindex-item",75 contentItemId,76 static async (scope, id) =>77 {78 var manager = scope.ServiceProvider.GetRequiredService<IContentManager>();79 var item = await manager.GetAsync(id);80 // ... process the freshly loaded, committed item81 });82```8384## Defer until after commit, then fire-and-forget8586This is the recommended pattern when the background work must only happen if the87current unit of work is actually persisted. Register a deferred task; inside it,88schedule the after-request job.8990```csharp91using OrchardCore.BackgroundJobs;92using OrchardCore.Environment.Shell.Scope;9394// e.g. inside a content handler or controller, after mutating data:95ShellScope.AddDeferredTask(async scope =>96{97 // Runs in a NEW scope AFTER the session has been committed.98 await HttpBackgroundJob.ExecuteAfterEndOfRequestAsync(99 "process-order",100 orderId,101 static async (jobScope, id) =>102 {103 var orders = jobScope.ServiceProvider.GetRequiredService<IOrderService>();104 await orders.ProcessAsync(id);105 });106});107```108109## Guidelines110111- Both APIs run outside the original request-scoped services. **Resolve every112 dependency from the provided `scope.ServiceProvider`**; do not close over the113 ambient `IServiceProvider`, `HttpContext`, or request-scoped services.114- Pass identifiers or immutable values, then **re-load entities** inside the job115 so you read committed state.116- `ExecuteAfterEndOfRequestAsync` requires an active `HttpContext`; if there is117 no HTTP context it logs a warning and does nothing. For non-HTTP flows118 (background tasks, CLI, setup) use a deferred task or run the work directly.119- The after-request job **waits up to 60 seconds** for the current `HttpContext`120 to be released before running; keep controller work bounded.121- The after-request job restores the **current user principal**, so authorization122 checks inside the job reflect the requesting user.123- Make the work **idempotent**. Fire-and-forget jobs are not retried and are not124 durable across an app restart; for guaranteed delivery use a real queue.125- Exceptions inside either callback are caught and logged, not surfaced to the126 client; add your own logging and compensation.127- No feature needs to be enabled — these are framework primitives in128 `OrchardCore.Abstractions`, referenced transitively by modules.129- All C# classes in examples are sealed except View Models.130131See `references/deferred-tasks-examples.md` for content-handler, controller, and132signal-invalidation examples.