Skip to content

Commit 373ccce

Browse files
authored
Merge pull request #573 from seesharper/asyncdisposable
Asyncdisposable
2 parents f7d850b + f2813bd commit 373ccce

File tree

5 files changed

+261
-29
lines changed

5 files changed

+261
-29
lines changed

readme.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -598,6 +598,29 @@ var container = new ServiceContainer(o => o.EnableCurrentScope = false);
598598

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

601+
### IAsyncDisposable
602+
603+
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.
604+
605+
```csharp
606+
await using (var scope = container.BeginScope())
607+
{
608+
asyncDisposable = container.GetInstance<AsyncDisposable>();
609+
}
610+
```
611+
612+
The `Scope` returned from `BeginScope` also implements `IAsyncDisposable` and will call `DisposeAsync` on all scoped services resolved within the `Scope`.
613+
Services only implementing `IDisposable` will also be disposed the the async scope ends.
614+
615+
If on the other hand, a service ONLY implements `IAsyncDisposable` and is resolved within a synchronous scope, an exception will be thrown
616+
617+
```csharp
618+
using (var scope = container.BeginScope())
619+
{
620+
asyncDisposable = container.GetInstance<AsyncDisposable>();
621+
}
622+
```
623+
601624
## Dependencies ##
602625

603626

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
using System;
2+
using System.Collections.Generic;
3+
using System.Threading.Tasks;
4+
using Xunit;
5+
6+
namespace LightInject.Tests
7+
{
8+
public class AsyncDisposableTests : TestBase
9+
{
10+
[Fact]
11+
public async Task ShouldDisposeAsyncDisposable()
12+
{
13+
var container = CreateContainer();
14+
List<object> disposedObjects = new();
15+
16+
container.RegisterScoped<AsyncDisposable>(sf => new AsyncDisposable(disposedObject => disposedObjects.Add(disposedObject)));
17+
18+
AsyncDisposable asyncDisposable = null;
19+
await using (var scope = container.BeginScope())
20+
{
21+
asyncDisposable = container.GetInstance<AsyncDisposable>();
22+
}
23+
24+
Assert.Contains(asyncDisposable, disposedObjects);
25+
}
26+
27+
[Fact]
28+
public async Task ShouldDisposeSlowAsyncDisposable()
29+
{
30+
var container = CreateContainer();
31+
List<object> disposedObjects = new();
32+
container.RegisterScoped<SlowAsyncDisposable>(sf => new SlowAsyncDisposable(disposedObject => disposedObjects.Add(disposedObject)));
33+
34+
SlowAsyncDisposable asyncDisposable = null;
35+
await using (var scope = container.BeginScope())
36+
{
37+
asyncDisposable = container.GetInstance<SlowAsyncDisposable>();
38+
}
39+
40+
Assert.Contains(asyncDisposable, disposedObjects);
41+
}
42+
43+
[Fact]
44+
public async Task ShouldDisposeInCorrectOrder()
45+
{
46+
var container = CreateContainer();
47+
List<object> disposedObjects = new();
48+
container.RegisterScoped<AsyncDisposable>(sf => new AsyncDisposable(disposedObject => disposedObjects.Add(disposedObject)));
49+
container.RegisterScoped<SlowAsyncDisposable>(sf => new SlowAsyncDisposable(disposedObject => disposedObjects.Add(disposedObject)));
50+
container.RegisterScoped<Disposable>(sf => new Disposable(disposedObject => disposedObjects.Add(disposedObject)));
51+
52+
AsyncDisposable asyncDisposable = null;
53+
SlowAsyncDisposable slowAsyncDisposable = null;
54+
Disposable disposable = null;
55+
await using (var scope = container.BeginScope())
56+
{
57+
disposable = container.GetInstance<Disposable>();
58+
asyncDisposable = container.GetInstance<AsyncDisposable>();
59+
slowAsyncDisposable = container.GetInstance<SlowAsyncDisposable>();
60+
}
61+
62+
Assert.Same(disposedObjects[0], slowAsyncDisposable);
63+
Assert.Same(disposedObjects[1], asyncDisposable);
64+
Assert.Same(disposedObjects[2], disposable);
65+
}
66+
67+
[Fact]
68+
public async Task ShouldDisposeDisposable()
69+
{
70+
var container = CreateContainer();
71+
List<object> disposedObjects = new();
72+
73+
container.RegisterScoped<Disposable>(sf => new Disposable(disposedObject => disposedObjects.Add(disposedObject)));
74+
Disposable disposable = null;
75+
await using (var scope = container.BeginScope())
76+
{
77+
disposable = container.GetInstance<Disposable>();
78+
}
79+
80+
Assert.Contains(disposable, disposedObjects);
81+
}
82+
83+
[Fact]
84+
public void ShouldThrowWhenAsyncDisposableIsDisposedInSynchronousScope()
85+
{
86+
var container = CreateContainer();
87+
container.RegisterScoped<AsyncDisposable>(sf => new AsyncDisposable(_ => { }));
88+
89+
AsyncDisposable asyncDisposable = null;
90+
var scope = container.BeginScope();
91+
asyncDisposable = container.GetInstance<AsyncDisposable>();
92+
93+
Assert.Throws<InvalidOperationException>(() => scope.Dispose());
94+
}
95+
96+
public class SlowAsyncDisposable : IAsyncDisposable
97+
{
98+
private readonly Action<object> onDisposed;
99+
100+
public SlowAsyncDisposable(Action<object> onDisposed)
101+
{
102+
this.onDisposed = onDisposed;
103+
}
104+
public async ValueTask DisposeAsync()
105+
{
106+
await Task.Delay(100);
107+
onDisposed(this);
108+
}
109+
}
110+
111+
public class AsyncDisposable : IAsyncDisposable
112+
{
113+
private readonly Action<object> onDisposed;
114+
115+
public AsyncDisposable(Action<object> onDisposed)
116+
{
117+
this.onDisposed = onDisposed;
118+
}
119+
public ValueTask DisposeAsync()
120+
{
121+
onDisposed(this);
122+
return ValueTask.CompletedTask;
123+
}
124+
}
125+
126+
public class Disposable : IDisposable
127+
{
128+
private readonly Action<object> onDisposed;
129+
130+
public Disposable(Action<object> onDisposed)
131+
{
132+
this.onDisposed = onDisposed;
133+
}
134+
135+
public void Dispose()
136+
{
137+
onDisposed(this);
138+
}
139+
}
140+
}
141+
}

src/LightInject.Tests/LightInject.Tests.csproj

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
<PropertyGroup>
44
<TargetFramework>net6.0</TargetFramework>
55
<NoWarn>$(NoWarn);CS0579</NoWarn>
6-
<!-- <TestTargetFrameworks>net6.0;netstandard2.0</TestTargetFrameworks> -->
6+
<TestTargetFrameworks>net6.0;netstandard2.0</TestTargetFrameworks>
77
<TestTargetFramework>net6.0</TestTargetFramework>
88
</PropertyGroup>
99
<Choose>
@@ -13,9 +13,12 @@
1313
</PropertyGroup>
1414
</When>
1515
</Choose>
16-
<PropertyGroup Condition=" '$(TestTargetFramework)'=='net6.1' ">
16+
<PropertyGroup Condition=" '$(TestTargetFramework)'=='net6.0' ">
1717
<DefineConstants>USE_ASSEMBLY_VERIFICATION</DefineConstants>
1818
</PropertyGroup>
19+
<ItemGroup Condition=" '$(TestTargetFramework)' == 'net46' OR '$(TestTargetFramework)' == 'netstandard2.0'">
20+
<PackageReference Include="Microsoft.Bcl.AsyncInterfaces" Version="6.0.0" />
21+
</ItemGroup>
1922
<ItemGroup>
2023
<PackageReference Include="coverlet.collector" Version="3.1.2">
2124
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>

src/LightInject/LightInject.cs

Lines changed: 85 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
2626
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
2727
SOFTWARE.
2828
******************************************************************************
29-
LightInject version 6.5.1
29+
LightInject version 6.6.0
3030
http://www.lightinject.net/
3131
http://twitter.com/bernhardrichter
3232
******************************************************************************/
@@ -57,6 +57,7 @@ namespace LightInject
5757
using System.Text;
5858
using System.Text.RegularExpressions;
5959
using System.Threading;
60+
using System.Threading.Tasks;
6061

6162
/// <summary>
6263
/// A delegate that represents the dynamic method compiled to resolved service instances.
@@ -6606,7 +6607,7 @@ public override Scope CurrentScope
66066607
/// <summary>
66076608
/// Represents a scope.
66086609
/// </summary>
6609-
public class Scope : IServiceFactory, IDisposable
6610+
public class Scope : IServiceFactory, IDisposable, IAsyncDisposable
66106611
{
66116612
/// <summary>
66126613
/// Gets a value indicating whether this scope has been disposed.
@@ -6624,7 +6625,7 @@ public class Scope : IServiceFactory, IDisposable
66246625

66256626
private readonly ServiceContainer serviceFactory;
66266627

6627-
private List<IDisposable> disposableObjects;
6628+
private List<object> disposableObjects;
66286629

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

@@ -6657,14 +6658,14 @@ public Scope(ServiceContainer serviceFactory)
66576658
/// <summary>
66586659
/// Registers the <paramref name="disposable"/> so that it is disposed when the scope is completed.
66596660
/// </summary>
6660-
/// <param name="disposable">The <see cref="IDisposable"/> object to register.</param>
6661-
public void TrackInstance(IDisposable disposable)
6661+
/// <param name="disposable">The <see cref="IDisposable"/> or <see cref="IAsyncDisposable"/> object to register.</param>
6662+
public void TrackInstance(object disposable)
66626663
{
66636664
lock (lockObject)
66646665
{
66656666
if (disposableObjects == null)
66666667
{
6667-
disposableObjects = new List<IDisposable>();
6668+
disposableObjects = new List<object>();
66686669
}
66696670

66706671
disposableObjects.Add(disposable);
@@ -6689,6 +6690,10 @@ public void Dispose()
66896690
disposable.Dispose();
66906691
}
66916692
}
6693+
else
6694+
{
6695+
throw new InvalidOperationException($"The type {disposableObjects[i].GetType()} only implements `IAsyncDisposable` and can only be disposed in an asynchronous scope started with `BeginScopeAsync()`");
6696+
}
66926697
}
66936698
}
66946699

@@ -6698,6 +6703,76 @@ public void Dispose()
66986703
IsDisposed = true;
66996704
}
67006705

6706+
/// <inheritdoc/>
6707+
public ValueTask DisposeAsync()
6708+
{
6709+
if (disposableObjects != null && disposableObjects.Count > 0)
6710+
{
6711+
HashSet<object> disposedObjects = new HashSet<object>();
6712+
6713+
for (var i = disposableObjects.Count - 1; i >= 0; i--)
6714+
{
6715+
object objectToDispose = disposableObjects[i];
6716+
if (objectToDispose is IAsyncDisposable asyncDisposable)
6717+
{
6718+
if (!disposedObjects.Add(objectToDispose))
6719+
{
6720+
continue;
6721+
}
6722+
6723+
ValueTask valueTask = asyncDisposable.DisposeAsync();
6724+
if (!valueTask.IsCompletedSuccessfully)
6725+
{
6726+
// If we end up here, it means that the ValueTask is not completed
6727+
return Await(i, valueTask, disposableObjects, disposedObjects);
6728+
}
6729+
6730+
// If its a IValueTaskSource backed ValueTask,
6731+
// inform it its result has been read so it can reset
6732+
valueTask.GetAwaiter().GetResult();
6733+
}
6734+
else
6735+
{
6736+
if (!disposedObjects.Add(objectToDispose))
6737+
{
6738+
continue;
6739+
}
6740+
else
6741+
{
6742+
((IDisposable)objectToDispose).Dispose();
6743+
}
6744+
}
6745+
}
6746+
}
6747+
return default;
6748+
6749+
static async ValueTask Await(int i, ValueTask vt, List<object> toDispose, HashSet<object> disposedObjects)
6750+
{
6751+
await vt.ConfigureAwait(false);
6752+
6753+
// vt is acting on the disposable at index i,
6754+
// decrement it and move to the next iteration
6755+
i--;
6756+
6757+
for (; i >= 0; i--)
6758+
{
6759+
object objectToDispose = toDispose[i];
6760+
if (!disposedObjects.Add(objectToDispose))
6761+
{
6762+
continue;
6763+
}
6764+
if (objectToDispose is IAsyncDisposable asyncDisposable)
6765+
{
6766+
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
6767+
}
6768+
else
6769+
{
6770+
((IDisposable)objectToDispose).Dispose();
6771+
}
6772+
}
6773+
}
6774+
}
6775+
67016776
/// <inheritdoc/>
67026777
public Scope BeginScope() => serviceFactory.BeginScope();
67036778

@@ -6747,9 +6822,9 @@ internal object GetScopedInstance(GetInstanceDelegate getInstanceDelegate, objec
67476822
if (createdInstance == null)
67486823
{
67496824
createdInstance = getInstanceDelegate(arguments, this);
6750-
if (createdInstance is IDisposable disposable)
6825+
if (createdInstance is IDisposable || createdInstance is IAsyncDisposable)
67516826
{
6752-
TrackInstance(disposable);
6827+
TrackInstance(createdInstance);
67536828
}
67546829

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

83858460
public static object ValidateTrackedTransient(object instance, Scope scope)
83868461
{
8387-
if (instance is IDisposable disposable)
8462+
if (instance is IDisposable || instance is IAsyncDisposable)
83888463
{
83898464
if (scope == null)
83908465
{
@@ -8394,7 +8469,7 @@ public static object ValidateTrackedTransient(object instance, Scope scope)
83948469
throw new InvalidOperationException(message);
83958470
}
83968471

8397-
scope.TrackInstance(disposable);
8472+
scope.TrackInstance(instance);
83988473
}
83998474

84008475
return instance;

0 commit comments

Comments
 (0)