Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Adds extension method to create service scope that implements IAsyncDisposable. #51840

Merged
merged 8 commits into from
Apr 28, 2021
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,15 @@ public partial class ActivatorUtilitiesConstructorAttribute : System.Attribute
{
public ActivatorUtilitiesConstructorAttribute() { }
}
public readonly partial struct AsyncServiceScope : Microsoft.Extensions.DependencyInjection.IServiceScope, System.IAsyncDisposable, System.IDisposable
{
private readonly object _dummy;
private readonly int _dummyPrimitive;
public AsyncServiceScope(Microsoft.Extensions.DependencyInjection.IServiceScope serviceScope) { throw null; }
public System.IServiceProvider ServiceProvider { get { throw null; } }
public void Dispose() { }
public System.Threading.Tasks.ValueTask DisposeAsync() { throw null; }
}
public partial interface IServiceCollection : System.Collections.Generic.ICollection<Microsoft.Extensions.DependencyInjection.ServiceDescriptor>, System.Collections.Generic.IEnumerable<Microsoft.Extensions.DependencyInjection.ServiceDescriptor>, System.Collections.Generic.IList<Microsoft.Extensions.DependencyInjection.ServiceDescriptor>, System.Collections.IEnumerable
{
}
Expand Down Expand Up @@ -106,6 +115,7 @@ public enum ServiceLifetime
}
public static partial class ServiceProviderServiceExtensions
{
public static Microsoft.Extensions.DependencyInjection.AsyncServiceScope CreateAsyncScope(this System.IServiceProvider provider) { throw null; }
public static Microsoft.Extensions.DependencyInjection.IServiceScope CreateScope(this System.IServiceProvider provider) { throw null; }
public static object GetRequiredService(this System.IServiceProvider provider, System.Type serviceType) { throw null; }
public static T GetRequiredService<T>(this System.IServiceProvider provider) where T : notnull { throw null; }
Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,16 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
<TargetFrameworks>netstandard2.1;netstandard2.0;net461</TargetFrameworks>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<Compile Include="Microsoft.Extensions.DependencyInjection.Abstractions.cs" />
<Compile Include="$(CoreLibSharedDir)System\Diagnostics\CodeAnalysis\DynamicallyAccessedMembersAttribute.cs" />
<Compile Include="$(CoreLibSharedDir)System\Diagnostics\CodeAnalysis\DynamicallyAccessedMemberTypes.cs" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' or
$([MSBuild]::GetTargetFrameworkIdentifier('$(TargetFramework)')) == '.NETFramework'">
<PackageReference Include="System.Threading.Tasks.Extensions" Version="$(SystemThreadingTasksExtensionsVersion)" />
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Bcl.AsyncInterfaces\src\Microsoft.Bcl.AsyncInterfaces.csproj" />
bjorkstromm marked this conversation as resolved.
Show resolved Hide resolved
</ItemGroup>
</Project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
// 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.Threading.Tasks;

namespace Microsoft.Extensions.DependencyInjection
{
/// <summary>
/// A <see cref="IServiceScope" /> implementation that implements <see cref="IAsyncDisposable" />.
/// </summary>
public readonly struct AsyncServiceScope : IServiceScope, IAsyncDisposable
{
private readonly IServiceScope _serviceScope;

/// <summary>
/// Initializes a new instance of the <see cref="AsyncServiceScope"/> struct.
/// Wraps an instance of <see cref="IServiceScope" />.
/// <param name="serviceScope">The <see cref="IServiceScope"/> instance to wrap.</param>
/// </summary>
public AsyncServiceScope(IServiceScope serviceScope)
{
_serviceScope = serviceScope ?? throw new ArgumentNullException(nameof(serviceScope));
}

/// <inheritdoc />
public IServiceProvider ServiceProvider => _serviceScope.ServiceProvider;

/// <inheritdoc />
public void Dispose()
{
_serviceScope.Dispose();
}

/// <inheritdoc />
public ValueTask DisposeAsync()
{
if (_serviceScope is IAsyncDisposable ad)
{
return ad.DisposeAsync();
}
_serviceScope.Dispose();

// ValueTask.CompletedTask is only available in net5.0 and later.
return default;
bjorkstromm marked this conversation as resolved.
Show resolved Hide resolved
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<TargetFrameworks>netstandard2.0;net461</TargetFrameworks>
<TargetFrameworks>netstandard2.1;netstandard2.0;net461</TargetFrameworks>
<EnableDefaultItems>true</EnableDefaultItems>
<Nullable>enable</Nullable>
</PropertyGroup>
Expand All @@ -14,4 +14,10 @@
<Compile Include="$(CoreLibSharedDir)System\Diagnostics\CodeAnalysis\UnconditionalSuppressMessageAttribute.cs" />
</ItemGroup>

<ItemGroup Condition="'$(TargetFramework)' == 'netstandard2.0' or
$([MSBuild]::GetTargetFrameworkIdentifier('$(TargetFramework)')) == '.NETFramework'">
<PackageReference Include="System.Threading.Tasks.Extensions" Version="$(SystemThreadingTasksExtensionsVersion)" />
<ProjectReference Include="$(LibrariesProjectRoot)Microsoft.Bcl.AsyncInterfaces\src\Microsoft.Bcl.AsyncInterfaces.csproj" />
</ItemGroup>

</Project>
Original file line number Diff line number Diff line change
Expand Up @@ -125,5 +125,15 @@ public static IServiceScope CreateScope(this IServiceProvider provider)
{
return provider.GetRequiredService<IServiceScopeFactory>().CreateScope();
}

/// <summary>
/// Creates a new <see cref="AsyncServiceScope"/> that can be used to resolve scoped services.
/// </summary>
/// <param name="provider">The <see cref="IServiceProvider"/> to create the scope from.</param>
/// <returns>A <see cref="AsyncServiceScope"/> that can be used to resolve scoped services.</returns>
public static AsyncServiceScope CreateAsyncScope(this IServiceProvider provider)
{
return new AsyncServiceScope(provider.CreateScope());
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
// 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.Threading.Tasks;
using Xunit;

namespace Microsoft.Extensions.DependencyInjection.Tests
{
public class AsyncServiceScopeTests
{
[Fact]
public void ThrowsIfServiceScopeIsNull()
{
var exception = Assert.Throws<ArgumentNullException>(() => new AsyncServiceScope(null));
Assert.Equal("serviceScope", exception.ParamName);
}

[Fact]
public void ReturnsServiceProviderFromWrappedScope()
{
var wrappedScope = new FakeSyncServiceScope();
var asyncScope = new AsyncServiceScope(wrappedScope);

Assert.Same(wrappedScope.ServiceProvider, asyncScope.ServiceProvider);
}

[Fact]
public void CallsDisposeOnWrappedSyncScopeOnDispose()
{
var wrappedScope = new FakeSyncServiceScope();
var asyncScope = new AsyncServiceScope(wrappedScope);

asyncScope.Dispose();

Assert.True(wrappedScope.DisposeCalled);
}

[Fact]
public async ValueTask CallsDisposeOnWrappedSyncScopeOnDisposeAsync()
{
var wrappedScope = new FakeSyncServiceScope();
var asyncScope = new AsyncServiceScope(wrappedScope);

await asyncScope.DisposeAsync();

Assert.True(wrappedScope.DisposeCalled);
}

[Fact]
public void CallsDisposeOnWrappedAsyncScopeOnDispose()
{
var wrappedScope = new FakeAsyncServiceScope();
var asyncScope = new AsyncServiceScope(wrappedScope);

asyncScope.Dispose();

Assert.True(wrappedScope.DisposeCalled);
Assert.False(wrappedScope.DisposeAsyncCalled);
}

[Fact]
public async ValueTask CallsDisposeAsyncOnWrappedSyncScopeOnDisposeAsync()
{
var wrappedScope = new FakeAsyncServiceScope();
var asyncScope = new AsyncServiceScope(wrappedScope);

await asyncScope.DisposeAsync();

Assert.False(wrappedScope.DisposeCalled);
Assert.True(wrappedScope.DisposeAsyncCalled);
}

public class FakeServiceProvider : IServiceProvider
{
public object? GetService(Type serviceType) => throw new NotImplementedException();
}

public class FakeSyncServiceScope : IServiceScope
{
public FakeSyncServiceScope()
{
ServiceProvider = new FakeServiceProvider();
}

public IServiceProvider ServiceProvider { get; }

public bool DisposeCalled { get; private set; }

public void Dispose()
{
DisposeCalled = true;
}
}

public class FakeAsyncServiceScope : FakeSyncServiceScope, IAsyncDisposable
{
public FakeAsyncServiceScope() : base()
{
}

public bool DisposeAsyncCalled { get; private set; }

public ValueTask DisposeAsync()
{
DisposeAsyncCalled = true;

return default;
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -392,7 +392,7 @@ public async Task AddDisposablesAndAsyncDisposables_DisposeAsync_AllDisposed(boo
}

await sp.DisposeAsync();

Assert.True(disposable.Disposed);
Assert.True(asyncDisposable.DisposeAsyncCalled);
if (includeDelayedAsyncDisposable)
Expand Down Expand Up @@ -449,7 +449,7 @@ private class InnerSingleton
public InnerSingleton(ManualResetEvent mre1, ManualResetEvent mre2)
{
// Making sure ctor gets called only once
Assert.True(!mre1.WaitOne(0) && !mre2.WaitOne(0));
Assert.True(!mre1.WaitOne(0) && !mre2.WaitOne(0));

// Then use mre2 to signal execution reached this ctor call
mre2.Set();
Expand Down Expand Up @@ -493,13 +493,13 @@ public async Task GetRequiredService_ResolvingSameSingletonInTwoThreads_SameServ
// This waits on InnerSingleton singleton lock that is taken in thread 1
innerSingleton = sp.GetRequiredService<InnerSingleton>();
});

mreForThread3.WaitOne();

// Set a timeout before unblocking execution of both thread1 and thread2 via mre1:
Assert.False(mreForThread1.WaitOne(10));

// By this time thread 1 has already reached InnerSingleton ctor and is waiting for mre1.
// By this time thread 1 has already reached InnerSingleton ctor and is waiting for mre1.
// within the GetRequiredService call, thread 2 should be waiting on a singleton lock for InnerSingleton
// (rather than trying to instantiating InnerSingleton twice).
mreForThread1.Set();
Expand Down Expand Up @@ -546,7 +546,7 @@ public async Task GetRequiredService_UsesSingletonAndLazyLocks_NoDeadlock()
sb.Append("3");
mreForThread2.Set(); // Now that thread 1 holds lazy lock, allow thread 2 to continue

// by this time, Thread 2 is holding a singleton lock for Thing2,
// by this time, Thread 2 is holding a singleton lock for Thing2,
// and Thread one holds the lazy lock
// the call below to resolve Thing0 does not hang
// since singletons do not share the same lock upon resolve anymore.
Expand Down Expand Up @@ -895,6 +895,67 @@ public void ProviderScopeDisposeThrowsWhenOnlyDisposeAsyncImplemented()
exception.Message);
}

[Fact]
public async Task ProviderAsyncScopeDisposeAsyncCallsDisposeAsyncOnServices()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<AsyncDisposable>();

var serviceProvider = CreateServiceProvider(serviceCollection);
var scope = serviceProvider.CreateAsyncScope();
var disposable = scope.ServiceProvider.GetService<AsyncDisposable>();

await scope.DisposeAsync();

Assert.True(disposable.DisposeAsyncCalled);
}

[Fact]
public async Task ProviderAsyncScopeDisposeAsyncPrefersDisposeAsyncOnServices()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<SyncAsyncDisposable>();

var serviceProvider = CreateServiceProvider(serviceCollection);
var scope = serviceProvider.CreateAsyncScope();
var disposable = scope.ServiceProvider.GetService<SyncAsyncDisposable>();

await scope.DisposeAsync();

Assert.True(disposable.DisposeAsyncCalled);
}

[Fact]
public void ProviderAsyncScopeDisposePrefersServiceDispose()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<SyncAsyncDisposable>();

var serviceProvider = CreateServiceProvider(serviceCollection);
var scope = serviceProvider.CreateScope();
var disposable = scope.ServiceProvider.GetService<SyncAsyncDisposable>();

scope.Dispose();

Assert.True(disposable.DisposeCalled);
}

[Fact]
public void ProviderAsyncScopeDisposeThrowsWhenOnlyDisposeAsyncImplemented()
{
var serviceCollection = new ServiceCollection();
serviceCollection.AddTransient<AsyncDisposable>();

var serviceProvider = CreateServiceProvider(serviceCollection);
var scope = serviceProvider.CreateScope();
var disposable = scope.ServiceProvider.GetService<AsyncDisposable>();

var exception = Assert.Throws<InvalidOperationException>(() => scope.Dispose());
Assert.Equal(
"'Microsoft.Extensions.DependencyInjection.Tests.ServiceProviderContainerTests+AsyncDisposable' type only implements IAsyncDisposable. Use DisposeAsync to dispose the container.",
exception.Message);
}

[Fact]
public void SingletonServiceCreatedFromFactoryIsDisposedWhenContainerIsDisposed()
{
Expand Down Expand Up @@ -1031,7 +1092,7 @@ private async Task<bool> ResolveUniqueServicesConcurrently()
{
var types = new Type[]
{
typeof(A), typeof(B), typeof(C), typeof(D), typeof(E),
typeof(A), typeof(B), typeof(C), typeof(D), typeof(E),
typeof(F), typeof(G), typeof(H), typeof(I), typeof(J)
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using Xunit;

namespace Microsoft.Extensions.DependencyInjection
Expand Down Expand Up @@ -213,6 +214,23 @@ public void NonGeneric_GetServices_WithBuildServiceProvider_Returns_EmptyList_Wh
Assert.IsType<List<IFoo>>(services);
}

[Fact]
public async Task CreateAsyncScope_Returns_AsyncServiceScope_Wrapping_ServiceScope()
{
// Arrange
var serviceCollection = new ServiceCollection();
serviceCollection.AddScoped<IFoo, Foo1>();
var serviceProvider = serviceCollection.BuildServiceProvider();

await using var scope = serviceProvider.CreateAsyncScope();

// Act
var service = scope.ServiceProvider.GetService<IFoo>();

// Assert
Assert.IsType<Foo1>(service);
}

private static IServiceProvider CreateTestServiceProvider(int count)
{
var serviceCollection = new ServiceCollection();
Expand Down