Skip to content

Make CreateScope return ServiceScope that also implements IAsyncDisposable #43970

@bjorkstromm

Description

@bjorkstromm

Background and Motivation

IAsyncDisposable support in DI was added a while ago (see dotnet/extensions#426), but then it was agreed to only add interface implementation to ServiceProvider. I didn't find any tracking issue on implementing it also on scopes.

Currently, the following code will throw exception:

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

await using var provider = new ServiceCollection()
        .AddScoped<Foo>()
        .BuildServiceProvider();

using (var scope = provider.CreateScope())
{
    var foo = scope.ServiceProvider.GetRequiredService<Foo>();
}

class Foo : IAsyncDisposable
{
    public ValueTask DisposeAsync() => default;
}
Unhandled exception. System.InvalidOperationException: 'Foo' type only implements IAsyncDisposable. Use DisposeAsync to dispose the container.
   at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.Dispose()
   at <Program>$.<<Main>$>d__0.MoveNext() in C:\src\tmp\asyncdisposablescope\Program.cs:line 12
--- End of stack trace from previous location ---
   at <Program>$.<<Main>$>d__0.MoveNext() in C:\src\tmp\asyncdisposablescope\Program.cs:line 12
--- End of stack trace from previous location ---
   at <Program>$.<Main>(String[] args)

Workaround today is to cast the returned scope to IAsyncDisposable, because the concrete implementation in DI implements that.

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

await using var provider = new ServiceCollection()
        .AddScoped<Foo>()
        .BuildServiceProvider();

{
    var scope = provider.CreateScope();

    var foo = scope.ServiceProvider.GetRequiredService<Foo>();

    await ((IAsyncDisposable)scope).DisposeAsync();
}

class Foo : IAsyncDisposable
{
    public ValueTask DisposeAsync() => default;
}

Proposed API

IServiceScope implements IAsyncDisposable

 namespace Microsoft.Extensions.DependencyInjection
 {
-    public interface IServiceScope : IDisposable
+    public interface IServiceScope : IDisposable, IAsyncDisposable
 }

Usage Examples

Changing this would allow us to use DisposeAsync on the returned scope.

using System;
using System.Threading.Tasks;
using Microsoft.Extensions.DependencyInjection;

await using var provider = new ServiceCollection()
        .AddScoped<Foo>()
        .BuildServiceProvider();

await using (var scope = provider.CreateScope())
{
    var foo = scope.ServiceProvider.GetRequiredService<Foo>();
}

class Foo : IAsyncDisposable
{
    public ValueTask DisposeAsync() => default;
}

Alternative Designs

Create a new interface IAsyncDisposableServiceScope (needs better naming), which implements IServiceScope and IAsyncDisposable and let IServiceScopeFactory.CreateScope() return that.

 namespace Microsoft.Extensions.DependencyInjection
 {
+    public interface IAsyncDisposableServiceScope : IServiceScope, IAsyncDisposable { }

     public interface IServiceScopeFactory
     {
-        IServiceScope CreateScope();
+        IAsyncDisposableServiceScope CreateScope();
     }

    public static class ServiceProviderServiceExtensions
    {
-        public static IServiceScope CreateScope(this IServiceProvider provider)
+        public static IAsyncDisposableServiceScope CreateScope(this IServiceProvider provider)
    }
}

Risks

Will probably break some 3rd party implementations of Microsoft.Extensions.DependencyInjection.Abstractions and consumers.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions