Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,21 @@ public void DynamicMethodBuilt(string serviceType, int methodSize)
WriteEvent(4, serviceType, methodSize);
}

[Event(5, Level = EventLevel.Verbose)]
public void ScopeDisposed(int serviceProviderHashCode, int scopedServicesResolved, int disposableServices)
{
WriteEvent(5, serviceProviderHashCode, scopedServicesResolved, disposableServices);
}

[NonEvent]
public void ScopeDisposed(ServiceProviderEngine engine, ScopeState state)
{
if (IsEnabled(EventLevel.Verbose, EventKeywords.All))
{
ScopeDisposed(engine.GetHashCode(), state.ResolvedServicesCount, state.DisposableServicesCount);
}
}

[NonEvent]
public void ServiceResolved(Type serviceType)
{
Expand Down

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// 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.Concurrent;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection.ServiceLookup;

namespace Microsoft.Extensions.DependencyInjection
{
internal class ScopeState
{
public IDictionary<ServiceCacheKey, object> ResolvedServices { get; }
public List<object> Disposables { get; set; }

public int DisposableServicesCount => Disposables?.Count ?? 0;
public int ResolvedServicesCount => ResolvedServices.Count;

public ScopeState(bool isRoot)
{
// When isRoot is true to reduce lock contention for singletons upon resolve we use a concurrent dictionary.
ResolvedServices = isRoot ? new ConcurrentDictionary<ServiceCacheKey, object>() : new Dictionary<ServiceCacheKey, object>();
}

public void Track(ServiceProviderEngine engine)
{
DependencyInjectionEventSource.Log.ScopeDisposed(engine, this);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -25,11 +25,8 @@ protected ServiceProviderEngine(IEnumerable<ServiceDescriptor> serviceDescriptor
CallSiteFactory.Add(typeof(IServiceProvider), new ServiceProviderCallSite());
CallSiteFactory.Add(typeof(IServiceScopeFactory), new ServiceScopeFactoryCallSite());
RealizedServices = new ConcurrentDictionary<Type, Func<ServiceProviderEngineScope, object>>();
ScopePool = new ScopePool();
}

internal ScopePool ScopePool { get; }

internal ConcurrentDictionary<Type, Func<ServiceProviderEngineScope, object>> RealizedServices { get; }

internal CallSiteFactory CallSiteFactory { get; }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,22 +14,20 @@ internal sealed class ServiceProviderEngineScope : IServiceScope, IServiceProvid
internal Action<object> _captureDisposableCallback;

private bool _disposed;
private ScopePool.State _state;

// This lock protects state on the scope, in particular, for the root scope, it protects
// the list of disposable entries only, since ResolvedServices is a concurrent dictionary.
// For other scopes, it protects ResolvedServices and the list of disposables
private readonly object _scopeLock = new object();
private readonly ScopeState _state;

public ServiceProviderEngineScope(ServiceProviderEngine engine, bool isRoot = false)
{
Engine = engine;
_state = isRoot ? new ScopePool.State() : engine.ScopePool.Rent();
_state = new ScopeState(isRoot);
}

internal IDictionary<ServiceCacheKey, object> ResolvedServices => _state?.ResolvedServices ?? ScopeDisposed();
internal IDictionary<ServiceCacheKey, object> ResolvedServices => _state.ResolvedServices;

internal object Sync => _scopeLock;
// This lock protects state on the scope, in particular, for the root scope, it protects
// the list of disposable entries only, since ResolvedServices is a concurrent dictionary.
// For other scopes, it protects ResolvedServices and the list of disposables
internal object Sync => _state;

public ServiceProviderEngine Engine { get; }

Expand All @@ -54,7 +52,7 @@ internal object CaptureDisposable(object service)
return service;
}

lock (_scopeLock)
lock (Sync)
{
if (_disposed)
{
Expand Down Expand Up @@ -97,8 +95,6 @@ public void Dispose()
}
}
}

ClearState();
}

public ValueTask DisposeAsync()
Expand All @@ -117,7 +113,7 @@ public ValueTask DisposeAsync()
ValueTask vt = asyncDisposable.DisposeAsync();
if (!vt.IsCompletedSuccessfully)
{
return Await(this, i, vt, toDispose);
return Await(i, vt, toDispose);
}

// If its a IValueTaskSource backed ValueTask,
Expand All @@ -136,11 +132,9 @@ public ValueTask DisposeAsync()
}
}

ClearState();

return default;

static async ValueTask Await(ServiceProviderEngineScope scope, int i, ValueTask vt, List<object> toDispose)
static async ValueTask Await(int i, ValueTask vt, List<object> toDispose)
{
await vt.ConfigureAwait(false);
// vt is acting on the disposable at index i,
Expand All @@ -159,56 +153,30 @@ static async ValueTask Await(ServiceProviderEngineScope scope, int i, ValueTask
((IDisposable)disposable).Dispose();
}
}

scope.ClearState();
}
}

private IDictionary<ServiceCacheKey, object> ScopeDisposed()
{
ThrowHelper.ThrowObjectDisposedException();
return null;
}

private void ClearState()
{
// We lock here since ResolvedServices is always accessed in the scope lock, this means we'll never
// try to return to the pool while somebody is trying to access ResolvedServices.
lock (_scopeLock)
{
// Don't attempt to dispose if we're already disposed
if (_state == null)
{
return;
}

// 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.

// Dispose the state, which will end up attempting to return the state pool.
// This will return false if the pool is full or if this state object is the root scope
if (_state.Return())
{
_state = null;
}
}
}

private List<object> BeginDispose()
{
lock (_scopeLock)
lock (Sync)
{
if (_disposed)
{
return null;
}

// Track statistics about the scope (number of disposable objects and number of disposed services)
_state.Track(Engine);

// 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;

// 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 _state.Disposables;
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,25 +2,13 @@
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.Collections.Generic;
using Microsoft.Extensions.DependencyInjection.Specification.Fakes;
using Xunit;

namespace Microsoft.Extensions.DependencyInjection.ServiceLookup
{
public class ServiceProviderEngineScopeTests
{
[Fact]
public void ResolvedServicesAfterDispose_ThrowsObjectDispose()
{
var engine = new FakeEngine();
var serviceProviderEngineScope = new ServiceProviderEngineScope(engine);
serviceProviderEngineScope.ResolvedServices.Add(new ServiceCacheKey(typeof(IFakeService), 0), null);
serviceProviderEngineScope.Dispose();

Assert.Throws<ObjectDisposedException>(() => serviceProviderEngineScope.ResolvedServices);
}

[Fact]
public void DoubleDisposeWorks()
{
Expand Down