Skip to content

Commit

Permalink
Merge pull request #573 from seesharper/asyncdisposable
Browse files Browse the repository at this point in the history
Asyncdisposable
  • Loading branch information
seesharper committed Sep 12, 2022
2 parents f7d850b + f2813bd commit 373ccce
Show file tree
Hide file tree
Showing 5 changed files with 261 additions and 29 deletions.
23 changes: 23 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -598,6 +598,29 @@ var container = new ServiceContainer(o => o.EnableCurrentScope = false);

This also improves performance ever so slightly as we don't need to maintain a current scope when scopes are started and ended.

### IAsyncDisposable

LightInject also supports [IAsyncDisposable](https://docs.microsoft.com/en-us/dotnet/api/system.iasyncdisposable) meaning that [IAsyncDisposable.DisposeAsync](https://docs.microsoft.com/en-us/dotnet/api/system.iasyncdisposable.disposeasync) will be called if the scope is started with a using-block adding the await `await` keyword.

```csharp
await using (var scope = container.BeginScope())
{
asyncDisposable = container.GetInstance<AsyncDisposable>();
}
```

The `Scope` returned from `BeginScope` also implements `IAsyncDisposable` and will call `DisposeAsync` on all scoped services resolved within the `Scope`.
Services only implementing `IDisposable` will also be disposed the the async scope ends.

If on the other hand, a service ONLY implements `IAsyncDisposable` and is resolved within a synchronous scope, an exception will be thrown

```csharp
using (var scope = container.BeginScope())
{
asyncDisposable = container.GetInstance<AsyncDisposable>();
}
```

## Dependencies ##


Expand Down
141 changes: 141 additions & 0 deletions src/LightInject.Tests/AsyncDisposableTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Xunit;

namespace LightInject.Tests
{
public class AsyncDisposableTests : TestBase
{
[Fact]
public async Task ShouldDisposeAsyncDisposable()
{
var container = CreateContainer();
List<object> disposedObjects = new();

container.RegisterScoped<AsyncDisposable>(sf => new AsyncDisposable(disposedObject => disposedObjects.Add(disposedObject)));

AsyncDisposable asyncDisposable = null;
await using (var scope = container.BeginScope())
{
asyncDisposable = container.GetInstance<AsyncDisposable>();
}

Assert.Contains(asyncDisposable, disposedObjects);
}

[Fact]
public async Task ShouldDisposeSlowAsyncDisposable()
{
var container = CreateContainer();
List<object> disposedObjects = new();
container.RegisterScoped<SlowAsyncDisposable>(sf => new SlowAsyncDisposable(disposedObject => disposedObjects.Add(disposedObject)));

SlowAsyncDisposable asyncDisposable = null;
await using (var scope = container.BeginScope())
{
asyncDisposable = container.GetInstance<SlowAsyncDisposable>();
}

Assert.Contains(asyncDisposable, disposedObjects);
}

[Fact]
public async Task ShouldDisposeInCorrectOrder()
{
var container = CreateContainer();
List<object> disposedObjects = new();
container.RegisterScoped<AsyncDisposable>(sf => new AsyncDisposable(disposedObject => disposedObjects.Add(disposedObject)));
container.RegisterScoped<SlowAsyncDisposable>(sf => new SlowAsyncDisposable(disposedObject => disposedObjects.Add(disposedObject)));
container.RegisterScoped<Disposable>(sf => new Disposable(disposedObject => disposedObjects.Add(disposedObject)));

AsyncDisposable asyncDisposable = null;
SlowAsyncDisposable slowAsyncDisposable = null;
Disposable disposable = null;
await using (var scope = container.BeginScope())
{
disposable = container.GetInstance<Disposable>();
asyncDisposable = container.GetInstance<AsyncDisposable>();
slowAsyncDisposable = container.GetInstance<SlowAsyncDisposable>();
}

Assert.Same(disposedObjects[0], slowAsyncDisposable);
Assert.Same(disposedObjects[1], asyncDisposable);
Assert.Same(disposedObjects[2], disposable);
}

[Fact]
public async Task ShouldDisposeDisposable()
{
var container = CreateContainer();
List<object> disposedObjects = new();

container.RegisterScoped<Disposable>(sf => new Disposable(disposedObject => disposedObjects.Add(disposedObject)));
Disposable disposable = null;
await using (var scope = container.BeginScope())
{
disposable = container.GetInstance<Disposable>();
}

Assert.Contains(disposable, disposedObjects);
}

[Fact]
public void ShouldThrowWhenAsyncDisposableIsDisposedInSynchronousScope()
{
var container = CreateContainer();
container.RegisterScoped<AsyncDisposable>(sf => new AsyncDisposable(_ => { }));

AsyncDisposable asyncDisposable = null;
var scope = container.BeginScope();
asyncDisposable = container.GetInstance<AsyncDisposable>();

Assert.Throws<InvalidOperationException>(() => scope.Dispose());
}

public class SlowAsyncDisposable : IAsyncDisposable
{
private readonly Action<object> onDisposed;

public SlowAsyncDisposable(Action<object> onDisposed)
{
this.onDisposed = onDisposed;
}
public async ValueTask DisposeAsync()
{
await Task.Delay(100);
onDisposed(this);
}
}

public class AsyncDisposable : IAsyncDisposable
{
private readonly Action<object> onDisposed;

public AsyncDisposable(Action<object> onDisposed)
{
this.onDisposed = onDisposed;
}
public ValueTask DisposeAsync()
{
onDisposed(this);
return ValueTask.CompletedTask;
}
}

public class Disposable : IDisposable
{
private readonly Action<object> onDisposed;

public Disposable(Action<object> onDisposed)
{
this.onDisposed = onDisposed;
}

public void Dispose()
{
onDisposed(this);
}
}
}
}
7 changes: 5 additions & 2 deletions src/LightInject.Tests/LightInject.Tests.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
<PropertyGroup>
<TargetFramework>net6.0</TargetFramework>
<NoWarn>$(NoWarn);CS0579</NoWarn>
<!-- <TestTargetFrameworks>net6.0;netstandard2.0</TestTargetFrameworks> -->
<TestTargetFrameworks>net6.0;netstandard2.0</TestTargetFrameworks>
<TestTargetFramework>net6.0</TestTargetFramework>
</PropertyGroup>
<Choose>
Expand All @@ -13,9 +13,12 @@
</PropertyGroup>
</When>
</Choose>
<PropertyGroup Condition=" '$(TestTargetFramework)'=='net6.1' ">
<PropertyGroup Condition=" '$(TestTargetFramework)'=='net6.0' ">
<DefineConstants>USE_ASSEMBLY_VERIFICATION</DefineConstants>
</PropertyGroup>
<ItemGroup Condition=" '$(TestTargetFramework)' == 'net46' OR '$(TestTargetFramework)' == 'netstandard2.0'">
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="coverlet.collector" Version="3.1.2">
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
Expand Down
95 changes: 85 additions & 10 deletions src/LightInject/LightInject.cs
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
******************************************************************************
LightInject version 6.5.1
LightInject version 6.6.0
http://www.lightinject.net/
http://twitter.com/bernhardrichter
******************************************************************************/
Expand Down Expand Up @@ -57,6 +57,7 @@ namespace LightInject
using System.Text;
using System.Text.RegularExpressions;
using System.Threading;
using System.Threading.Tasks;

/// <summary>
/// A delegate that represents the dynamic method compiled to resolved service instances.
Expand Down Expand Up @@ -6606,7 +6607,7 @@ public override Scope CurrentScope
/// <summary>
/// Represents a scope.
/// </summary>
public class Scope : IServiceFactory, IDisposable
public class Scope : IServiceFactory, IDisposable, IAsyncDisposable
{
/// <summary>
/// Gets a value indicating whether this scope has been disposed.
Expand All @@ -6624,7 +6625,7 @@ public class Scope : IServiceFactory, IDisposable

private readonly ServiceContainer serviceFactory;

private List<IDisposable> disposableObjects;
private List<object> disposableObjects;

private ImmutableMapTree<object> createdInstances = ImmutableMapTree<object>.Empty;

Expand Down Expand Up @@ -6657,14 +6658,14 @@ public Scope(ServiceContainer serviceFactory)
/// <summary>
/// Registers the <paramref name="disposable"/> so that it is disposed when the scope is completed.
/// </summary>
/// <param name="disposable">The <see cref="IDisposable"/> object to register.</param>
public void TrackInstance(IDisposable disposable)
/// <param name="disposable">The <see cref="IDisposable"/> or <see cref="IAsyncDisposable"/> object to register.</param>
public void TrackInstance(object disposable)
{
lock (lockObject)
{
if (disposableObjects == null)
{
disposableObjects = new List<IDisposable>();
disposableObjects = new List<object>();
}

disposableObjects.Add(disposable);
Expand All @@ -6689,6 +6690,10 @@ public void Dispose()
disposable.Dispose();
}
}
else
{
throw new InvalidOperationException($"The type {disposableObjects[i].GetType()} only implements `IAsyncDisposable` and can only be disposed in an asynchronous scope started with `BeginScopeAsync()`");
}
}
}

Expand All @@ -6698,6 +6703,76 @@ public void Dispose()
IsDisposed = true;
}

/// <inheritdoc/>
public ValueTask DisposeAsync()
{
if (disposableObjects != null && disposableObjects.Count > 0)
{
HashSet<object> disposedObjects = new HashSet<object>();

for (var i = disposableObjects.Count - 1; i >= 0; i--)
{
object objectToDispose = disposableObjects[i];
if (objectToDispose is IAsyncDisposable asyncDisposable)
{
if (!disposedObjects.Add(objectToDispose))
{
continue;
}

ValueTask valueTask = asyncDisposable.DisposeAsync();
if (!valueTask.IsCompletedSuccessfully)
{
// If we end up here, it means that the ValueTask is not completed
return Await(i, valueTask, disposableObjects, disposedObjects);
}

// If its a IValueTaskSource backed ValueTask,
// inform it its result has been read so it can reset
valueTask.GetAwaiter().GetResult();
}
else
{
if (!disposedObjects.Add(objectToDispose))
{
continue;
}
else
{
((IDisposable)objectToDispose).Dispose();
}
}
}
}
return default;

static async ValueTask Await(int i, ValueTask vt, List<object> toDispose, HashSet<object> disposedObjects)
{
await vt.ConfigureAwait(false);

// vt is acting on the disposable at index i,
// decrement it and move to the next iteration
i--;

for (; i >= 0; i--)
{
object objectToDispose = toDispose[i];
if (!disposedObjects.Add(objectToDispose))
{
continue;
}
if (objectToDispose is IAsyncDisposable asyncDisposable)
{
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
}
else
{
((IDisposable)objectToDispose).Dispose();
}
}
}
}

/// <inheritdoc/>
public Scope BeginScope() => serviceFactory.BeginScope();

Expand Down Expand Up @@ -6747,9 +6822,9 @@ internal object GetScopedInstance(GetInstanceDelegate getInstanceDelegate, objec
if (createdInstance == null)
{
createdInstance = getInstanceDelegate(arguments, this);
if (createdInstance is IDisposable disposable)
if (createdInstance is IDisposable || createdInstance is IAsyncDisposable)
{
TrackInstance(disposable);
TrackInstance(createdInstance);
}

Interlocked.Exchange(ref createdInstances, createdInstances.Add(instanceDelegateIndex, createdInstance));
Expand Down Expand Up @@ -8384,7 +8459,7 @@ static ScopeLoader()

public static object ValidateTrackedTransient(object instance, Scope scope)
{
if (instance is IDisposable disposable)
if (instance is IDisposable || instance is IAsyncDisposable)
{
if (scope == null)
{
Expand All @@ -8394,7 +8469,7 @@ public static object ValidateTrackedTransient(object instance, Scope scope)
throw new InvalidOperationException(message);
}

scope.TrackInstance(disposable);
scope.TrackInstance(instance);
}

return instance;
Expand Down

0 comments on commit 373ccce

Please sign in to comment.