{"record":{"id":"6120bc99e4677957","repo":"OrchardCMS/OrchardCore","slug":"invalid-serial-number-for-shell-descriptor","errorCode":null,"errorMessage":"Invalid serial number for shell descriptor","messagePattern":"Invalid serial number for shell descriptor","errorType":"exception","errorClass":"InvalidOperationException","httpStatus":null,"severity":"error","filePath":"src/OrchardCore/OrchardCore.Infrastructure/Shell/ShellDescriptorManager.cs","lineNumber":99,"sourceCode":"        var missingDependencies = (await _extensionManager.LoadFeaturesAsync(featureIds))\n            .Select(entry => entry.Id)\n            .Except(featureIds)\n            .Select(id => new ShellFeature(id));\n\n        shellDescriptor.Features = features\n            .Concat(missingDependencies)\n            .ToList();\n\n        return _shellDescriptor = shellDescriptor;\n    }\n\n    public async Task UpdateShellDescriptorAsync(int priorSerialNumber, IEnumerable<ShellFeature> enabledFeatures)\n    {\n        var shellDescriptor = await _documentStore.GetOrCreateMutableAsync<ShellDescriptor>();\n\n        if (priorSerialNumber != shellDescriptor.SerialNumber)\n        {\n            throw new InvalidOperationException(\"Invalid serial number for shell descriptor\");\n        }\n\n        if (_logger.IsEnabled(LogLevel.Information))\n        {\n            _logger.LogInformation(\"Updating shell descriptor for tenant '{TenantName}' ...\", _shellSettings.Name);\n        }\n\n        shellDescriptor.SerialNumber++;\n\n        shellDescriptor.Features = _alwaysEnabledFeatures.Union(enabledFeatures).ToList();\n        foreach (var feature in shellDescriptor.Features)\n        {\n            if (shellDescriptor.Installed.Contains(feature))\n            {\n                continue;\n            }\n\n            var installed = new InstalledShellFeature(feature)","sourceCodeStart":81,"sourceCodeEnd":117,"githubUrl":"https://github.com/OrchardCMS/OrchardCore/blob/4306c0717fe573f6fca1b4955909ddab6a192807/src/OrchardCore/OrchardCore.Infrastructure/Shell/ShellDescriptorManager.cs#L81-L117","documentation":"ShellDescriptorManager.UpdateShellDescriptorAsync uses optimistic concurrency: the caller passes the SerialNumber it previously read, and the manager compares it to the current ShellDescriptor's SerialNumber before applying updates. A mismatch means the descriptor changed concurrently, so it throws InvalidOperationException to prevent clobbering.","triggerScenarios":"Calling UpdateShellDescriptorAsync with a priorSerialNumber that differs from the currently stored ShellDescriptor.SerialNumber — e.g. two features enabling simultaneously, or the caller read the descriptor long before updating.","commonSituations":"Concurrent feature enable/disable in the admin UI from multiple tabs; module startup code caching a shell descriptor across requests; race between recipe execution and user actions.","solutions":["Re-read the current ShellDescriptor and retry the update with its fresh SerialNumber.","Serialize shell updates (avoid parallel feature-enable calls for the same tenant).","Catch InvalidOperationException and surface a 'conflict, please retry' message rather than crashing the request."],"exampleFix":"// before\nawait manager.UpdateShellDescriptorAsync(staleSerialNumber, features); // descriptor changed meanwhile\n// after\nvar descriptor = await manager.GetShellDescriptorAsync();\nawait manager.UpdateShellDescriptorAsync(descriptor.SerialNumber, features);","handlingStrategy":"retry","validationCode":"var descriptor = await manager.GetShellDescriptorAsync();\nif (descriptor is null || descriptor.SerialNumber != priorSerialNumber) priorSerialNumber = descriptor?.SerialNumber ?? 0;","typeGuard":null,"tryCatchPattern":"try { await manager.UpdateShellDescriptorAsync(priorSerialNumber, features); }\ncatch (InvalidOperationException ex) when (ex.Message.Contains(\"Invalid serial number\")) { /* reload descriptor and retry */ }","preventionTips":["Always pass the SerialNumber from the descriptor just read, never a cached one","Avoid parallel feature-enable operations for the same tenant","Wrap shell descriptor updates in a retry-on-conflict loop"],"tags":["multi-tenancy","concurrency","optimistic-locking","invalid-state-transition"],"backgroundTag":"optimistic-concurrency-conflict","analyzedSha":"4306c0717fe573f6fca1b4955909ddab6a192807","analyzedAt":"2026-09-13T17:41:05.024Z","contentChangedAt":"2026-09-13T17:41:05.024Z","schemaVersion":2},"datasetVersion":"2026-09-16T04:17:20.429Z"}