From 41f3d48b520d4cf4f9e051dc07f480dcb9c62e08 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Bj=C3=B6rkstr=C3=B6m?= Date: Wed, 28 Apr 2021 05:31:46 +0300 Subject: [PATCH] Adds extension method to create service scope that implements IAsyncDisposable. (#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 * Use null-coalescing for null checking and argument exception. Co-authored-by: David Fowler * 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 --- ...nsions.DependencyInjection.Abstractions.cs | 10 ++ ...ns.DependencyInjection.Abstractions.csproj | 7 +- .../src/AsyncServiceScope.cs | 48 ++++++++ ...ns.DependencyInjection.Abstractions.csproj | 8 +- .../src/ServiceProviderServiceExtensions.cs | 10 ++ .../tests/DI.Tests/AsyncServiceScopeTests.cs | 112 ++++++++++++++++++ .../DI.Tests/ServiceProviderContainerTests.cs | 73 +++++++++++- .../ServiceProviderServiceExtensionsTest.cs | 18 +++ 8 files changed, 278 insertions(+), 8 deletions(-) create mode 100644 src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/AsyncServiceScope.cs create mode 100644 src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/AsyncServiceScopeTests.cs diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.cs index 6b52486993922..482c51e78384d 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.cs @@ -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, System.Collections.Generic.IEnumerable, System.Collections.Generic.IList, System.Collections.IEnumerable { } @@ -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(this System.IServiceProvider provider) where T : notnull { throw null; } diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.csproj b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.csproj index 6b74303de9eec..f85823f03e54c 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.csproj +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/ref/Microsoft.Extensions.DependencyInjection.Abstractions.csproj @@ -1,6 +1,6 @@ - netstandard2.0;net461 + netstandard2.1;netstandard2.0;net461 enable @@ -8,4 +8,9 @@ + + + + diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/AsyncServiceScope.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/AsyncServiceScope.cs new file mode 100644 index 0000000000000..3c0edf88ab7b9 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/AsyncServiceScope.cs @@ -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 +{ + /// + /// A implementation that implements . + /// + public readonly struct AsyncServiceScope : IServiceScope, IAsyncDisposable + { + private readonly IServiceScope _serviceScope; + + /// + /// Initializes a new instance of the struct. + /// Wraps an instance of . + /// The instance to wrap. + /// + public AsyncServiceScope(IServiceScope serviceScope) + { + _serviceScope = serviceScope ?? throw new ArgumentNullException(nameof(serviceScope)); + } + + /// + public IServiceProvider ServiceProvider => _serviceScope.ServiceProvider; + + /// + public void Dispose() + { + _serviceScope.Dispose(); + } + + /// + 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; + } + } +} diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/Microsoft.Extensions.DependencyInjection.Abstractions.csproj b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/Microsoft.Extensions.DependencyInjection.Abstractions.csproj index 65d3953595f06..c7ac4d7c6380a 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/Microsoft.Extensions.DependencyInjection.Abstractions.csproj +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/Microsoft.Extensions.DependencyInjection.Abstractions.csproj @@ -1,7 +1,7 @@ - netstandard2.0;net461 + netstandard2.1;netstandard2.0;net461 true enable @@ -14,4 +14,10 @@ + + + + + diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceProviderServiceExtensions.cs b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceProviderServiceExtensions.cs index c30b867c4a392..ff6a16e08165e 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceProviderServiceExtensions.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection.Abstractions/src/ServiceProviderServiceExtensions.cs @@ -125,5 +125,15 @@ public static IServiceScope CreateScope(this IServiceProvider provider) { return provider.GetRequiredService().CreateScope(); } + + /// + /// Creates a new that can be used to resolve scoped services. + /// + /// The to create the scope from. + /// A that can be used to resolve scoped services. + public static AsyncServiceScope CreateAsyncScope(this IServiceProvider provider) + { + return new AsyncServiceScope(provider.CreateScope()); + } } } diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/AsyncServiceScopeTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/AsyncServiceScopeTests.cs new file mode 100644 index 0000000000000..dde5ebc57bdc5 --- /dev/null +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/AsyncServiceScopeTests.cs @@ -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(() => 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; + } + } + } +} diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderContainerTests.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderContainerTests.cs index 5831a0ef1b0f3..0ca582c004ec2 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderContainerTests.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderContainerTests.cs @@ -392,7 +392,7 @@ public async Task AddDisposablesAndAsyncDisposables_DisposeAsync_AllDisposed(boo } await sp.DisposeAsync(); - + Assert.True(disposable.Disposed); Assert.True(asyncDisposable.DisposeAsyncCalled); if (includeDelayedAsyncDisposable) @@ -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(); @@ -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(); }); - + 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(); @@ -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. @@ -895,6 +895,67 @@ public void ProviderScopeDisposeThrowsWhenOnlyDisposeAsyncImplemented() exception.Message); } + [Fact] + public async Task ProviderAsyncScopeDisposeAsyncCallsDisposeAsyncOnServices() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddTransient(); + + var serviceProvider = CreateServiceProvider(serviceCollection); + var scope = serviceProvider.CreateAsyncScope(); + var disposable = scope.ServiceProvider.GetService(); + + await scope.DisposeAsync(); + + Assert.True(disposable.DisposeAsyncCalled); + } + + [Fact] + public async Task ProviderAsyncScopeDisposeAsyncPrefersDisposeAsyncOnServices() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddTransient(); + + var serviceProvider = CreateServiceProvider(serviceCollection); + var scope = serviceProvider.CreateAsyncScope(); + var disposable = scope.ServiceProvider.GetService(); + + await scope.DisposeAsync(); + + Assert.True(disposable.DisposeAsyncCalled); + } + + [Fact] + public void ProviderAsyncScopeDisposePrefersServiceDispose() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddTransient(); + + var serviceProvider = CreateServiceProvider(serviceCollection); + var scope = serviceProvider.CreateScope(); + var disposable = scope.ServiceProvider.GetService(); + + scope.Dispose(); + + Assert.True(disposable.DisposeCalled); + } + + [Fact] + public void ProviderAsyncScopeDisposeThrowsWhenOnlyDisposeAsyncImplemented() + { + var serviceCollection = new ServiceCollection(); + serviceCollection.AddTransient(); + + var serviceProvider = CreateServiceProvider(serviceCollection); + var scope = serviceProvider.CreateScope(); + var disposable = scope.ServiceProvider.GetService(); + + var exception = Assert.Throws(() => 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() { @@ -1031,7 +1092,7 @@ private async Task 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) }; diff --git a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderServiceExtensionsTest.cs b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderServiceExtensionsTest.cs index 9bed4bfe5ab80..33b9c25fcdd8b 100644 --- a/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderServiceExtensionsTest.cs +++ b/src/libraries/Microsoft.Extensions.DependencyInjection/tests/DI.Tests/ServiceProviderServiceExtensionsTest.cs @@ -4,6 +4,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; using Xunit; namespace Microsoft.Extensions.DependencyInjection @@ -213,6 +214,23 @@ public void NonGeneric_GetServices_WithBuildServiceProvider_Returns_EmptyList_Wh Assert.IsType>(services); } + [Fact] + public async Task CreateAsyncScope_Returns_AsyncServiceScope_Wrapping_ServiceScope() + { + // Arrange + var serviceCollection = new ServiceCollection(); + serviceCollection.AddScoped(); + var serviceProvider = serviceCollection.BuildServiceProvider(); + + await using var scope = serviceProvider.CreateAsyncScope(); + + // Act + var service = scope.ServiceProvider.GetService(); + + // Assert + Assert.IsType(service); + } + private static IServiceProvider CreateTestServiceProvider(int count) { var serviceCollection = new ServiceCollection();