Skip to content

Commit

Permalink
Adds extension method to create service scope that implements IAsyncD…
Browse files Browse the repository at this point in the history
…isposable. (#51840)

* Adds extension method to create service scope that implements IAsyncDisposable.

- Introduces a new type AsyncServiceScope that implements IServiceScope and IAsyncDisposable. The type just wraps an existing IServiceScope instance, which it tries to cast it to an IAsyncDisposable when DisposeAsync is called.
- Adds netstandard2.1 target to avoid bringing in System.Threading.Tasks.Extensions and Microsoft.Bcl.AsyncInterfaces if not needed.
- Fixes #43970

* Make AsyncServiceScope readonly

Co-authored-by: David Fowler <davidfowl@gmail.com>

* Use null-coalescing for null checking and argument exception.

Co-authored-by: David Fowler <davidfowl@gmail.com>

* Make AsyncServiceScope readonly in reference source.

* Adds tests for AsyncServiceScope and CreateAsyncScope extension method.

* Merge generated ref source.

* Document why 'default' is used instead of 'ValueTask.CompletedTask'

* Remove unnecessary casts to IDisposable and IAsyncDisposable in tests.

Co-authored-by: David Fowler <davidfowl@gmail.com>
  • Loading branch information
bjorkstromm and davidfowl authored Apr 28, 2021
1 parent 9f55689 commit 41f3d48
Show file tree
Hide file tree
Showing 8 changed files with 278 additions and 8 deletions.
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" />
</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;
}
}
}
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

0 comments on commit 41f3d48

Please sign in to comment.