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

  1. Wrap the call in a shell scope: await _shellScopeManager.GetScopeAsync(shellContext).UsingScopeAsync(scope => notifier.RequestUpdate<TOptions>());
  2. Resolve IOptionsUpdateNotifier from the scoped shell service provider, not the root provider.
  3. For background work, use IShellHost to create a scope for the target tenant before notifying.
  4. 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

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)