From 3933a1904e68e338b0f69c8cb6be5971cb4e29a4 Mon Sep 17 00:00:00 2001 From: John Luo Date: Tue, 20 Oct 2015 15:43:01 -0700 Subject: [PATCH] Refactoring IServerFactory #395 --- .../IServer.cs | 26 +++++++ .../IServerFactory.cs | 14 ++-- .../project.json | 4 +- .../Internal/HostingEngine.cs | 36 +++++----- .../WebHostBuilder.cs | 22 +++++- .../ClientHandler.cs | 30 ++++++-- src/Microsoft.AspNet.TestHost/TestServer.cs | 39 +++++----- .../WebSocketClient.cs | 28 +++++--- .../HostingEngineTests.cs | 64 +++++++++++++---- .../WebHostBuilderTests.cs | 71 +++++++++---------- .../ClientHandlerTests.cs | 57 +++++++-------- 11 files changed, 240 insertions(+), 151 deletions(-) create mode 100644 src/Microsoft.AspNet.Hosting.Server.Abstractions/IServer.cs diff --git a/src/Microsoft.AspNet.Hosting.Server.Abstractions/IServer.cs b/src/Microsoft.AspNet.Hosting.Server.Abstractions/IServer.cs new file mode 100644 index 0000000000..c6909990d0 --- /dev/null +++ b/src/Microsoft.AspNet.Hosting.Server.Abstractions/IServer.cs @@ -0,0 +1,26 @@ +// 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 Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Features; + +namespace Microsoft.AspNet.Hosting.Server +{ + /// + /// Represents a server. + /// + public interface IServer : IDisposable + { + /// + /// A collection of HTTP features of the server. + /// + IFeatureCollection Features { get; } + + /// + /// Start the server with the given function that processes an HTTP request. + /// + /// A function that processes an HTTP request. + void Start(RequestDelegate requestDelegate); + } +} diff --git a/src/Microsoft.AspNet.Hosting.Server.Abstractions/IServerFactory.cs b/src/Microsoft.AspNet.Hosting.Server.Abstractions/IServerFactory.cs index c685ff8570..163d39f1c0 100644 --- a/src/Microsoft.AspNet.Hosting.Server.Abstractions/IServerFactory.cs +++ b/src/Microsoft.AspNet.Hosting.Server.Abstractions/IServerFactory.cs @@ -1,16 +1,20 @@ // 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.Threading.Tasks; -using Microsoft.AspNet.Http.Features; using Microsoft.Extensions.Configuration; namespace Microsoft.AspNet.Hosting.Server { + /// + /// Represents a factory for creating servers. + /// public interface IServerFactory { - IFeatureCollection Initialize(IConfiguration configuration); - IDisposable Start(IFeatureCollection serverFeatures, Func application); + /// + /// Creates based on the given configuration. + /// + /// An instance of . + /// The created server. + IServer CreateServer(IConfiguration configuration); } } diff --git a/src/Microsoft.AspNet.Hosting.Server.Abstractions/project.json b/src/Microsoft.AspNet.Hosting.Server.Abstractions/project.json index a3ccdf94bb..315af28c3f 100644 --- a/src/Microsoft.AspNet.Hosting.Server.Abstractions/project.json +++ b/src/Microsoft.AspNet.Hosting.Server.Abstractions/project.json @@ -6,11 +6,11 @@ "url": "git://github.com/aspnet/hosting" }, "dependencies": { - "Microsoft.AspNet.Http.Features": "1.0.0-*", + "Microsoft.AspNet.Http.Abstractions": "1.0.0-*", "Microsoft.Extensions.Configuration.Abstractions": "1.0.0-*" }, "frameworks": { "net451": {}, "dotnet5.4": {} } -} \ No newline at end of file +} diff --git a/src/Microsoft.AspNet.Hosting/Internal/HostingEngine.cs b/src/Microsoft.AspNet.Hosting/Internal/HostingEngine.cs index 55e2cd589c..c504e6d0e3 100644 --- a/src/Microsoft.AspNet.Hosting/Internal/HostingEngine.cs +++ b/src/Microsoft.AspNet.Hosting/Internal/HostingEngine.cs @@ -42,7 +42,7 @@ namespace Microsoft.AspNet.Hosting.Internal // Only one of these should be set internal IServerFactory ServerFactory { get; set; } internal string ServerFactoryLocation { get; set; } - private IFeatureCollection _serverFeatures; + internal IServer Server { get; set; } public HostingEngine( IServiceCollection appServices, @@ -87,15 +87,13 @@ namespace Microsoft.AspNet.Hosting.Internal var application = BuildApplication(); var logger = _applicationServices.GetRequiredService>(); - var contextFactory = _applicationServices.GetRequiredService(); var diagnosticSource = _applicationServices.GetRequiredService(); logger.Starting(); - var server = ServerFactory.Start(_serverFeatures, - async features => + Server.Start( + async httpContext => { - var httpContext = contextFactory.Create(features); httpContext.ApplicationServices = _applicationServices; if (diagnosticSource.IsEnabled("Microsoft.AspNet.Hosting.BeginRequest")) @@ -135,11 +133,11 @@ namespace Microsoft.AspNet.Hosting.Internal _applicationLifetime.NotifyStarted(); logger.Started(); - return new Application(ApplicationServices, _serverFeatures, new Disposable(() => + return new Application(ApplicationServices, Server.Features, new Disposable(() => { logger.Shutdown(); _applicationLifetime.StopApplication(); - server.Dispose(); + Server.Dispose(); _applicationLifetime.NotifyStopped(); (_applicationServices as IDisposable)?.Dispose(); })); @@ -191,7 +189,7 @@ namespace Microsoft.AspNet.Hosting.Internal EnsureServer(); var builderFactory = _applicationServices.GetRequiredService(); - var builder = builderFactory.CreateBuilder(_serverFeatures); + var builder = builderFactory.CreateBuilder(Server.Features); builder.ApplicationServices = _applicationServices; var startupFilters = _applicationServices.GetService>(); @@ -246,21 +244,21 @@ namespace Microsoft.AspNet.Hosting.Internal private void EnsureServer() { - if (ServerFactory == null) + if (Server == null) { - // Blow up if we don't have a server set at this point - if (ServerFactoryLocation == null) + if (ServerFactory == null) { - throw new InvalidOperationException("IHostingBuilder.UseServer() is required for " + nameof(Start) + "()"); + // Blow up if we don't have a server set at this point + if (ServerFactoryLocation == null) + { + throw new InvalidOperationException("IHostingBuilder.UseServer() is required for " + nameof(Start) + "()"); + } + + ServerFactory = _applicationServices.GetRequiredService().LoadServerFactory(ServerFactoryLocation); } - ServerFactory = _applicationServices.GetRequiredService().LoadServerFactory(ServerFactoryLocation); - } - - if (_serverFeatures == null) - { - _serverFeatures = ServerFactory.Initialize(_config); - var addresses = _serverFeatures?.Get()?.Addresses; + Server = ServerFactory.CreateServer(_config); + var addresses = Server.Features?.Get()?.Addresses; if (addresses != null && !addresses.IsReadOnly) { var port = _config[ServerPort]; diff --git a/src/Microsoft.AspNet.Hosting/WebHostBuilder.cs b/src/Microsoft.AspNet.Hosting/WebHostBuilder.cs index 8d5c5648e6..ddff824f21 100644 --- a/src/Microsoft.AspNet.Hosting/WebHostBuilder.cs +++ b/src/Microsoft.AspNet.Hosting/WebHostBuilder.cs @@ -42,6 +42,7 @@ namespace Microsoft.AspNet.Hosting // Only one of these should be set private string _serverFactoryLocation; private IServerFactory _serverFactory; + private IServer _server; public WebHostBuilder() : this(config: new ConfigurationBuilder().Build()) @@ -133,6 +134,7 @@ namespace Microsoft.AspNet.Hosting var engine = new HostingEngine(hostingServices, startupLoader, _config, _captureStartupErrors); // Only one of these should be set, but they are used in priority + engine.Server = _server; engine.ServerFactory = _serverFactory; engine.ServerFactoryLocation = _config[ServerKey] ?? _config[OldServerKey] ?? _serverFactoryLocation; @@ -161,7 +163,18 @@ namespace Microsoft.AspNet.Hosting return this; } - public WebHostBuilder UseServer(string assemblyName) + public WebHostBuilder UseServer(IServer server) + { + if (server == null) + { + throw new ArgumentNullException(nameof(server)); + } + + _server = server; + return this; + } + + public WebHostBuilder UseServerFactory(string assemblyName) { if (assemblyName == null) { @@ -172,8 +185,13 @@ namespace Microsoft.AspNet.Hosting return this; } - public WebHostBuilder UseServer(IServerFactory factory) + public WebHostBuilder UseServerFactory(IServerFactory factory) { + if (factory == null) + { + throw new ArgumentNullException(nameof(factory)); + } + _serverFactory = factory; return this; } diff --git a/src/Microsoft.AspNet.TestHost/ClientHandler.cs b/src/Microsoft.AspNet.TestHost/ClientHandler.cs index efcb5dc06e..78e3463bc6 100644 --- a/src/Microsoft.AspNet.TestHost/ClientHandler.cs +++ b/src/Microsoft.AspNet.TestHost/ClientHandler.cs @@ -23,21 +23,27 @@ namespace Microsoft.AspNet.TestHost /// public class ClientHandler : HttpMessageHandler { - private readonly Func _next; + private readonly RequestDelegate _next; private readonly PathString _pathBase; + private readonly IHttpContextFactory _factory; /// /// Create a new handler. /// /// The pipeline entry point. - public ClientHandler(Func next, PathString pathBase) + public ClientHandler(RequestDelegate next, PathString pathBase, IHttpContextFactory httpContextFactory) { if (next == null) { throw new ArgumentNullException(nameof(next)); } + if (httpContextFactory == null) + { + throw new ArgumentNullException(nameof(httpContextFactory)); + } _next = next; + _factory = httpContextFactory; // PathString.StartsWithSegments that we use below requires the base path to not end in a slash. if (pathBase.HasValue && pathBase.Value.EndsWith("/")) @@ -63,7 +69,7 @@ namespace Microsoft.AspNet.TestHost throw new ArgumentNullException(nameof(request)); } - var state = new RequestState(request, _pathBase); + var state = new RequestState(request, _pathBase, _factory); var requestContent = request.Content ?? new StreamContent(Stream.Null); var body = await requestContent.ReadAsStreamAsync(); if (body.CanSeek) @@ -79,7 +85,7 @@ namespace Microsoft.AspNet.TestHost { try { - await _next(state.HttpContext.Features); + await _next(state.HttpContext); state.CompleteResponse(); } catch (Exception ex) @@ -88,6 +94,7 @@ namespace Microsoft.AspNet.TestHost } finally { + state.ServerCleanup(); registration.Dispose(); } }); @@ -102,14 +109,16 @@ namespace Microsoft.AspNet.TestHost private ResponseStream _responseStream; private ResponseFeature _responseFeature; private CancellationTokenSource _requestAbortedSource; + private IHttpContextFactory _factory; private bool _pipelineFinished; - internal RequestState(HttpRequestMessage request, PathString pathBase) + internal RequestState(HttpRequestMessage request, PathString pathBase, IHttpContextFactory factory) { _request = request; _responseTcs = new TaskCompletionSource(); _requestAbortedSource = new CancellationTokenSource(); _pipelineFinished = false; + _factory = factory; if (request.RequestUri.IsDefaultPort) { @@ -120,7 +129,8 @@ namespace Microsoft.AspNet.TestHost request.Headers.Host = request.RequestUri.GetComponents(UriComponents.HostAndPort, UriFormat.UriEscaped); } - HttpContext = new DefaultHttpContext(); + HttpContext = _factory.Create(new FeatureCollection()); + HttpContext.Features.Set(new RequestFeature()); _responseFeature = new ResponseFeature(); HttpContext.Features.Set(_responseFeature); @@ -228,6 +238,14 @@ namespace Microsoft.AspNet.TestHost _responseStream.Abort(exception); _responseTcs.TrySetException(exception); } + + internal void ServerCleanup() + { + if (HttpContext != null) + { + _factory.Dispose(HttpContext); + } + } } } } diff --git a/src/Microsoft.AspNet.TestHost/TestServer.cs b/src/Microsoft.AspNet.TestHost/TestServer.cs index 176431060c..cd354c3eba 100644 --- a/src/Microsoft.AspNet.TestHost/TestServer.cs +++ b/src/Microsoft.AspNet.TestHost/TestServer.cs @@ -1,4 +1,4 @@ -// Copyright (c) .NET Foundation. All rights reserved. +// 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; @@ -14,22 +14,26 @@ using Microsoft.Extensions.DependencyInjection; namespace Microsoft.AspNet.TestHost { - public class TestServer : IServerFactory, IDisposable + public class TestServer : IServer { private const string DefaultEnvironmentName = "Development"; private const string ServerName = nameof(TestServer); - private static readonly IFeatureCollection ServerInfo = new FeatureCollection(); - private Func _appDelegate; + private RequestDelegate _appDelegate; private IDisposable _appInstance; private bool _disposed = false; + private IHttpContextFactory _httpContextFactory; public TestServer(WebHostBuilder builder) { - _appInstance = builder.UseServer(this).Build().Start(); + var hostingEngine = builder.UseServer(this).Build(); + _httpContextFactory = hostingEngine.ApplicationServices.GetService(); + _appInstance = hostingEngine.Start(); } public Uri BaseAddress { get; set; } = new Uri("http://localhost/"); + IFeatureCollection IServer.Features { get; } + public static TestServer Create() { return Create(config: null, configureApp: null, configureServices: null); @@ -95,7 +99,7 @@ namespace Microsoft.AspNet.TestHost public HttpMessageHandler CreateHandler() { var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); - return new ClientHandler(Invoke, pathBase); + return new ClientHandler(Invoke, pathBase, _httpContextFactory); } public HttpClient CreateClient() @@ -106,7 +110,7 @@ namespace Microsoft.AspNet.TestHost public WebSocketClient CreateWebSocketClient() { var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); - return new WebSocketClient(Invoke, pathBase); + return new WebSocketClient(Invoke, pathBase, _httpContextFactory); } /// @@ -119,25 +123,13 @@ namespace Microsoft.AspNet.TestHost return new RequestBuilder(this, path); } - public IFeatureCollection Initialize(IConfiguration configuration) - { - return ServerInfo; - } - - public IDisposable Start(IFeatureCollection serverInformation, Func application) - { - _appDelegate = application; - - return this; - } - - public Task Invoke(IFeatureCollection featureCollection) + public Task Invoke(HttpContext context) { if (_disposed) { throw new ObjectDisposedException(GetType().FullName); } - return _appDelegate(featureCollection); + return _appDelegate(context); } public void Dispose() @@ -145,5 +137,10 @@ namespace Microsoft.AspNet.TestHost _disposed = true; _appInstance.Dispose(); } + + void IServer.Start(RequestDelegate requestDelegate) + { + _appDelegate = requestDelegate; + } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.TestHost/WebSocketClient.cs b/src/Microsoft.AspNet.TestHost/WebSocketClient.cs index 9946d5e4bd..320f43afe8 100644 --- a/src/Microsoft.AspNet.TestHost/WebSocketClient.cs +++ b/src/Microsoft.AspNet.TestHost/WebSocketClient.cs @@ -8,25 +8,31 @@ using System.Net.WebSockets; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; -using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Internal; namespace Microsoft.AspNet.TestHost { public class WebSocketClient { - private readonly Func _next; + private readonly RequestDelegate _next; private readonly PathString _pathBase; + private readonly IHttpContextFactory _httpContextFactory; - internal WebSocketClient(Func next, PathString pathBase) + internal WebSocketClient(RequestDelegate next, PathString pathBase, IHttpContextFactory httpContextFactory) { if (next == null) { throw new ArgumentNullException(nameof(next)); } + if (httpContextFactory == null) + { + throw new ArgumentNullException(nameof(httpContextFactory)); + } _next = next; + _httpContextFactory = httpContextFactory; // PathString.StartsWithSegments that we use below requires the base path to not end in a slash. if (pathBase.HasValue && pathBase.Value.EndsWith("/")) @@ -52,7 +58,7 @@ namespace Microsoft.AspNet.TestHost public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken) { - var state = new RequestState(uri, _pathBase, cancellationToken); + var state = new RequestState(uri, _pathBase, cancellationToken, _httpContextFactory); if (ConfigureRequest != null) { @@ -64,7 +70,7 @@ namespace Microsoft.AspNet.TestHost { try { - await _next(state.FeatureCollection); + await _next(state.HttpContext); state.PipelineComplete(); } catch (Exception ex) @@ -84,18 +90,18 @@ namespace Microsoft.AspNet.TestHost { private TaskCompletionSource _clientWebSocketTcs; private WebSocket _serverWebSocket; + private IHttpContextFactory _factory; - public IFeatureCollection FeatureCollection { get; private set; } public HttpContext HttpContext { get; private set; } public Task WebSocketTask { get { return _clientWebSocketTcs.Task; } } - public RequestState(Uri uri, PathString pathBase, CancellationToken cancellationToken) + public RequestState(Uri uri, PathString pathBase, CancellationToken cancellationToken, IHttpContextFactory factory) { + _factory = factory; _clientWebSocketTcs = new TaskCompletionSource(); // HttpContext - FeatureCollection = new FeatureCollection(); - HttpContext = new DefaultHttpContext(FeatureCollection); + HttpContext = _factory.Create(new FeatureCollection()); // Request HttpContext.Features.Set(new RequestFeature()); @@ -147,6 +153,10 @@ namespace Microsoft.AspNet.TestHost public void Dispose() { + if (HttpContext != null) + { + _factory.Dispose(HttpContext); + } if (_serverWebSocket != null) { _serverWebSocket.Dispose(); diff --git a/test/Microsoft.AspNet.Hosting.Tests/HostingEngineTests.cs b/test/Microsoft.AspNet.Hosting.Tests/HostingEngineTests.cs index 928cd5bb35..0e0655ecac 100644 --- a/test/Microsoft.AspNet.Hosting.Tests/HostingEngineTests.cs +++ b/test/Microsoft.AspNet.Hosting.Tests/HostingEngineTests.cs @@ -14,6 +14,7 @@ using Microsoft.AspNet.Hosting.Server; using Microsoft.AspNet.Hosting.Startup; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Features; +using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Server.Features; using Microsoft.AspNet.Testing.xunit; using Microsoft.Extensions.Configuration; @@ -26,10 +27,33 @@ using Xunit; namespace Microsoft.AspNet.Hosting { - public class HostingEngineTests : IServerFactory + public class HostingEngineTests : IServerFactory, IServer { private readonly IList _startInstances = new List(); private IFeatureCollection _featuresSupportedByThisHost = NewFeatureCollection(); + private IFeatureCollection _instanceFeaturesSupportedByThisHost; + + public IFeatureCollection Features { + get + { + var features = new FeatureCollection(); + + foreach (var feature in _featuresSupportedByThisHost) + { + features[feature.Key] = feature.Value; + } + + if (_instanceFeaturesSupportedByThisHost != null) + { + foreach (var feature in _instanceFeaturesSupportedByThisHost) + { + features[feature.Key] = feature.Value; + } + } + + return features; + } + } static IFeatureCollection NewFeatureCollection() { @@ -431,26 +455,36 @@ namespace Microsoft.AspNet.Hosting return new WebHostBuilder(config ?? new ConfigurationBuilder().Build()); } - public IFeatureCollection Initialize(IConfiguration configuration) + public void Start(RequestDelegate requestDelegate) { - var features = new FeatureCollection(); - features.Set(new ServerAddressesFeature()); - return features; - } - - public IDisposable Start(IFeatureCollection serverFeatures, Func application) - { - var startInstance = new StartInstance(application); + var startInstance = new StartInstance(requestDelegate); _startInstances.Add(startInstance); - application(_featuresSupportedByThisHost); - return startInstance; + requestDelegate(new DefaultHttpContext(Features)); } - public class StartInstance : IDisposable + public void Dispose() { - private readonly Func _application; + if (_startInstances != null) + { + foreach (var startInstance in _startInstances) + { + startInstance.Dispose(); + } + } + } - public StartInstance(Func application) + public IServer CreateServer(IConfiguration configuration) + { + _instanceFeaturesSupportedByThisHost = new FeatureCollection(); + _instanceFeaturesSupportedByThisHost.Set(new ServerAddressesFeature()); + return this; + } + + private class StartInstance : IDisposable + { + private readonly RequestDelegate _application; + + public StartInstance(RequestDelegate application) { _application = application; } diff --git a/test/Microsoft.AspNet.Hosting.Tests/WebHostBuilderTests.cs b/test/Microsoft.AspNet.Hosting.Tests/WebHostBuilderTests.cs index 988194f0a3..3ab61daf96 100644 --- a/test/Microsoft.AspNet.Hosting.Tests/WebHostBuilderTests.cs +++ b/test/Microsoft.AspNet.Hosting.Tests/WebHostBuilderTests.cs @@ -1,13 +1,13 @@ // 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.IO; using System.Threading.Tasks; using Microsoft.AspNet.Hosting.Fakes; using Microsoft.AspNet.Hosting.Internal; using Microsoft.AspNet.Hosting.Server; +using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Internal; using Microsoft.Extensions.Configuration; @@ -42,11 +42,11 @@ namespace Microsoft.AspNet.Hosting public async Task StartupMissing_Fallback() { var builder = CreateWebHostBuilder(); - var serverFactory = new TestServerFactory(); - var engine = builder.UseServer(serverFactory).UseStartup("MissingStartupAssembly").Build(); + var server = new TestServer(); + var engine = builder.UseServer(server).UseStartup("MissingStartupAssembly").Build(); using (engine.Start()) { - await AssertResponseContains(serverFactory.Application, "MissingStartupAssembly"); + await AssertResponseContains(server.RequestDelegate, "MissingStartupAssembly"); } } @@ -54,11 +54,11 @@ namespace Microsoft.AspNet.Hosting public async Task StartupStaticCtorThrows_Fallback() { var builder = CreateWebHostBuilder(); - var serverFactory = new TestServerFactory(); - var engine = builder.UseServer(serverFactory).UseStartup().Build(); + var server = new TestServer(); + var engine = builder.UseServer(server).UseStartup().Build(); using (engine.Start()) { - await AssertResponseContains(serverFactory.Application, "Exception from static constructor"); + await AssertResponseContains(server.RequestDelegate, "Exception from static constructor"); } } @@ -66,11 +66,11 @@ namespace Microsoft.AspNet.Hosting public async Task StartupCtorThrows_Fallback() { var builder = CreateWebHostBuilder(); - var serverFactory = new TestServerFactory(); - var engine = builder.UseServer(serverFactory).UseStartup().Build(); + var server = new TestServer(); + var engine = builder.UseServer(server).UseStartup().Build(); using (engine.Start()) { - await AssertResponseContains(serverFactory.Application, "Exception from constructor"); + await AssertResponseContains(server.RequestDelegate, "Exception from constructor"); } } @@ -78,11 +78,11 @@ namespace Microsoft.AspNet.Hosting public async Task StartupCtorThrows_TypeLoadException() { var builder = CreateWebHostBuilder(); - var serverFactory = new TestServerFactory(); - var engine = builder.UseServer(serverFactory).UseStartup().Build(); + var server = new TestServer(); + var engine = builder.UseServer(server).UseStartup().Build(); using (engine.Start()) { - await AssertResponseContains(serverFactory.Application, "Message from the LoaderException"); + await AssertResponseContains(server.RequestDelegate, "Message from the LoaderException"); } } @@ -90,13 +90,13 @@ namespace Microsoft.AspNet.Hosting public async Task IApplicationLifetimeRegisteredEvenWhenStartupCtorThrows_Fallback() { var builder = CreateWebHostBuilder(); - var serverFactory = new TestServerFactory(); - var engine = builder.UseServer(serverFactory).UseStartup().Build(); + var server = new TestServer(); + var engine = builder.UseServer(server).UseStartup().Build(); using (engine.Start()) { - var service = engine.ApplicationServices.GetService(); + var service = engine.ApplicationServices.GetServices(); Assert.NotNull(service); - await AssertResponseContains(serverFactory.Application, "Exception from constructor"); + await AssertResponseContains(server.RequestDelegate, "Exception from constructor"); } } @@ -104,11 +104,11 @@ namespace Microsoft.AspNet.Hosting public async Task StartupConfigureServicesThrows_Fallback() { var builder = CreateWebHostBuilder(); - var serverFactory = new TestServerFactory(); - var engine = builder.UseServer(serverFactory).UseStartup().Build(); + var server = new TestServer(); + var engine = builder.UseServer(server).UseStartup().Build(); using (engine.Start()) { - await AssertResponseContains(serverFactory.Application, "Exception from ConfigureServices"); + await AssertResponseContains(server.RequestDelegate, "Exception from ConfigureServices"); } } @@ -116,11 +116,11 @@ namespace Microsoft.AspNet.Hosting public async Task StartupConfigureThrows_Fallback() { var builder = CreateWebHostBuilder(); - var serverFactory = new TestServerFactory(); - var engine = builder.UseServer(serverFactory).UseStartup().Build(); + var server = new TestServer(); + var engine = builder.UseServer(server).UseStartup().Build(); using (engine.Start()) { - await AssertResponseContains(serverFactory.Application, "Exception from Configure"); + await AssertResponseContains(server.RequestDelegate, "Exception from Configure"); } } @@ -137,36 +137,29 @@ namespace Microsoft.AspNet.Hosting return new WebHostBuilder(config, captureStartupErrors: true); } - private async Task AssertResponseContains(Func app, string expectedText) + private async Task AssertResponseContains(RequestDelegate app, string expectedText) { var httpContext = new DefaultHttpContext(); httpContext.Response.Body = new MemoryStream(); - await app(httpContext.Features); + await app(httpContext); httpContext.Response.Body.Seek(0, SeekOrigin.Begin); var bodyText = new StreamReader(httpContext.Response.Body).ReadToEnd(); Assert.Contains(expectedText, bodyText); } - private class TestServerFactory : IServerFactory + private class TestServer : IServer { - public Func Application { get; set; } + IFeatureCollection IServer.Features { get; } + public RequestDelegate RequestDelegate { get; private set; } - public IFeatureCollection Initialize(IConfiguration configuration) + public void Dispose() { - return new FeatureCollection(); + } - public IDisposable Start(IFeatureCollection serverFeatures, Func application) + public void Start(RequestDelegate requestDelegate) { - Application = application; - return new Disposable(); - } - - private class Disposable : IDisposable - { - public void Dispose() - { - } + RequestDelegate = requestDelegate; } } } diff --git a/test/Microsoft.AspNet.TestHost.Tests/ClientHandlerTests.cs b/test/Microsoft.AspNet.TestHost.Tests/ClientHandlerTests.cs index ec7976d685..adacdd9d8e 100644 --- a/test/Microsoft.AspNet.TestHost.Tests/ClientHandlerTests.cs +++ b/test/Microsoft.AspNet.TestHost.Tests/ClientHandlerTests.cs @@ -17,13 +17,13 @@ namespace Microsoft.AspNet.TestHost { public class ClientHandlerTests { + private IHttpContextFactory _httpContextFactory = new HttpContextFactory(new HttpContextAccessor()); + [Fact] public Task ExpectedKeysAreAvailable() { - var handler = new ClientHandler(env => + var handler = new ClientHandler(context => { - var context = new DefaultHttpContext((IFeatureCollection)env); - // TODO: Assert.True(context.RequestAborted.CanBeCanceled); Assert.Equal("HTTP/1.1", context.Request.Protocol); Assert.Equal("GET", context.Request.Method); @@ -40,7 +40,7 @@ namespace Microsoft.AspNet.TestHost Assert.Equal("example.com", context.Request.Host.Value); return Task.FromResult(0); - }, new PathString("/A/Path/")); + }, new PathString("/A/Path/"), _httpContextFactory); var httpClient = new HttpClient(handler); return httpClient.GetAsync("https://example.com/A/Path/and/file.txt?and=query"); } @@ -48,14 +48,13 @@ namespace Microsoft.AspNet.TestHost [Fact] public Task SingleSlashNotMovedToPathBase() { - var handler = new ClientHandler(env => + var handler = new ClientHandler(context => { - var context = new DefaultHttpContext((IFeatureCollection)env); Assert.Equal("", context.Request.PathBase.Value); Assert.Equal("/", context.Request.Path.Value); return Task.FromResult(0); - }, new PathString("")); + }, new PathString(""), _httpContextFactory); var httpClient = new HttpClient(handler); return httpClient.GetAsync("https://example.com/"); } @@ -64,15 +63,14 @@ namespace Microsoft.AspNet.TestHost public async Task ResubmitRequestWorks() { int requestCount = 1; - var handler = new ClientHandler(env => + var handler = new ClientHandler(context => { - var context = new DefaultHttpContext((IFeatureCollection)env); int read = context.Request.Body.Read(new byte[100], 0, 100); Assert.Equal(11, read); context.Response.Headers["TestHeader"] = "TestValue:" + requestCount++; return Task.FromResult(0); - }, PathString.Empty); + }, PathString.Empty, _httpContextFactory); HttpMessageInvoker invoker = new HttpMessageInvoker(handler); HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, "https://example.com/"); @@ -88,13 +86,11 @@ namespace Microsoft.AspNet.TestHost [Fact] public async Task MiddlewareOnlySetsHeaders() { - var handler = new ClientHandler(env => + var handler = new ClientHandler(context => { - var context = new DefaultHttpContext((IFeatureCollection)env); - context.Response.Headers["TestHeader"] = "TestValue"; return Task.FromResult(0); - }, PathString.Empty); + }, PathString.Empty, _httpContextFactory); var httpClient = new HttpClient(handler); HttpResponseMessage response = await httpClient.GetAsync("https://example.com/"); Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First()); @@ -104,11 +100,11 @@ namespace Microsoft.AspNet.TestHost public async Task BlockingMiddlewareShouldNotBlockClient() { ManualResetEvent block = new ManualResetEvent(false); - var handler = new ClientHandler(env => + var handler = new ClientHandler(context => { block.WaitOne(); return Task.FromResult(0); - }, PathString.Empty); + }, PathString.Empty, _httpContextFactory); var httpClient = new HttpClient(handler); Task task = httpClient.GetAsync("https://example.com/"); Assert.False(task.IsCompleted); @@ -121,14 +117,13 @@ namespace Microsoft.AspNet.TestHost public async Task HeadersAvailableBeforeBodyFinished() { ManualResetEvent block = new ManualResetEvent(false); - var handler = new ClientHandler(async env => + var handler = new ClientHandler(async context => { - var context = new DefaultHttpContext((IFeatureCollection)env); context.Response.Headers["TestHeader"] = "TestValue"; await context.Response.WriteAsync("BodyStarted,"); block.WaitOne(); await context.Response.WriteAsync("BodyFinished"); - }, PathString.Empty); + }, PathString.Empty, _httpContextFactory); var httpClient = new HttpClient(handler); HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", HttpCompletionOption.ResponseHeadersRead); @@ -141,14 +136,13 @@ namespace Microsoft.AspNet.TestHost public async Task FlushSendsHeaders() { ManualResetEvent block = new ManualResetEvent(false); - var handler = new ClientHandler(async env => + var handler = new ClientHandler(async context => { - var context = new DefaultHttpContext((IFeatureCollection)env); context.Response.Headers["TestHeader"] = "TestValue"; context.Response.Body.Flush(); block.WaitOne(); await context.Response.WriteAsync("BodyFinished"); - }, PathString.Empty); + }, PathString.Empty, _httpContextFactory); var httpClient = new HttpClient(handler); HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", HttpCompletionOption.ResponseHeadersRead); @@ -161,14 +155,13 @@ namespace Microsoft.AspNet.TestHost public async Task ClientDisposalCloses() { ManualResetEvent block = new ManualResetEvent(false); - var handler = new ClientHandler(env => + var handler = new ClientHandler(context => { - var context = new DefaultHttpContext((IFeatureCollection)env); context.Response.Headers["TestHeader"] = "TestValue"; context.Response.Body.Flush(); block.WaitOne(); return Task.FromResult(0); - }, PathString.Empty); + }, PathString.Empty, _httpContextFactory); var httpClient = new HttpClient(handler); HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", HttpCompletionOption.ResponseHeadersRead); @@ -187,14 +180,13 @@ namespace Microsoft.AspNet.TestHost public async Task ClientCancellationAborts() { ManualResetEvent block = new ManualResetEvent(false); - var handler = new ClientHandler(env => + var handler = new ClientHandler(context => { - var context = new DefaultHttpContext((IFeatureCollection)env); context.Response.Headers["TestHeader"] = "TestValue"; context.Response.Body.Flush(); block.WaitOne(); return Task.FromResult(0); - }, PathString.Empty); + }, PathString.Empty, _httpContextFactory); var httpClient = new HttpClient(handler); HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", HttpCompletionOption.ResponseHeadersRead); @@ -213,10 +205,10 @@ namespace Microsoft.AspNet.TestHost [Fact] public Task ExceptionBeforeFirstWriteIsReported() { - var handler = new ClientHandler(env => + var handler = new ClientHandler(context => { throw new InvalidOperationException("Test Exception"); - }, PathString.Empty); + }, PathString.Empty, _httpContextFactory); var httpClient = new HttpClient(handler); return Assert.ThrowsAsync(() => httpClient.GetAsync("https://example.com/", HttpCompletionOption.ResponseHeadersRead)); @@ -227,14 +219,13 @@ namespace Microsoft.AspNet.TestHost public async Task ExceptionAfterFirstWriteIsReported() { ManualResetEvent block = new ManualResetEvent(false); - var handler = new ClientHandler(async env => + var handler = new ClientHandler(async context => { - var context = new DefaultHttpContext((IFeatureCollection)env); context.Response.Headers["TestHeader"] = "TestValue"; await context.Response.WriteAsync("BodyStarted"); block.WaitOne(); throw new InvalidOperationException("Test Exception"); - }, PathString.Empty); + }, PathString.Empty, _httpContextFactory); var httpClient = new HttpClient(handler); HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", HttpCompletionOption.ResponseHeadersRead);