Problem
ServiceProvider.Dispose sets _disposed = true before it disposes anything, then walks its children, its transient disposal records, and its creation records in reverse order. None of the disposal calls are guarded, so the first service whose Dispose throws aborts the whole method.
What is left behind:
- Every service earlier in the iteration order is never disposed. For services holding GPU or SDL handles, those are leaked for the lifetime of the process.
- Remaining child providers are unreachable.
_children is set to null before the child loop runs, so children that were not reached cannot be disposed by any later call.
- The field cleanup at the end of the method never runs, so
_services, _creationRecords, and the caches stay populated and keep every instance alive.
_disposed is already true, so a retry returns immediately. The provider reports itself disposed while most of it is not.
A single misbehaving service therefore silently disables disposal for everything else in the provider, and there is no way to detect it or recover.
This is reachable from ordinary use. StageManager.ReplaceStage disposes the outgoing stage provider on every stage transition, so one throwing service leaks the entire stage.
Expected behavior
- One failing
Dispose does not prevent the remaining services and child providers from being disposed.
- The provider finishes its own cleanup regardless.
- Disposal failures are surfaced rather than swallowed, so a throwing
Dispose is still visible as a defect.
Possible approach
Continue the loops on failure and collect the exceptions, then rethrow them together once the provider has finished:
- wrap each
RunDisposingCallbacks and Dispose call so a throw records the exception and moves on;
- run the child loop, both service loops, and the field cleanup to completion;
- if anything was collected, throw a single
AggregateException at the end.
Aggregating matches the observation that disposal has no meaningful partial-failure recovery: the caller cannot fix it, but it must not be hidden.
Coverage
- a throwing service
Dispose still disposes the services registered before it;
- a throwing child provider
Dispose still disposes the parent's own services and the remaining children;
- the provider's caches and records are cleared even when a disposal throws;
- the exception from a throwing
Dispose reaches the caller;
- multiple throwing services produce one exception carrying all of them;
- disposal with no failures behaves exactly as it does now, including reverse creation order and alias deduplication.
Problem
ServiceProvider.Disposesets_disposed = truebefore it disposes anything, then walks its children, its transient disposal records, and its creation records in reverse order. None of the disposal calls are guarded, so the first service whoseDisposethrows aborts the whole method.What is left behind:
_childrenis set tonullbefore the child loop runs, so children that were not reached cannot be disposed by any later call._services,_creationRecords, and the caches stay populated and keep every instance alive._disposedis alreadytrue, so a retry returns immediately. The provider reports itself disposed while most of it is not.A single misbehaving service therefore silently disables disposal for everything else in the provider, and there is no way to detect it or recover.
This is reachable from ordinary use.
StageManager.ReplaceStagedisposes the outgoing stage provider on every stage transition, so one throwing service leaks the entire stage.Expected behavior
Disposedoes not prevent the remaining services and child providers from being disposed.Disposeis still visible as a defect.Possible approach
Continue the loops on failure and collect the exceptions, then rethrow them together once the provider has finished:
RunDisposingCallbacksandDisposecall so a throw records the exception and moves on;AggregateExceptionat the end.Aggregating matches the observation that disposal has no meaningful partial-failure recovery: the caller cannot fix it, but it must not be hidden.
Coverage
Disposestill disposes the services registered before it;Disposestill disposes the parent's own services and the remaining children;Disposereaches the caller;