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

Make CreateScope return ServiceScope that also implements IAsyncDisposable #43970

Closed
bjorkstromm opened this issue Oct 28, 2020 · 12 comments · Fixed by #51840
Closed

Make CreateScope return ServiceScope that also implements IAsyncDisposable #43970

bjorkstromm opened this issue Oct 28, 2020 · 12 comments · Fixed by #51840
Assignees
Labels
api-approved API was approved in API review, it can be implemented area-Extensions-DependencyInjection help wanted [up-for-grabs] Good issue for external contributors
Milestone

Comments

@bjorkstromm
Copy link
Contributor

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.

@bjorkstromm bjorkstromm added the api-suggestion Early API idea and discussion, it is NOT ready for implementation label Oct 28, 2020
@Dotnet-GitSync-Bot Dotnet-GitSync-Bot added area-Extensions-DependencyInjection untriaged New issue has not been triaged by the area owner labels Oct 28, 2020
@ghost
Copy link

ghost commented Oct 28, 2020

Tagging subscribers to this area: @eerhardt, @maryamariyan
See info in area-owners.md if you want to be subscribed.

@eerhardt
Copy link
Member

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

Is this a valuable enough change to break these implementations?

cc @davidfowl

@davidfowl
Copy link
Member

Is this a valuable enough change to break these implementations?

It is not.

@bjorkstromm
Copy link
Contributor Author

As Microsoft.Extensions.DependencyInjection is basically broken when it comes to using IAsyncDisposable in scopes, would you consider adding a new extension method for IServiceProvider in Microsoft.Extensions.DependencyInjection that would return a type that implements both IAsyncDisposable and IServiceScope? That way we won't break Microsoft.Extensions.DependencyInjection.Abstractions.

@davidfowl
Copy link
Member

Can you propose the API? It would need to be an extension method (with a different name than CreateScope) that wraps the existing scope and does the test for IAsyncDisposable.

@bjorkstromm
Copy link
Contributor Author

Can you propose the API?

Sure, naming is the hardest 😄. Sometimes I wish one could just slam on the good old Ex suffix from Win32.

Alternative 1. Introduce new interface

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

    public static class ServiceProviderScopeExtensions
    {
        public static IAsyncDisposableServiceScope CreateServiceScope(this IServiceProvider provider) { }
    }
}

It would need to be an extension method (with a different name than CreateScope) that wraps the existing scope and does the test for IAsyncDisposable.

No need to wrap in most cases if we let ServiceProviderEngineScope implement the new interface. We only need to wrap in an internal class if the returned scope from IServiceScopeFactory isn't of type ServiceProviderEngineScope.

internal class ServiceProviderEngineScope : IServiceScope, IServiceProvider, IAsyncDisposable

Alternative 2. Introduce new class

This is similar to what's happening when creating ServiceProvider from IServiceCollection.

namespace Microsoft.Extensions.DependencyInjection
{
    public abstract class ServiceScope : IServiceScope, IAsyncDisposable
    {
        public abstract IServiceProvider ServiceProvider { get; }
        public abstract void Dispose();
        public abstract ValueTask DisposeAsync();
    }

    public static class ServiceProviderScopeExtensions
    {
        public static ServiceScope CreateServiceScope(this IServiceProvider provider) { }
    }
}

Same here, no need to wrap in most cases if we let ServiceProviderEngineScope inherit from ServiceScope.

@davidfowl
Copy link
Member

Alternative 1 would return null if the underlying scope doesn't implement the interface? Remember our container is a single implementation, there are many others that would need to implement this new interface. That means it would return null if the implementation doesn't?

The latter is fine but requires an additional allocation for those providers that don't derive from our new base class.

@bjorkstromm
Copy link
Contributor Author

Alternative 1 would wrap the scope in an internal class (implenting the interface) instead of returning null, thus would also require an additional allocation. So both alternatives are similar.

Question. If the new interface (or class) and extension method would reside in Microsoft.Extensions.DependencyInjection, would it affect other implementations? Quickly checking, the other implementations depend on Microsoft.Extensions.DependencyInjection.Abstractions. How is the root container for e.g. Autofac implementation created when using with ASP.NET Core? Does it still rely on Microsoft.Extensions.DependencyInjection when creating the container (service provider)?

Would it make sense to add the new interface and extensions method to Microsoft.Extensions.DependencyInjection.Abstractions instead and obsolete the the old extension instead? This would also require a new interface for replacing IServiceScopeFactory.

@maryamariyan maryamariyan removed the untriaged New issue has not been triaged by the area owner label Feb 18, 2021
@maryamariyan maryamariyan added this to the Future milestone Feb 18, 2021
@ghost ghost moved this from Untriaged to Future in ML, Extensions, Globalization, etc, POD. Feb 18, 2021
@davidfowl
Copy link
Member

davidfowl commented Mar 14, 2021

This keeps coming up so I think we should go with a modified version of alternative 2:

public static class ServiceScopeExtensions
{
    public static AsyncServiceScope CreateAsyncServiceScope(this IServiceProvider serviceProvider)
    {
        return new AsyncServiceScope(serviceProvider.CreateScope());
    }
}

public struct AsyncServiceScope : IServiceScope, IAsyncDisposable
{
    private readonly IServiceScope _serviceScope;

    public AsyncServiceScope(IServiceScope serviceScope)
    {
        _serviceScope = serviceScope;
    }

    public IServiceProvider ServiceProvider => _serviceScope.ServiceProvider;

    public void Dispose()
    {
        _serviceScope.Dispose();
    }

    public ValueTask DisposeAsync()
    {
        if (_serviceScope is IAsyncDisposable ad)
        {
            return ad.DisposeAsync();
        }
        _serviceScope.Dispose();
        return ValueTask.CompletedTask;
    }
}

@maryamariyan we should take this to one API review. The change is tiny but we need to agree on the name.

@davidfowl davidfowl modified the milestones: Future, 6.0.0 Mar 14, 2021
@ghost ghost moved this from Future to 6.0.0 in ML, Extensions, Globalization, etc, POD. Mar 14, 2021
@davidfowl davidfowl added api-ready-for-review API is ready for review, it is NOT ready for implementation and removed api-suggestion Early API idea and discussion, it is NOT ready for implementation labels Mar 14, 2021
@davidfowl davidfowl self-assigned this Mar 26, 2021
@davidfowl davidfowl added the blocking Marks issues that we want to fast track in order to unblock other important work label Apr 16, 2021
@bartonjs
Copy link
Member

bartonjs commented Apr 23, 2021

Video

  • We renamed CreateAsyncServiceScope to CreateAsyncScope to better match the existing name
  • We moved it into the extension type for the existing method to avoid making a new type
public partial class ServiceProviderServiceExtensions
{
    public static AsyncServiceScope CreateAsyncScope(this IServiceProvider serviceProvider)
    {
        return new AsyncServiceScope(serviceProvider.CreateScope());
    }
}

public struct AsyncServiceScope : IServiceScope, IAsyncDisposable
{
    private readonly IServiceScope _serviceScope;

    public AsyncServiceScope(IServiceScope serviceScope)
    {
        _serviceScope = serviceScope;
    }

    public IServiceProvider ServiceProvider => _serviceScope.ServiceProvider;

    public void Dispose()
    {
        _serviceScope.Dispose();
    }

    public ValueTask DisposeAsync()
    {
        if (_serviceScope is IAsyncDisposable ad)
        {
            return ad.DisposeAsync();
        }
        _serviceScope.Dispose();
        return ValueTask.CompletedTask;
    }
}

@bartonjs bartonjs added api-approved API was approved in API review, it can be implemented and removed api-ready-for-review API is ready for review, it is NOT ready for implementation blocking Marks issues that we want to fast track in order to unblock other important work labels Apr 23, 2021
@maryamariyan maryamariyan added the help wanted [up-for-grabs] Good issue for external contributors label Apr 23, 2021
@davidfowl
Copy link
Member

@bjorkstromm Are you interested in sending a pull request for this API?

@bjorkstromm
Copy link
Contributor Author

@bjorkstromm Are you interested in sending a pull request for this API?

Sure! It would be my pleasure.

@davidfowl davidfowl assigned bjorkstromm and unassigned davidfowl Apr 23, 2021
bjorkstromm added a commit to bjorkstromm/runtime that referenced this issue Apr 25, 2021
…isposable.

- 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 dotnet#43970
@ghost ghost added the in-pr There is an active PR which will close this issue when it is merged label Apr 25, 2021
ML, Extensions, Globalization, etc, POD. automation moved this from 6.0.0 to Done Apr 28, 2021
@ghost ghost removed the in-pr There is an active PR which will close this issue when it is merged label Apr 28, 2021
eerhardt pushed a commit that referenced this issue Apr 28, 2021
…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>
@ghost ghost locked as resolved and limited conversation to collaborators May 28, 2021
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
api-approved API was approved in API review, it can be implemented area-Extensions-DependencyInjection help wanted [up-for-grabs] Good issue for external contributors
Projects
None yet
Development

Successfully merging a pull request may close this issue.

6 participants