{"record":{"id":"f3877a15319c9abf","repo":"OrchardCMS/OrchardCore","slug":"ioptionsupdatenotifier-can-only-be-used-from-an-active-shell","errorCode":null,"errorMessage":"'IOptionsUpdateNotifier' can only be used from an active shell scope.","messagePattern":"'IOptionsUpdateNotifier' can only be used from an active shell scope\\.","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/OrchardCore/OrchardCore/Options/DefaultOptionsUpdateNotifier.cs","lineNumber":29,"sourceCode":"public sealed class DefaultOptionsUpdateNotifier : IOptionsUpdateNotifier\n{\n    private readonly IDocumentStore _documentStore;\n    private readonly HashSet<OptionsUpdateRequest> _pendingUpdates = [];\n\n    private bool _commitRegistered;\n    private bool _commitSucceeded;\n    private bool _deferredTaskAdded;\n\n    public DefaultOptionsUpdateNotifier(IDocumentStore documentStore)\n    {\n        _documentStore = documentStore;\n    }\n\n    public IOptionsUpdateNotifier RequestUpdate<TOptions>(string name = \"\") where TOptions : class\n    {\n        if (ShellScope.Current is null)\n        {\n            throw new InvalidOperationException($\"'{nameof(IOptionsUpdateNotifier)}' can only be used from an active shell scope.\");\n        }\n\n        _pendingUpdates.Add(new(typeof(TOptions), name ?? Microsoft.Extensions.Options.Options.DefaultName));\n\n        if (!_commitRegistered)\n        {\n            _commitRegistered = true;\n\n            _documentStore.AfterCommitSuccess<DefaultOptionsUpdateNotifier>(() =>\n            {\n                _commitSucceeded = true;\n                return Task.CompletedTask;\n            });\n        }\n\n        if (_deferredTaskAdded)\n        {\n            return this;","sourceCodeStart":11,"sourceCodeEnd":47,"githubUrl":"https://github.com/OrchardCMS/OrchardCore/blob/4306c0717fe573f6fca1b4955909ddab6a192807/src/OrchardCore/OrchardCore/Options/DefaultOptionsUpdateNotifier.cs#L11-L47","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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)."],"exampleFix":"// before\nawait Task.Run(() => _notifier.RequestUpdate<MyOptions>()); // no active shell scope\n\n// after\nawait _shellHost.GetScopeAsync(shellContext)\n    .ContinueWith(t => t.Result.UsingScopeAsync(scope =>\n        scope.ServiceProvider.GetRequiredService<IOptionsUpdateNotifier>().RequestUpdate<MyOptions>()));","handlingStrategy":"try-catch","validationCode":"if (ShellScope.Current is null)\n    throw new InvalidOperationException(\"RequestUpdate requires an active shell scope; use IShellHost to create one.\");","typeGuard":"bool CanNotifyOptionsUpdate() => ShellScope.Current is not null;","tryCatchPattern":"try { notifier.RequestUpdate<MyOptions>(); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"active shell scope\"))\n{\n    // recreate the work inside a scope via IShellHost.CreateScopeAsync\n}","preventionTips":["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."],"tags":["options","shell-scope","multitenancy"],"backgroundTag":"invalid-state-transition","analyzedSha":"4306c0717fe573f6fca1b4955909ddab6a192807","analyzedAt":"2026-09-13T17:41:05.024Z","contentChangedAt":"2026-09-13T17:41:05.024Z","schemaVersion":2},"datasetVersion":"2026-09-23T08:17:48.524Z"}