OrchardCMS/OrchardCore · error · InvalidOperationException
'IOptionsUpdateNotifier' can only be used from an active…
Error message
'IOptionsUpdateNotifier' can only be used from an active shell scope.
What it means
DefaultOptionsUpdateNotifier batches options-change notifications and commits them when the current shell scope completes. It depends on ShellScope.Current being the ambient scope; calling RequestUpdate outside any shell scope has nowhere to attach the commit hook, so it throws InvalidOperationException immediately.
Solutions
- Wrap the call in a shell scope: await _shellScopeManager.GetScopeAsync(shellContext).UsingScopeAsync(scope => notifier.RequestUpdate<TOptions>());
- Resolve IOptionsUpdateNotifier from the scoped shell service provider, not the root provider.
- For background work, use IShellHost to create a scope for the target tenant before notifying.
- Defer the notification to an event that fires within a request/scope context (e.g., after content save).
Example fix
// before
await Task.Run(() => _notifier.RequestUpdate<MyOptions>()); // no active shell scope
// after
await _shellHost.GetScopeAsync(shellContext)
.ContinueWith(t => t.Result.UsingScopeAsync(scope =>
scope.ServiceProvider.GetRequiredService<IOptionsUpdateNotifier>().RequestUpdate<MyOptions>())); Defensive patterns
Strategy: try-catch
Validate before calling
if (ShellScope.Current is null)
throw new InvalidOperationException("RequestUpdate requires an active shell scope; use IShellHost to create one."); Type guard
bool CanNotifyOptionsUpdate() => ShellScope.Current is not null;
Try / catch
try { notifier.RequestUpdate<MyOptions>(); }
catch (InvalidOperationException ex) when (ex.Message.Contains("active shell scope"))
{
// recreate the work inside a scope via IShellHost.CreateScopeAsync
} Prevention
- Resolve IOptionsUpdateNotifier from the scoped shell provider, never the root provider.
- In background services, open a shell scope with IShellHost before touching tenant services.
- Never capture the notifier in a singleton and use it on detached threads.
- Trigger option updates from events that fire inside a request/scope.
When it happens
Trigger: Calling IOptionsUpdateNotifier.RequestUpdate<TOptions>() from a context with no active shell scope: a background thread/BackgroundService, a singleton created before the scope, a hosted task using Task.Run, or code running before the tenant shell context is established.
Common situations: Notifying option changes from a background job or message handler without creating a shell scope; calling during app startup before tenants are initialized; unit tests instantiating the notifier directly without a ShellScope.
Understand the failure class
Background: "Invalid state transition" errors: "status must be X, actually Y", "already rejected/charging/uninstalled", "cannot ... while running" — what they mean when a library rejects your call — this error's family across 31 libraries.
Related errors
AI-assisted analysis of OrchardCMS/OrchardCore@4306c0717f (2026-09-13).
Data as JSON: /api/errors/f3877a15319c9abf.
Report an issue: GitHub.
Appendix: source
Thrown at src/OrchardCore/OrchardCore/Options/DefaultOptionsUpdateNotifier.cs:29
public sealed class DefaultOptionsUpdateNotifier : IOptionsUpdateNotifier
{
private readonly IDocumentStore _documentStore;
private readonly HashSet<OptionsUpdateRequest> _pendingUpdates = [];
private bool _commitRegistered;
private bool _commitSucceeded;
private bool _deferredTaskAdded;
public DefaultOptionsUpdateNotifier(IDocumentStore documentStore)
{
_documentStore = documentStore;
}
public IOptionsUpdateNotifier RequestUpdate<TOptions>(string name = "") where TOptions : class
{
if (ShellScope.Current is null)
{
throw new InvalidOperationException($"'{nameof(IOptionsUpdateNotifier)}' can only be used from an active shell scope.");
}
_pendingUpdates.Add(new(typeof(TOptions), name ?? Microsoft.Extensions.Options.Options.DefaultName));
if (!_commitRegistered)
{
_commitRegistered = true;
_documentStore.AfterCommitSuccess<DefaultOptionsUpdateNotifier>(() =>
{
_commitSucceeded = true;
return Task.CompletedTask;
});
}
if (_deferredTaskAdded)
{
return this;View on GitHub (pinned to 4306c0717f)