To better understand the subject first, we need to know about IAsyncDisposable!
One of the features that added to .NET Core 3.0 was IAsyncDisposable, which lets you run async code when disposing of resources, helping to avoid deadlocks. Before we had only an IDisposable interface with a void
method called Dispose, so obviously we couldn't use async/await, We had something like this:
public class Phone : IPhone, IDisposable
{
public void Dispose()
{
// Free resources
}
}
But here is the async version of it:
public class Phone : IPhone, IAsyncDisposable
{
public ValueTask DisposeAsync()
{
// Free resources asynchronously
}
}
The DisposeAsync
method allows us to clean up resources asynchronously.
So what is the issue then?! As maybe you already thought, the tricky part is that any code cleaning up IDisposable objects now also needs to handle IAsyncDisposable objects. This means those code paths must also be async, due to the nature of async/await.
One of the places that needs to change is the DependencyInjection
container, so we all know DI will resolve services and then dispose
of them based on their lifetime. So normally DI will call either Dispose or DisposeAsync method based on class implementation.
So given that IPhone
interface, assuming we inject it as Scoped
service:
builder.Services.AddScoped<IPhone, Phone>();
//------------Endpoint-----------
app.MapGet("/phones", ([FromServices] IPhone phone) => { /* code */});
Every time we call the /phones
API, DI will create a new scope
and instantiate IPhone
implementation and after the request is finished will dispose the object (by calling DisposeAsync
).
All good, no issues, right?
The problem comes into the picture when we want to create a scope manually by using IServiceProvider
or IServiceScopeFactory
, let's back to that API again:
app.MapGet("/phones", ([FromServices] IServiceScopeFactory scopeFactory) =>
{
using var scope = scopeFactory.CreateScope();
var phone = scope.ServiceProvider.GetRequiredService<IPhone>();
/* Code */
})
Basically, there we create a scope and then get a new instance of IPhone
implementation which is Phone
. If we call the API you will get this exception:
System.InvalidOperationException
HResult=0x80131509
Message= 'Phone' type only implements IAsyncDisposable. Use DisposeAsync to dispose the container.
Source=Microsoft.Extensions.DependencyInjection
StackTrace:
at Microsoft.Extensions.DependencyInjection.ServiceLookup.ServiceProviderEngineScope.Dispose()
at Program.<>c.<<Main>$>b__0_3(IServiceScopeFactory scopeFactory) in C:\Users\******\Program.cs:line 114
at Microsoft.AspNetCore.HttpsPolicy.HttpsRedirectionMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.ExceptionHandlerMiddlewareImpl.Invoke(HttpContext context)
It is complaining that Phone is implemented the IAsyncDispose
but we are calling the Dispose
method not DisposeAsync
, actually calling dispose method will be done behind the scenes when using
is done and tries to dispose the created scope
and all created instances, here AsyncServiceScope
comes into the picture!
AsyncServiceScope allows DI to call the DisposeAsync
method and basically means allows DI to dispose objects asynchronously!
Then our above code should be changed as below:
app.MapGet("/phones", async ([FromServices] IServiceScopeFactory scopeFactory) =>
{
await using var scope = scopeFactory.CreateAsyncScope();
var phone = scope.ServiceProvider.GetRequiredService<IPhone>();
/* Code */
});
Now DI is happy and we are happy as everything works!
Code Simple!