File: ServiceLookup\ServiceProviderEngineScope.cs
Web Access
Project: src\src\libraries\Microsoft.Extensions.DependencyInjection\src\Microsoft.Extensions.DependencyInjection.csproj (Microsoft.Extensions.DependencyInjection)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
 
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading.Tasks;
using Microsoft.Extensions.Internal;
 
namespace Microsoft.Extensions.DependencyInjection.ServiceLookup
{
    [DebuggerDisplay("{DebuggerToString(),nq}")]
    [DebuggerTypeProxy(typeof(ServiceProviderEngineScopeDebugView))]
    internal sealed class ServiceProviderEngineScope : IServiceScope, IServiceProvider, IKeyedServiceProvider, IAsyncDisposable, IServiceScopeFactory
    {
        // For testing and debugging only
        internal IList<object> Disposables => _disposables ?? (IList<object>)Array.Empty<object>();
 
        private bool _disposed;
        private List<object>? _disposables;
 
        public ServiceProviderEngineScope(ServiceProvider provider, bool isRootScope)
        {
            ResolvedServices = new Dictionary<ServiceCacheKey, object?>();
            RootProvider = provider;
            IsRootScope = isRootScope;
        }
 
        internal Dictionary<ServiceCacheKey, object?> ResolvedServices { get; }
 
        internal bool Disposed => _disposed;
 
        // This lock protects state on the scope, in particular, for the root scope, it protects
        // the list of disposable entries only, since ResolvedServices are cached on CallSites
        // For other scopes, it protects ResolvedServices and the list of disposables
        internal object Sync => ResolvedServices;
 
        public bool IsRootScope { get; }
 
        internal ServiceProvider RootProvider { get; }
 
        public object? GetService(Type serviceType)
        {
            if (_disposed)
            {
                ThrowHelper.ThrowObjectDisposedException();
            }
 
            return RootProvider.GetService(ServiceIdentifier.FromServiceType(serviceType), this);
        }
 
        public object? GetKeyedService(Type serviceType, object? serviceKey)
        {
            if (_disposed)
            {
                ThrowHelper.ThrowObjectDisposedException();
            }
 
            return RootProvider.GetKeyedService(serviceType, serviceKey, this);
        }
 
        public object GetRequiredKeyedService(Type serviceType, object? serviceKey)
        {
            if (_disposed)
            {
                ThrowHelper.ThrowObjectDisposedException();
            }
 
            return RootProvider.GetRequiredKeyedService(serviceType, serviceKey, this);
        }
 
        public IServiceProvider ServiceProvider => this;
 
        public IServiceScope CreateScope() => RootProvider.CreateScope();
 
        [return: NotNullIfNotNull(nameof(service))]
        internal object? CaptureDisposable(object? service)
        {
            if (ReferenceEquals(this, service) || !(service is IDisposable || service is IAsyncDisposable))
            {
                return service;
            }
 
            bool disposed = false;
            lock (Sync)
            {
                if (_disposed)
                {
                    disposed = true;
                }
                else
                {
                    _disposables ??= new List<object>();
 
                    _disposables.Add(service);
                }
            }
 
            // Don't run customer code under the lock
            if (disposed)
            {
                if (service is IDisposable disposable)
                {
                    disposable.Dispose();
                }
                else
                {
                    // sync over async, for the rare case that an object only implements IAsyncDisposable and may end up starving the thread pool.
                    object? localService = service; // copy to avoid closure on other paths
                    Task.Run(() => ((IAsyncDisposable)localService).DisposeAsync().AsTask()).GetAwaiter().GetResult();
                }
 
                ThrowHelper.ThrowObjectDisposedException();
            }
 
            return service;
        }
 
        public void Dispose()
        {
            List<object>? toDispose = BeginDispose();
 
            if (toDispose != null)
            {
                for (int i = toDispose.Count - 1; i >= 0; i--)
                {
                    if (toDispose[i] is IDisposable disposable)
                    {
                        disposable.Dispose();
                    }
                    else
                    {
                        throw new InvalidOperationException(SR.Format(SR.AsyncDisposableServiceDispose, TypeNameHelper.GetTypeDisplayName(toDispose[i])));
                    }
                }
            }
        }
 
        public ValueTask DisposeAsync()
        {
            List<object>? toDispose = BeginDispose();
 
            if (toDispose != null)
            {
                try
                {
                    for (int i = toDispose.Count - 1; i >= 0; i--)
                    {
                        object disposable = toDispose[i];
                        if (disposable is IAsyncDisposable asyncDisposable)
                        {
                            ValueTask vt = asyncDisposable.DisposeAsync();
                            if (!vt.IsCompletedSuccessfully)
                            {
                                return Await(i, vt, toDispose);
                            }
 
                            // If its a IValueTaskSource backed ValueTask,
                            // inform it its result has been read so it can reset
                            vt.GetAwaiter().GetResult();
                        }
                        else
                        {
                            ((IDisposable)disposable).Dispose();
                        }
                    }
                }
                catch (Exception ex)
                {
                    return new ValueTask(Task.FromException(ex));
                }
            }
 
            return default;
 
            static async ValueTask Await(int i, ValueTask vt, List<object> toDispose)
            {
                await vt.ConfigureAwait(false);
                // vt is acting on the disposable at index i,
                // decrement it and move to the next iteration
                i--;
 
                for (; i >= 0; i--)
                {
                    object disposable = toDispose[i];
                    if (disposable is IAsyncDisposable asyncDisposable)
                    {
                        await asyncDisposable.DisposeAsync().ConfigureAwait(false);
                    }
                    else
                    {
                        ((IDisposable)disposable).Dispose();
                    }
                }
            }
        }
 
        private List<object>? BeginDispose()
        {
            lock (Sync)
            {
                if (_disposed)
                {
                    return null;
                }
 
                // Track statistics about the scope (number of disposable objects and number of disposed services)
                DependencyInjectionEventSource.Log.ScopeDisposed(RootProvider.GetHashCode(), ResolvedServices.Count, _disposables?.Count ?? 0);
 
                // We've transitioned to the disposed state, so future calls to
                // CaptureDisposable will immediately dispose the object.
                // No further changes to _state.Disposables, are allowed.
                _disposed = true;
 
            }
 
            if (IsRootScope && !RootProvider.IsDisposed())
            {
                // If this ServiceProviderEngineScope instance is a root scope, disposing this instance will need to dispose the RootProvider too.
                // Otherwise the RootProvider will never get disposed and will leak.
                // Note, if the RootProvider get disposed first, it will automatically dispose all attached ServiceProviderEngineScope objects.
                RootProvider.Dispose();
            }
 
            // ResolvedServices is never cleared for singletons because there might be a compilation running in background
            // trying to get a cached singleton service. If it doesn't find it
            // it will try to create a new one which will result in an ObjectDisposedException.
            return _disposables;
        }
 
        internal string DebuggerToString()
        {
            string debugText = $"ServiceDescriptors = {RootProvider.CallSiteFactory.Descriptors.Length}";
            if (!IsRootScope)
            {
                debugText += $", IsScope = true";
            }
            if (_disposed)
            {
                debugText += $", Disposed = true";
            }
            return debugText;
        }
 
        private sealed class ServiceProviderEngineScopeDebugView
        {
            private readonly ServiceProviderEngineScope _serviceProvider;
 
            public ServiceProviderEngineScopeDebugView(ServiceProviderEngineScope serviceProvider)
            {
                _serviceProvider = serviceProvider;
            }
 
            public List<ServiceDescriptor> ServiceDescriptors => new List<ServiceDescriptor>(_serviceProvider.RootProvider.CallSiteFactory.Descriptors);
            public List<object> Disposables => new List<object>(_serviceProvider.Disposables);
            public bool Disposed => _serviceProvider._disposed;
            public bool IsScope => !_serviceProvider.IsRootScope;
        }
    }
}