Skip to content

Commit a38846c

Browse files
authored
Add DisposeAsync support to generic host (#1010)
1 parent 56b843e commit a38846c

File tree

9 files changed

+190
-8
lines changed

9 files changed

+190
-8
lines changed

src/DependencyInjection/DI/src/ServiceLookup/ServiceProviderEngineScope.cs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@
1010
namespace Microsoft.Extensions.DependencyInjection.ServiceLookup
1111
{
1212
internal class ServiceProviderEngineScope : IServiceScope, IServiceProvider
13+
#if DISPOSE_ASYNC
14+
, IAsyncDisposable
15+
#endif
1316
{
1417
// For testing only
1518
internal Action<object> _captureDisposableCallback;

src/DependencyInjection/DI/test/ServiceProviderContainerTests.AsyncDisposable.cs

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,67 @@ public void ProviderDisposeThrowsWhenOnlyDisposeAsyncImplemented()
6868
exception.Message);
6969
}
7070

71+
[Fact]
72+
public async Task ProviderScopeDisposeAsyncCallsDisposeAsyncOnServices()
73+
{
74+
var serviceCollection = new ServiceCollection();
75+
serviceCollection.AddTransient<AsyncDisposable>();
76+
77+
var serviceProvider = CreateServiceProvider(serviceCollection);
78+
var scope = serviceProvider.CreateScope();
79+
var disposable = scope.ServiceProvider.GetService<AsyncDisposable>();
80+
81+
await (scope as IAsyncDisposable).DisposeAsync();
82+
83+
Assert.True(disposable.DisposeAsyncCalled);
84+
}
85+
86+
[Fact]
87+
public async Task ProviderScopeDisposeAsyncPrefersDisposeAsyncOnServices()
88+
{
89+
var serviceCollection = new ServiceCollection();
90+
serviceCollection.AddTransient<SyncAsyncDisposable>();
91+
92+
var serviceProvider = CreateServiceProvider(serviceCollection);
93+
var scope = serviceProvider.CreateScope();
94+
var disposable = scope.ServiceProvider.GetService<SyncAsyncDisposable>();
95+
96+
await (scope as IAsyncDisposable).DisposeAsync();
97+
98+
Assert.True(disposable.DisposeAsyncCalled);
99+
}
100+
101+
[Fact]
102+
public void ProviderScopeDisposePrefersServiceDispose()
103+
{
104+
var serviceCollection = new ServiceCollection();
105+
serviceCollection.AddTransient<SyncAsyncDisposable>();
106+
107+
var serviceProvider = CreateServiceProvider(serviceCollection);
108+
var scope = serviceProvider.CreateScope();
109+
var disposable = scope.ServiceProvider.GetService<SyncAsyncDisposable>();
110+
111+
(scope as IDisposable).Dispose();
112+
113+
Assert.True(disposable.DisposeCalled);
114+
}
115+
116+
[Fact]
117+
public void ProviderScopeDisposeThrowsWhenOnlyDisposeAsyncImplemented()
118+
{
119+
var serviceCollection = new ServiceCollection();
120+
serviceCollection.AddTransient<AsyncDisposable>();
121+
122+
var serviceProvider = CreateServiceProvider(serviceCollection);
123+
var scope = serviceProvider.CreateScope();
124+
var disposable = scope.ServiceProvider.GetService<AsyncDisposable>();
125+
126+
var exception = Assert.Throws<InvalidOperationException>(() => (scope as IDisposable).Dispose());
127+
Assert.Equal(
128+
"'Microsoft.Extensions.DependencyInjection.Tests.ServiceProviderContainerTests+AsyncDisposable' type only implements IAsyncDisposable. Use DisposeAsync to dispose the container.",
129+
exception.Message);
130+
}
131+
71132
private class AsyncDisposable: IFakeService, IAsyncDisposable
72133
{
73134
public bool DisposeAsyncCalled { get; private set; }

src/Hosting/Abstractions/src/HostingAbstractionsHostExtensions.cs

Lines changed: 16 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,12 +56,26 @@ public static void Run(this IHost host)
5656
/// <param name="token">The token to trigger shutdown.</param>
5757
public static async Task RunAsync(this IHost host, CancellationToken token = default)
5858
{
59-
using (host)
59+
try
6060
{
6161
await host.StartAsync(token);
6262

6363
await host.WaitForShutdownAsync(token);
6464
}
65+
finally
66+
{
67+
#if DISPOSE_ASYNC
68+
if (host is IAsyncDisposable asyncDisposable)
69+
{
70+
await asyncDisposable.DisposeAsync();
71+
}
72+
else
73+
#endif
74+
{
75+
host.Dispose();
76+
}
77+
78+
}
6579
}
6680

6781
/// <summary>
@@ -92,4 +106,4 @@ public static async Task WaitForShutdownAsync(this IHost host, CancellationToken
92106
await host.StopAsync();
93107
}
94108
}
95-
}
109+
}

src/Hosting/Abstractions/src/Microsoft.Extensions.Hosting.Abstractions.csproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,12 @@
22

33
<PropertyGroup>
44
<Description>.NET Core hosting and startup abstractions for applications.</Description>
5-
<TargetFramework>netstandard2.0</TargetFramework>
5+
<TargetFrameworks>netstandard2.0;netcoreapp3.0</TargetFrameworks>
66
<NoWarn>$(NoWarn);CS1591</NoWarn>
77
<GenerateDocumentationFile>true</GenerateDocumentationFile>
88
<PackageTags>hosting</PackageTags>
99
<RootNamespace>Microsoft.Extensions.Hosting</RootNamespace>
10-
<IsShipping>true</IsShipping>
10+
<DefineConstants Condition="'$(TargetFramework)' == 'netcoreapp3.0'">$(DefineConstants);DISPOSE_ASYNC</DefineConstants>
1111
</PropertyGroup>
1212

1313
<ItemGroup>

src/Hosting/Hosting/src/Internal/Host.cs

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
// Copyright (c) .NET Foundation. All rights reserved.
1+
// Copyright (c) .NET Foundation. All rights reserved.
22
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
33

44
using System;
@@ -13,6 +13,9 @@
1313
namespace Microsoft.Extensions.Hosting.Internal
1414
{
1515
internal class Host : IHost
16+
#if DISPOSE_ASYNC
17+
, IAsyncDisposable
18+
#endif
1619
{
1720
private readonly ILogger<Host> _logger;
1821
private readonly IHostLifetime _hostLifetime;
@@ -98,9 +101,29 @@ public async Task StopAsync(CancellationToken cancellationToken = default)
98101
_logger.Stopped();
99102
}
100103

104+
#if DISPOSE_ASYNC
105+
public void Dispose()
106+
{
107+
DisposeAsync().GetAwaiter().GetResult();
108+
}
109+
110+
public async ValueTask DisposeAsync()
111+
{
112+
switch (Services)
113+
{
114+
case IAsyncDisposable asyncDisposable:
115+
await asyncDisposable.DisposeAsync();
116+
break;
117+
case IDisposable disposable:
118+
disposable.Dispose();
119+
break;
120+
}
121+
}
122+
#else
101123
public void Dispose()
102124
{
103125
(Services as IDisposable)?.Dispose();
104126
}
127+
#endif
105128
}
106129
}

src/Hosting/Hosting/src/Microsoft.Extensions.Hosting.csproj

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22

33
<PropertyGroup>
44
<Description>.NET Core hosting and startup infrastructures for applications.</Description>
5-
<TargetFramework>netstandard2.0</TargetFramework>
5+
<TargetFrameworks>netstandard2.0;netcoreapp3.0</TargetFrameworks>
66
<NoWarn>$(NoWarn);CS1591</NoWarn>
77
<GenerateDocumentationFile>true</GenerateDocumentationFile>
88
<PackageTags>hosting</PackageTags>
99
<IsShipping>true</IsShipping>
10+
<DefineConstants Condition="'$(TargetFramework)' == 'netcoreapp3.0'">$(DefineConstants);DISPOSE_ASYNC</DefineConstants>
1011
</PropertyGroup>
1112

1213
<ItemGroup>

src/Hosting/Hosting/test/HostTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414

1515
namespace Microsoft.Extensions.Hosting
1616
{
17-
public class HostTests
17+
public partial class HostTests
1818
{
1919
[Fact]
2020
public void CreateDefaultBuilder_IncludesContentRootByDefault()
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
// Copyright (c) .NET Foundation. All rights reserved.
2+
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
3+
4+
#if NETCOREAPP
5+
6+
using System;
7+
using System.Collections.Generic;
8+
using System.Threading.Tasks;
9+
using Microsoft.Extensions.DependencyInjection;
10+
using Xunit;
11+
12+
namespace Microsoft.Extensions.Hosting.Internal
13+
{
14+
public partial class HostTests
15+
{
16+
[Fact]
17+
public async Task HostCallsDisposeAsyncOnServiceProvider()
18+
{
19+
using (var host = CreateBuilder()
20+
.ConfigureServices((hostContext, services) =>
21+
{
22+
services.AddSingleton<AsyncDisposableService>();
23+
})
24+
.Build())
25+
{
26+
await host.StartAsync();
27+
28+
var asyncDisposableService = host.Services.GetService<AsyncDisposableService>();
29+
30+
Assert.False(asyncDisposableService.DisposeAsyncCalled);
31+
32+
await host.StopAsync();
33+
34+
Assert.False(asyncDisposableService.DisposeAsyncCalled);
35+
36+
host.Dispose();
37+
38+
Assert.True(asyncDisposableService.DisposeAsyncCalled);
39+
}
40+
}
41+
42+
[Fact]
43+
public async Task HostCallsDisposeAsyncOnServiceProviderWhenDisposeAsyncCalled()
44+
{
45+
using (var host = CreateBuilder()
46+
.ConfigureServices((hostContext, services) =>
47+
{
48+
services.AddSingleton<AsyncDisposableService>();
49+
})
50+
.Build())
51+
{
52+
await host.StartAsync();
53+
54+
var asyncDisposableService = host.Services.GetService<AsyncDisposableService>();
55+
56+
Assert.False(asyncDisposableService.DisposeAsyncCalled);
57+
58+
await host.StopAsync();
59+
60+
Assert.False(asyncDisposableService.DisposeAsyncCalled);
61+
62+
await ((IAsyncDisposable)host).DisposeAsync();
63+
64+
Assert.True(asyncDisposableService.DisposeAsyncCalled);
65+
}
66+
}
67+
68+
private class AsyncDisposableService: IAsyncDisposable
69+
{
70+
public bool DisposeAsyncCalled { get; set; }
71+
72+
public ValueTask DisposeAsync()
73+
{
74+
DisposeAsyncCalled = true;
75+
return default;
76+
}
77+
}
78+
}
79+
}
80+
#endif

src/Hosting/Hosting/test/Internal/HostTests.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616

1717
namespace Microsoft.Extensions.Hosting.Internal
1818
{
19-
public class HostTests
19+
public partial class HostTests
2020
{
2121
[Fact]
2222
public async Task HostInjectsHostingEnvironment()

0 commit comments

Comments
 (0)