OrchardCMS/OrchardCore · error · InvalidOperationException
Invalid serial number for shell descriptor
Error message
Invalid serial number for shell descriptor
What it means
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.
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.
Example fix
// before await manager.UpdateShellDescriptorAsync(staleSerialNumber, features); // descriptor changed meanwhile // after var descriptor = await manager.GetShellDescriptorAsync(); await manager.UpdateShellDescriptorAsync(descriptor.SerialNumber, features);
Defensive patterns
Strategy: retry
Validate before calling
var descriptor = await manager.GetShellDescriptorAsync(); if (descriptor is null || descriptor.SerialNumber != priorSerialNumber) priorSerialNumber = descriptor?.SerialNumber ?? 0;
Try / catch
try { await manager.UpdateShellDescriptorAsync(priorSerialNumber, features); }
catch (InvalidOperationException ex) when (ex.Message.Contains("Invalid serial number")) { /* reload descriptor and retry */ } Prevention
- 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
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- Unable to reload the tenant
- Can't resolve a scope on tenant
- The ' ' could not be persisted and cached as it has been…
- The token was concurrently updated and cannot be persisted…
- Unexpected shell state for
AI-assisted analysis of OrchardCMS/OrchardCore@4306c0717f (2026-09-13).
Data as JSON: /api/errors/6120bc99e4677957.
Report an issue: GitHub.
Appendix: source
Thrown at src/OrchardCore/OrchardCore.Infrastructure/Shell/ShellDescriptorManager.cs:99
var missingDependencies = (await _extensionManager.LoadFeaturesAsync(featureIds))
.Select(entry => entry.Id)
.Except(featureIds)
.Select(id => new ShellFeature(id));
shellDescriptor.Features = features
.Concat(missingDependencies)
.ToList();
return _shellDescriptor = shellDescriptor;
}
public async Task UpdateShellDescriptorAsync(int priorSerialNumber, IEnumerable<ShellFeature> enabledFeatures)
{
var shellDescriptor = await _documentStore.GetOrCreateMutableAsync<ShellDescriptor>();
if (priorSerialNumber != shellDescriptor.SerialNumber)
{
throw new InvalidOperationException("Invalid serial number for shell descriptor");
}
if (_logger.IsEnabled(LogLevel.Information))
{
_logger.LogInformation("Updating shell descriptor for tenant '{TenantName}' ...", _shellSettings.Name);
}
shellDescriptor.SerialNumber++;
shellDescriptor.Features = _alwaysEnabledFeatures.Union(enabledFeatures).ToList();
foreach (var feature in shellDescriptor.Features)
{
if (shellDescriptor.Installed.Contains(feature))
{
continue;
}
var installed = new InstalledShellFeature(feature)View on GitHub (pinned to 4306c0717f)