Add DisposeAsync support to WebHost and RequestServices (#7091)
This commit is contained in:
parent
6a46f48eb0
commit
b21c09665e
|
|
@ -24,7 +24,7 @@ using Microsoft.Extensions.StackTrace.Sources;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Hosting.Internal
|
namespace Microsoft.AspNetCore.Hosting.Internal
|
||||||
{
|
{
|
||||||
internal class WebHost : IWebHost
|
internal class WebHost : IWebHost, IAsyncDisposable
|
||||||
{
|
{
|
||||||
private static readonly string DeprecatedServerUrlsKey = "server.urls";
|
private static readonly string DeprecatedServerUrlsKey = "server.urls";
|
||||||
|
|
||||||
|
|
@ -342,12 +342,17 @@ namespace Microsoft.AspNetCore.Hosting.Internal
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
|
{
|
||||||
|
DisposeAsync().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
if (!_stopped)
|
if (!_stopped)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
StopAsync().GetAwaiter().GetResult();
|
await StopAsync().ConfigureAwait(false);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
|
|
@ -355,8 +360,21 @@ namespace Microsoft.AspNetCore.Hosting.Internal
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
(_applicationServices as IDisposable)?.Dispose();
|
await DisposeServiceProviderAsync(_applicationServices).ConfigureAwait(false);
|
||||||
(_hostingServiceProvider as IDisposable)?.Dispose();
|
await DisposeServiceProviderAsync(_hostingServiceProvider).ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async ValueTask DisposeServiceProviderAsync(IServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
switch (serviceProvider)
|
||||||
|
{
|
||||||
|
case IAsyncDisposable asyncDisposable:
|
||||||
|
await asyncDisposable.DisposeAsync();
|
||||||
|
break;
|
||||||
|
case IDisposable disposable:
|
||||||
|
disposable.Dispose();
|
||||||
|
break;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -101,7 +101,7 @@ namespace Microsoft.AspNetCore.Hosting
|
||||||
|
|
||||||
private static async Task RunAsync(this IWebHost host, CancellationToken token, string shutdownMessage)
|
private static async Task RunAsync(this IWebHost host, CancellationToken token, string shutdownMessage)
|
||||||
{
|
{
|
||||||
using (host)
|
try
|
||||||
{
|
{
|
||||||
await host.StartAsync(token);
|
await host.StartAsync(token);
|
||||||
|
|
||||||
|
|
@ -131,6 +131,17 @@ namespace Microsoft.AspNetCore.Hosting
|
||||||
|
|
||||||
await host.WaitForTokenShutdownAsync(token);
|
await host.WaitForTokenShutdownAsync(token);
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (host is IAsyncDisposable asyncDisposable)
|
||||||
|
{
|
||||||
|
await asyncDisposable.DisposeAsync().ConfigureAwait(false);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
host.Dispose();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static async Task WaitForTokenShutdownAsync(this IWebHost host, CancellationToken token)
|
private static async Task WaitForTokenShutdownAsync(this IWebHost host, CancellationToken token)
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,93 @@
|
||||||
|
// Copyright (c) .NET Foundation. All rights reserved.
|
||||||
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Threading.Tasks;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
using Xunit;
|
||||||
|
|
||||||
|
namespace Microsoft.AspNetCore.Hosting
|
||||||
|
{
|
||||||
|
public partial class WebHostTests
|
||||||
|
{
|
||||||
|
[Fact]
|
||||||
|
public async Task DisposingHostCallsDisposeAsyncOnProvider()
|
||||||
|
{
|
||||||
|
var providerFactory = new AsyncServiceProviderFactory();
|
||||||
|
using (var host = CreateBuilder()
|
||||||
|
.UseFakeServer()
|
||||||
|
.ConfigureServices((context, services) =>
|
||||||
|
services.Add(ServiceDescriptor.Singleton<IServiceProviderFactory<IServiceCollection>>(providerFactory)
|
||||||
|
))
|
||||||
|
.UseStartup("Microsoft.AspNetCore.Hosting.Tests")
|
||||||
|
.Build())
|
||||||
|
{
|
||||||
|
await host.StartAsync();
|
||||||
|
|
||||||
|
Assert.Equal(2, providerFactory.Providers.Count);
|
||||||
|
|
||||||
|
await host.StopAsync();
|
||||||
|
|
||||||
|
Assert.All(providerFactory.Providers, provider => {
|
||||||
|
Assert.False(provider.DisposeCalled);
|
||||||
|
Assert.False(provider.DisposeAsyncCalled);
|
||||||
|
});
|
||||||
|
|
||||||
|
host.Dispose();
|
||||||
|
|
||||||
|
Assert.All(providerFactory.Providers, provider => {
|
||||||
|
Assert.False(provider.DisposeCalled);
|
||||||
|
Assert.True(provider.DisposeAsyncCalled);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AsyncServiceProviderFactory : IServiceProviderFactory<IServiceCollection>
|
||||||
|
{
|
||||||
|
public List<AsyncDisposableServiceProvider> Providers { get; } = new List<AsyncDisposableServiceProvider>();
|
||||||
|
|
||||||
|
public IServiceCollection CreateBuilder(IServiceCollection services)
|
||||||
|
{
|
||||||
|
return services;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IServiceProvider CreateServiceProvider(IServiceCollection containerBuilder)
|
||||||
|
{
|
||||||
|
var provider = new AsyncDisposableServiceProvider(containerBuilder.BuildServiceProvider());
|
||||||
|
Providers.Add(provider);
|
||||||
|
return provider;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class AsyncDisposableServiceProvider : IServiceProvider, IDisposable, IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly ServiceProvider _serviceProvider;
|
||||||
|
|
||||||
|
public AsyncDisposableServiceProvider(ServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DisposeCalled { get; set; }
|
||||||
|
|
||||||
|
public bool DisposeAsyncCalled { get; set; }
|
||||||
|
|
||||||
|
public object GetService(Type serviceType) => _serviceProvider.GetService(serviceType);
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
DisposeCalled = true;
|
||||||
|
_serviceProvider.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
DisposeAsyncCalled = true;
|
||||||
|
_serviceProvider.Dispose();
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
@ -25,7 +25,7 @@ using Xunit;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Hosting
|
namespace Microsoft.AspNetCore.Hosting
|
||||||
{
|
{
|
||||||
public class WebHostTests
|
public partial class WebHostTests
|
||||||
{
|
{
|
||||||
[Fact]
|
[Fact]
|
||||||
public async Task WebHostThrowsWithNoServer()
|
public async Task WebHostThrowsWithNoServer()
|
||||||
|
|
@ -1325,4 +1325,4 @@ namespace Microsoft.AspNetCore.Hosting
|
||||||
return builder.ConfigureServices(services => services.AddSingleton<IServer, WebHostTests.FakeServer>());
|
return builder.ConfigureServices(services => services.AddSingleton<IServer, WebHostTests.FakeServer>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -22,6 +22,8 @@ namespace Microsoft.AspNetCore.Http
|
||||||
return Task.CompletedTask;
|
return Task.CompletedTask;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
private static readonly Func<object, Task> _disposeAsyncDelegate = disposable => ((IAsyncDisposable)disposable).DisposeAsync().AsTask();
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Gets the <see cref="HttpContext"/> for this response.
|
/// Gets the <see cref="HttpContext"/> for this response.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
@ -93,6 +95,12 @@ namespace Microsoft.AspNetCore.Http
|
||||||
/// <param name="disposable">The object to be disposed.</param>
|
/// <param name="disposable">The object to be disposed.</param>
|
||||||
public virtual void RegisterForDispose(IDisposable disposable) => OnCompleted(_disposeDelegate, disposable);
|
public virtual void RegisterForDispose(IDisposable disposable) => OnCompleted(_disposeDelegate, disposable);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Registers an object for asynchronous disposal by the host once the request has finished processing.
|
||||||
|
/// </summary>
|
||||||
|
/// <param name="disposable">The object to be disposed asynchronously.</param>
|
||||||
|
public virtual void RegisterForDisposeAsync(IAsyncDisposable disposable) => OnCompleted(_disposeAsyncDelegate, disposable);
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Adds a delegate to be invoked after the response has finished being sent to the client.
|
/// Adds a delegate to be invoked after the response has finished being sent to the client.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|
|
||||||
|
|
@ -2,17 +2,18 @@
|
||||||
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Threading.Tasks;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace Microsoft.AspNetCore.Http.Features
|
namespace Microsoft.AspNetCore.Http.Features
|
||||||
{
|
{
|
||||||
public class RequestServicesFeature : IServiceProvidersFeature, IDisposable
|
public class RequestServicesFeature : IServiceProvidersFeature, IDisposable, IAsyncDisposable
|
||||||
{
|
{
|
||||||
private readonly IServiceScopeFactory _scopeFactory;
|
private readonly IServiceScopeFactory _scopeFactory;
|
||||||
private IServiceProvider _requestServices;
|
private IServiceProvider _requestServices;
|
||||||
private IServiceScope _scope;
|
private IServiceScope _scope;
|
||||||
private bool _requestServicesSet;
|
private bool _requestServicesSet;
|
||||||
private HttpContext _context;
|
private readonly HttpContext _context;
|
||||||
|
|
||||||
public RequestServicesFeature(HttpContext context, IServiceScopeFactory scopeFactory)
|
public RequestServicesFeature(HttpContext context, IServiceScopeFactory scopeFactory)
|
||||||
{
|
{
|
||||||
|
|
@ -26,7 +27,7 @@ namespace Microsoft.AspNetCore.Http.Features
|
||||||
{
|
{
|
||||||
if (!_requestServicesSet && _scopeFactory != null)
|
if (!_requestServicesSet && _scopeFactory != null)
|
||||||
{
|
{
|
||||||
_context.Response.RegisterForDispose(this);
|
_context.Response.RegisterForDisposeAsync(this);
|
||||||
_scope = _scopeFactory.CreateScope();
|
_scope = _scopeFactory.CreateScope();
|
||||||
_requestServices = _scope.ServiceProvider;
|
_requestServices = _scope.ServiceProvider;
|
||||||
_requestServicesSet = true;
|
_requestServicesSet = true;
|
||||||
|
|
@ -41,11 +42,25 @@ namespace Microsoft.AspNetCore.Http.Features
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Dispose()
|
public async ValueTask DisposeAsync()
|
||||||
{
|
{
|
||||||
_scope?.Dispose();
|
switch (_scope)
|
||||||
|
{
|
||||||
|
case IAsyncDisposable asyncDisposable:
|
||||||
|
await asyncDisposable.DisposeAsync();
|
||||||
|
break;
|
||||||
|
case IDisposable disposable:
|
||||||
|
disposable.Dispose();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
_scope = null;
|
_scope = null;
|
||||||
_requestServices = null;
|
_requestServices = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
DisposeAsync().ConfigureAwait(false).GetAwaiter().GetResult();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -199,7 +199,7 @@ namespace Microsoft.AspNetCore.Http
|
||||||
.BuildServiceProvider();
|
.BuildServiceProvider();
|
||||||
|
|
||||||
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
||||||
|
|
||||||
var context = new DefaultHttpContext();
|
var context = new DefaultHttpContext();
|
||||||
context.ServiceScopeFactory = scopeFactory;
|
context.ServiceScopeFactory = scopeFactory;
|
||||||
context.RequestServices = serviceProvider;
|
context.RequestServices = serviceProvider;
|
||||||
|
|
@ -211,8 +211,8 @@ namespace Microsoft.AspNetCore.Http
|
||||||
public async Task RequestServicesAreDisposedOnCompleted()
|
public async Task RequestServicesAreDisposedOnCompleted()
|
||||||
{
|
{
|
||||||
var serviceProvider = new ServiceCollection()
|
var serviceProvider = new ServiceCollection()
|
||||||
.AddTransient<DisposableThing>()
|
.AddTransient<DisposableThing>()
|
||||||
.BuildServiceProvider();
|
.BuildServiceProvider();
|
||||||
|
|
||||||
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
||||||
DisposableThing instance = null;
|
DisposableThing instance = null;
|
||||||
|
|
@ -234,6 +234,36 @@ namespace Microsoft.AspNetCore.Http
|
||||||
Assert.True(instance.Disposed);
|
Assert.True(instance.Disposed);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[Fact]
|
||||||
|
public async Task RequestServicesAreDisposedAsynOnCompleted()
|
||||||
|
{
|
||||||
|
var serviceProvider = new AsyncDisposableServiceProvider(new ServiceCollection()
|
||||||
|
.AddTransient<DisposableThing>()
|
||||||
|
.BuildServiceProvider());
|
||||||
|
|
||||||
|
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
|
||||||
|
DisposableThing instance = null;
|
||||||
|
|
||||||
|
var context = new DefaultHttpContext();
|
||||||
|
context.ServiceScopeFactory = scopeFactory;
|
||||||
|
var responseFeature = new TestHttpResponseFeature();
|
||||||
|
context.Features.Set<IHttpResponseFeature>(responseFeature);
|
||||||
|
|
||||||
|
Assert.NotNull(context.RequestServices);
|
||||||
|
Assert.Single(responseFeature.CompletedCallbacks);
|
||||||
|
|
||||||
|
instance = context.RequestServices.GetRequiredService<DisposableThing>();
|
||||||
|
|
||||||
|
var callback = responseFeature.CompletedCallbacks[0];
|
||||||
|
await callback.callback(callback.state);
|
||||||
|
|
||||||
|
Assert.Null(context.RequestServices);
|
||||||
|
Assert.True(instance.Disposed);
|
||||||
|
var scope = Assert.Single(serviceProvider.Scopes);
|
||||||
|
Assert.True(scope.DisposeAsyncCalled);
|
||||||
|
Assert.False(scope.DisposeCalled);
|
||||||
|
}
|
||||||
|
|
||||||
void TestAllCachedFeaturesAreNull(HttpContext context, IFeatureCollection features)
|
void TestAllCachedFeaturesAreNull(HttpContext context, IFeatureCollection features)
|
||||||
{
|
{
|
||||||
TestCachedFeaturesAreNull(context, features);
|
TestCachedFeaturesAreNull(context, features);
|
||||||
|
|
@ -427,5 +457,68 @@ namespace Microsoft.AspNetCore.Http
|
||||||
throw new NotImplementedException();
|
throw new NotImplementedException();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private class AsyncDisposableServiceProvider : IServiceProvider, IDisposable, IServiceScopeFactory
|
||||||
|
{
|
||||||
|
private readonly ServiceProvider _serviceProvider;
|
||||||
|
|
||||||
|
public AsyncDisposableServiceProvider(ServiceProvider serviceProvider)
|
||||||
|
{
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
|
}
|
||||||
|
|
||||||
|
public List<AsyncServiceScope> Scopes { get; } = new List<AsyncServiceScope>();
|
||||||
|
|
||||||
|
public object GetService(Type serviceType)
|
||||||
|
{
|
||||||
|
if (serviceType == typeof(IServiceScopeFactory))
|
||||||
|
{
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _serviceProvider.GetService(serviceType);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
_serviceProvider.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public IServiceScope CreateScope()
|
||||||
|
{
|
||||||
|
var scope = new AsyncServiceScope(_serviceProvider.GetService<IServiceScopeFactory>().CreateScope());
|
||||||
|
Scopes.Add(scope);
|
||||||
|
return scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal class AsyncServiceScope : IServiceScope, IAsyncDisposable
|
||||||
|
{
|
||||||
|
private readonly IServiceScope _scope;
|
||||||
|
|
||||||
|
public AsyncServiceScope(IServiceScope scope)
|
||||||
|
{
|
||||||
|
_scope = scope;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool DisposeCalled { get; set; }
|
||||||
|
|
||||||
|
public bool DisposeAsyncCalled { get; set; }
|
||||||
|
|
||||||
|
public void Dispose()
|
||||||
|
{
|
||||||
|
DisposeCalled = true;
|
||||||
|
_scope.Dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
public ValueTask DisposeAsync()
|
||||||
|
{
|
||||||
|
DisposeAsyncCalled = true;
|
||||||
|
_scope.Dispose();
|
||||||
|
return default;
|
||||||
|
}
|
||||||
|
|
||||||
|
public IServiceProvider ServiceProvider => _scope.ServiceProvider;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue