From 8c256a0d8718a1adccc2e6af283a232b4b6bcfc5 Mon Sep 17 00:00:00 2001 From: John Luo Date: Mon, 2 Nov 2015 13:55:44 -0800 Subject: [PATCH] New IServer design with IHttpApplication added #395 --- .../IHttpApplication.cs | 35 +++++++ .../IServer.cs | 7 +- .../Internal/HostingApplication.cs | 91 +++++++++++++++++++ .../Internal/HostingEngine.cs | 39 +------- .../Internal/HostingLoggerExtensions.cs | 63 +------------ .../Internal/LoggerEventIds.cs | 9 +- .../WebHostBuilder.cs | 1 - .../ClientHandler.cs | 69 +++++++------- src/Microsoft.AspNet.TestHost/TestServer.cs | 56 ++++++++---- .../WebSocketClient.cs | 63 +++++++------ .../HostingEngineTests.cs | 24 ++--- .../WebHostBuilderTests.cs | 18 +++- .../ClientHandlerTests.cs | 78 +++++++++++----- .../TestServerTests.cs | 9 +- 14 files changed, 332 insertions(+), 230 deletions(-) create mode 100644 src/Microsoft.AspNet.Hosting.Server.Abstractions/IHttpApplication.cs create mode 100644 src/Microsoft.AspNet.Hosting/Internal/HostingApplication.cs diff --git a/src/Microsoft.AspNet.Hosting.Server.Abstractions/IHttpApplication.cs b/src/Microsoft.AspNet.Hosting.Server.Abstractions/IHttpApplication.cs new file mode 100644 index 0000000000..07cbe53f82 --- /dev/null +++ b/src/Microsoft.AspNet.Hosting.Server.Abstractions/IHttpApplication.cs @@ -0,0 +1,35 @@ +// 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; + +namespace Microsoft.AspNet.Hosting.Server +{ + /// + /// Represents an HttpApplication. + /// + public interface IHttpApplication + { + /// + /// Create a TContext given a collection of HTTP features. + /// + /// A collection of HTTP features to be used for creating the TContext. + /// The created TContext. + TContext CreateContext(IFeatureCollection contextFeatures); + + /// + /// Asynchronously processes an TContext. + /// + /// The TContext that the operation will process. + Task ProcessRequestAsync(TContext context); + + /// + /// Dispose a given TContext. + /// + /// The TContext to be disposed. + /// The Exception thrown when processing did not complete successfully, otherwise null. + void DisposeContext(TContext context, Exception exception); + } +} diff --git a/src/Microsoft.AspNet.Hosting.Server.Abstractions/IServer.cs b/src/Microsoft.AspNet.Hosting.Server.Abstractions/IServer.cs index c6909990d0..cc1a88d519 100644 --- a/src/Microsoft.AspNet.Hosting.Server.Abstractions/IServer.cs +++ b/src/Microsoft.AspNet.Hosting.Server.Abstractions/IServer.cs @@ -2,7 +2,6 @@ // 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 @@ -18,9 +17,9 @@ namespace Microsoft.AspNet.Hosting.Server IFeatureCollection Features { get; } /// - /// Start the server with the given function that processes an HTTP request. + /// Start the server with an HttpApplication. /// - /// A function that processes an HTTP request. - void Start(RequestDelegate requestDelegate); + /// An instance of . + void Start(IHttpApplication application); } } diff --git a/src/Microsoft.AspNet.Hosting/Internal/HostingApplication.cs b/src/Microsoft.AspNet.Hosting/Internal/HostingApplication.cs new file mode 100644 index 0000000000..c5b74dca72 --- /dev/null +++ b/src/Microsoft.AspNet.Hosting/Internal/HostingApplication.cs @@ -0,0 +1,91 @@ +// 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.Diagnostics; +using System.Threading.Tasks; +using Microsoft.AspNet.Hosting.Server; +using Microsoft.AspNet.Http; +using Microsoft.AspNet.Http.Features; +using Microsoft.Extensions.Logging; + +namespace Microsoft.AspNet.Hosting.Internal +{ + public class HostingApplication : IHttpApplication + { + private readonly RequestDelegate _application; + private readonly ILogger _logger; + private readonly DiagnosticSource _diagnosticSource; + private readonly IHttpContextFactory _httpContextFactory; + + public HostingApplication( + RequestDelegate application, + ILogger logger, + DiagnosticSource diagnosticSource, + IHttpContextFactory httpContextFactory) + { + _application = application; + _logger = logger; + _diagnosticSource = diagnosticSource; + _httpContextFactory = httpContextFactory; + } + + public Context CreateContext(IFeatureCollection contextFeatures) + { + var httpContext = _httpContextFactory.Create(contextFeatures); + var startTick = Environment.TickCount; + + var scope = _logger.RequestScope(httpContext); + _logger.RequestStarting(httpContext); + if (_diagnosticSource.IsEnabled("Microsoft.AspNet.Hosting.BeginRequest")) + { + _diagnosticSource.Write("Microsoft.AspNet.Hosting.BeginRequest", new { httpContext = httpContext, tickCount = startTick }); + } + + return new Context + { + HttpContext = httpContext, + Scope = scope, + StartTick = startTick, + }; + } + + public void DisposeContext(Context context, Exception exception) + { + var httpContext = context.HttpContext; + var currentTick = Environment.TickCount; + _logger.RequestFinished(httpContext, context.StartTick, currentTick); + + if (exception == null) + { + if (_diagnosticSource.IsEnabled("Microsoft.AspNet.Hosting.EndRequest")) + { + _diagnosticSource.Write("Microsoft.AspNet.Hosting.EndRequest", new { httpContext = httpContext, tickCount = currentTick }); + } + } + else + { + if (_diagnosticSource.IsEnabled("Microsoft.AspNet.Hosting.UnhandledException")) + { + _diagnosticSource.Write("Microsoft.AspNet.Hosting.UnhandledException", new { httpContext = httpContext, tickCount = currentTick, exception = exception }); + } + } + + context.Scope.Dispose(); + + _httpContextFactory.Dispose(httpContext); + } + + public async Task ProcessRequestAsync(Context context) + { + await _application(context.HttpContext); + } + + public struct Context + { + public HttpContext HttpContext { get; set; } + public IDisposable Scope { get; set; } + public int StartTick { get; set; } + } + } +} diff --git a/src/Microsoft.AspNet.Hosting/Internal/HostingEngine.cs b/src/Microsoft.AspNet.Hosting/Internal/HostingEngine.cs index 4a796b6e6f..2d232995ee 100644 --- a/src/Microsoft.AspNet.Hosting/Internal/HostingEngine.cs +++ b/src/Microsoft.AspNet.Hosting/Internal/HostingEngine.cs @@ -6,7 +6,6 @@ using System.Collections.Generic; using System.Diagnostics; using System.Linq; using System.Threading; -using Microsoft.AspNet.Builder; using Microsoft.AspNet.Hosting.Builder; using Microsoft.AspNet.Hosting.Server; using Microsoft.AspNet.Hosting.Startup; @@ -90,45 +89,11 @@ namespace Microsoft.AspNet.Hosting.Internal var logger = _applicationServices.GetRequiredService>(); var diagnosticSource = _applicationServices.GetRequiredService(); + var httpContextFactory = _applicationServices.GetRequiredService(); logger.Starting(); - Server.Start( - async httpContext => - { - if (diagnosticSource.IsEnabled("Microsoft.AspNet.Hosting.BeginRequest")) - { - diagnosticSource.Write("Microsoft.AspNet.Hosting.BeginRequest", new { httpContext = httpContext }); - } - - using (logger.RequestScope(httpContext)) - { - int startTime = 0; - try - { - logger.RequestStarting(httpContext); - - startTime = Environment.TickCount; - await application(httpContext); - - logger.RequestFinished(httpContext, startTime); - } - catch (Exception ex) - { - logger.RequestFailed(httpContext, startTime); - - if (diagnosticSource.IsEnabled("Microsoft.AspNet.Hosting.UnhandledException")) - { - diagnosticSource.Write("Microsoft.AspNet.Hosting.UnhandledException", new { httpContext = httpContext, exception = ex }); - } - throw; - } - } - if (diagnosticSource.IsEnabled("Microsoft.AspNet.Hosting.EndRequest")) - { - diagnosticSource.Write("Microsoft.AspNet.Hosting.EndRequest", new { httpContext = httpContext }); - } - }); + Server.Start(new HostingApplication(application, logger, diagnosticSource, httpContextFactory)); _applicationLifetime.NotifyStarted(); logger.Started(); diff --git a/src/Microsoft.AspNet.Hosting/Internal/HostingLoggerExtensions.cs b/src/Microsoft.AspNet.Hosting/Internal/HostingLoggerExtensions.cs index 67f9f114ff..3730736d0b 100644 --- a/src/Microsoft.AspNet.Hosting/Internal/HostingLoggerExtensions.cs +++ b/src/Microsoft.AspNet.Hosting/Internal/HostingLoggerExtensions.cs @@ -30,11 +30,14 @@ namespace Microsoft.AspNet.Hosting.Internal } } - public static void RequestFinished(this ILogger logger, HttpContext httpContext, int startTimeInTicks) + public static void RequestFinished(this ILogger logger, HttpContext httpContext, int startTimeInTicks, int currentTick) { if (logger.IsEnabled(LogLevel.Information)) { - var elapsed = new TimeSpan(TicksPerMillisecond * (Environment.TickCount - startTimeInTicks)); + var elapsed = new TimeSpan(TicksPerMillisecond * (currentTick < startTimeInTicks ? + (int.MaxValue - startTimeInTicks) + (currentTick - int.MinValue) : + currentTick - startTimeInTicks)); + logger.Log( logLevel: LogLevel.Information, eventId: LoggerEventIds.RequestFinished, @@ -44,20 +47,6 @@ namespace Microsoft.AspNet.Hosting.Internal } } - public static void RequestFailed(this ILogger logger, HttpContext httpContext, int startTimeInTicks) - { - if (logger.IsEnabled(LogLevel.Information)) - { - var elapsed = new TimeSpan(TicksPerMillisecond * (Environment.TickCount - startTimeInTicks)); - logger.Log( - logLevel: LogLevel.Information, - eventId: LoggerEventIds.RequestFailed, - state: new HostingRequestFailed(httpContext, elapsed), - exception: null, - formatter: HostingRequestFailed.Callback); - } - } - public static void ApplicationError(this ILogger logger, Exception exception) { logger.LogError( @@ -221,48 +210,6 @@ namespace Microsoft.AspNet.Hosting.Internal return _cachedGetValues; } } - - private class HostingRequestFailed - { - internal static readonly Func Callback = (state, exception) => ((HostingRequestFailed)state).ToString(); - - private readonly HttpContext _httpContext; - private readonly TimeSpan _elapsed; - - private IEnumerable> _cachedGetValues; - private string _cachedToString; - - public HostingRequestFailed(HttpContext httpContext, TimeSpan elapsed) - { - _httpContext = httpContext; - _elapsed = elapsed; - } - - public override string ToString() - { - if (_cachedToString == null) - { - _cachedToString = $"Request finished in {_elapsed.TotalMilliseconds}ms 500"; - } - - return _cachedToString; - } - - public IEnumerable> GetValues() - { - if (_cachedGetValues == null) - { - _cachedGetValues = new[] - { - new KeyValuePair("ElapsedMilliseconds", _elapsed.TotalMilliseconds), - new KeyValuePair("StatusCode", 500), - new KeyValuePair("ContentType", null), - }; - } - - return _cachedGetValues; - } - } } } diff --git a/src/Microsoft.AspNet.Hosting/Internal/LoggerEventIds.cs b/src/Microsoft.AspNet.Hosting/Internal/LoggerEventIds.cs index 9bc4a4affe..0b23c81e2a 100644 --- a/src/Microsoft.AspNet.Hosting/Internal/LoggerEventIds.cs +++ b/src/Microsoft.AspNet.Hosting/Internal/LoggerEventIds.cs @@ -7,10 +7,9 @@ namespace Microsoft.AspNet.Hosting.Internal { public const int RequestStarting = 1; public const int RequestFinished = 2; - public const int RequestFailed = 3; - public const int Starting = 4; - public const int Started = 5; - public const int Shutdown = 6; - public const int ApplicationStartupException = 7; + public const int Starting = 3; + public const int Started = 4; + public const int Shutdown = 5; + public const int ApplicationStartupException = 6; } } diff --git a/src/Microsoft.AspNet.Hosting/WebHostBuilder.cs b/src/Microsoft.AspNet.Hosting/WebHostBuilder.cs index bc7e0c7883..309d9eff71 100644 --- a/src/Microsoft.AspNet.Hosting/WebHostBuilder.cs +++ b/src/Microsoft.AspNet.Hosting/WebHostBuilder.cs @@ -10,7 +10,6 @@ using Microsoft.AspNet.Hosting.Server; using Microsoft.AspNet.Hosting.Startup; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Internal; -using Microsoft.Extensions.CompilationAbstractions; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection.Extensions; diff --git a/src/Microsoft.AspNet.TestHost/ClientHandler.cs b/src/Microsoft.AspNet.TestHost/ClientHandler.cs index 78e3463bc6..200dc5db9a 100644 --- a/src/Microsoft.AspNet.TestHost/ClientHandler.cs +++ b/src/Microsoft.AspNet.TestHost/ClientHandler.cs @@ -11,9 +11,10 @@ using System.Net; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.Hosting.Server; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Features; -using Microsoft.AspNet.Http.Internal; +using Context = Microsoft.AspNet.Hosting.Internal.HostingApplication.Context; namespace Microsoft.AspNet.TestHost { @@ -23,27 +24,21 @@ namespace Microsoft.AspNet.TestHost /// public class ClientHandler : HttpMessageHandler { - private readonly RequestDelegate _next; + private readonly IHttpApplication _application; private readonly PathString _pathBase; - private readonly IHttpContextFactory _factory; /// /// Create a new handler. /// /// The pipeline entry point. - public ClientHandler(RequestDelegate next, PathString pathBase, IHttpContextFactory httpContextFactory) + public ClientHandler(PathString pathBase, IHttpApplication application) { - if (next == null) + if (application == null) { - throw new ArgumentNullException(nameof(next)); + throw new ArgumentNullException(nameof(application)); } - if (httpContextFactory == null) - { - throw new ArgumentNullException(nameof(httpContextFactory)); - } - - _next = next; - _factory = httpContextFactory; + + _application = application; // PathString.StartsWithSegments that we use below requires the base path to not end in a slash. if (pathBase.HasValue && pathBase.Value.EndsWith("/")) @@ -69,7 +64,7 @@ namespace Microsoft.AspNet.TestHost throw new ArgumentNullException(nameof(request)); } - var state = new RequestState(request, _pathBase, _factory); + var state = new RequestState(request, _pathBase, _application); var requestContent = request.Content ?? new StreamContent(Stream.Null); var body = await requestContent.ReadAsStreamAsync(); if (body.CanSeek) @@ -77,7 +72,7 @@ namespace Microsoft.AspNet.TestHost // This body may have been consumed before, rewind it. body.Seek(0, SeekOrigin.Begin); } - state.HttpContext.Request.Body = body; + state.Context.HttpContext.Request.Body = body; var registration = cancellationToken.Register(state.AbortRequest); // Async offload, don't let the test code block the caller. @@ -85,16 +80,17 @@ namespace Microsoft.AspNet.TestHost { try { - await _next(state.HttpContext); + await _application.ProcessRequestAsync(state.Context); state.CompleteResponse(); + state.ServerCleanup(exception: null); } catch (Exception ex) { state.Abort(ex); + state.ServerCleanup(ex); } finally { - state.ServerCleanup(); registration.Dispose(); } }); @@ -105,20 +101,20 @@ namespace Microsoft.AspNet.TestHost private class RequestState { private readonly HttpRequestMessage _request; + private readonly IHttpApplication _application; private TaskCompletionSource _responseTcs; private ResponseStream _responseStream; private ResponseFeature _responseFeature; private CancellationTokenSource _requestAbortedSource; - private IHttpContextFactory _factory; private bool _pipelineFinished; - internal RequestState(HttpRequestMessage request, PathString pathBase, IHttpContextFactory factory) + internal RequestState(HttpRequestMessage request, PathString pathBase, IHttpApplication application) { _request = request; + _application = application; _responseTcs = new TaskCompletionSource(); _requestAbortedSource = new CancellationTokenSource(); _pipelineFinished = false; - _factory = factory; if (request.RequestUri.IsDefaultPort) { @@ -129,12 +125,13 @@ namespace Microsoft.AspNet.TestHost request.Headers.Host = request.RequestUri.GetComponents(UriComponents.HostAndPort, UriFormat.UriEscaped); } - HttpContext = _factory.Create(new FeatureCollection()); - - HttpContext.Features.Set(new RequestFeature()); + Context = application.CreateContext(new FeatureCollection()); + var httpContext = Context.HttpContext; + + httpContext.Features.Set(new RequestFeature()); _responseFeature = new ResponseFeature(); - HttpContext.Features.Set(_responseFeature); - var serverRequest = HttpContext.Request; + httpContext.Features.Set(_responseFeature); + var serverRequest = httpContext.Request; serverRequest.Protocol = "HTTP/" + request.Version.ToString(2); serverRequest.Scheme = request.RequestUri.Scheme; serverRequest.Method = request.Method.ToString(); @@ -168,12 +165,12 @@ namespace Microsoft.AspNet.TestHost } _responseStream = new ResponseStream(ReturnResponseMessage, AbortRequest); - HttpContext.Response.Body = _responseStream; - HttpContext.Response.StatusCode = 200; - HttpContext.RequestAborted = _requestAbortedSource.Token; + httpContext.Response.Body = _responseStream; + httpContext.Response.StatusCode = 200; + httpContext.RequestAborted = _requestAbortedSource.Token; } - public HttpContext HttpContext { get; private set; } + public Context Context { get; private set; } public Task ResponseTask { @@ -212,16 +209,17 @@ namespace Microsoft.AspNet.TestHost private HttpResponseMessage GenerateResponse() { _responseFeature.FireOnSendingHeaders(); + var httpContext = Context.HttpContext; var response = new HttpResponseMessage(); - response.StatusCode = (HttpStatusCode)HttpContext.Response.StatusCode; - response.ReasonPhrase = HttpContext.Features.Get().ReasonPhrase; + response.StatusCode = (HttpStatusCode)httpContext.Response.StatusCode; + response.ReasonPhrase = httpContext.Features.Get().ReasonPhrase; response.RequestMessage = _request; // response.Version = owinResponse.Protocol; response.Content = new StreamContent(_responseStream); - foreach (var header in HttpContext.Response.Headers) + foreach (var header in httpContext.Response.Headers) { if (!response.Headers.TryAddWithoutValidation(header.Key, (IEnumerable)header.Value)) { @@ -239,12 +237,9 @@ namespace Microsoft.AspNet.TestHost _responseTcs.TrySetException(exception); } - internal void ServerCleanup() + internal void ServerCleanup(Exception exception) { - if (HttpContext != null) - { - _factory.Dispose(HttpContext); - } + _application.DisposeContext(Context, exception); } } } diff --git a/src/Microsoft.AspNet.TestHost/TestServer.cs b/src/Microsoft.AspNet.TestHost/TestServer.cs index cd354c3eba..efeefac128 100644 --- a/src/Microsoft.AspNet.TestHost/TestServer.cs +++ b/src/Microsoft.AspNet.TestHost/TestServer.cs @@ -11,6 +11,7 @@ using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Features; using Microsoft.Extensions.Configuration; using Microsoft.Extensions.DependencyInjection; +using Context = Microsoft.AspNet.Hosting.Internal.HostingApplication.Context; namespace Microsoft.AspNet.TestHost { @@ -18,15 +19,13 @@ namespace Microsoft.AspNet.TestHost { private const string DefaultEnvironmentName = "Development"; private const string ServerName = nameof(TestServer); - private RequestDelegate _appDelegate; private IDisposable _appInstance; private bool _disposed = false; - private IHttpContextFactory _httpContextFactory; + private IHttpApplication _application; public TestServer(WebHostBuilder builder) { var hostingEngine = builder.UseServer(this).Build(); - _httpContextFactory = hostingEngine.ApplicationServices.GetService(); _appInstance = hostingEngine.Start(); } @@ -99,7 +98,7 @@ namespace Microsoft.AspNet.TestHost public HttpMessageHandler CreateHandler() { var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); - return new ClientHandler(Invoke, pathBase, _httpContextFactory); + return new ClientHandler(pathBase, _application); } public HttpClient CreateClient() @@ -110,7 +109,7 @@ namespace Microsoft.AspNet.TestHost public WebSocketClient CreateWebSocketClient() { var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); - return new WebSocketClient(Invoke, pathBase, _httpContextFactory); + return new WebSocketClient(pathBase, _application); } /// @@ -123,24 +122,49 @@ namespace Microsoft.AspNet.TestHost return new RequestBuilder(this, path); } - public Task Invoke(HttpContext context) - { - if (_disposed) - { - throw new ObjectDisposedException(GetType().FullName); - } - return _appDelegate(context); - } - public void Dispose() { _disposed = true; _appInstance.Dispose(); } - void IServer.Start(RequestDelegate requestDelegate) + void IServer.Start(IHttpApplication application) { - _appDelegate = requestDelegate; + _application = new ApplicationWrapper((IHttpApplication)application, () => + { + if (_disposed) + { + throw new ObjectDisposedException(GetType().FullName); + } + }); + } + + private class ApplicationWrapper : IHttpApplication + { + IHttpApplication _application; + Action _preProcessRequestAsync; + + public ApplicationWrapper(IHttpApplication application, Action preProcessRequestAsync) + { + _application = application; + _preProcessRequestAsync = preProcessRequestAsync; + } + + public TContext CreateContext(IFeatureCollection contextFeatures) + { + return _application.CreateContext(contextFeatures); + } + + public void DisposeContext(TContext context, Exception exception) + { + _application.DisposeContext(context, exception); + } + + public Task ProcessRequestAsync(TContext context) + { + _preProcessRequestAsync(); + return _application.ProcessRequestAsync(context); + } } } } \ No newline at end of file diff --git a/src/Microsoft.AspNet.TestHost/WebSocketClient.cs b/src/Microsoft.AspNet.TestHost/WebSocketClient.cs index 320f43afe8..8c9da7e11b 100644 --- a/src/Microsoft.AspNet.TestHost/WebSocketClient.cs +++ b/src/Microsoft.AspNet.TestHost/WebSocketClient.cs @@ -8,31 +8,26 @@ using System.Net.WebSockets; using System.Security.Cryptography; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.Hosting.Server; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Features; -using Microsoft.AspNet.Http.Internal; +using Context = Microsoft.AspNet.Hosting.Internal.HostingApplication.Context; namespace Microsoft.AspNet.TestHost { public class WebSocketClient { - private readonly RequestDelegate _next; + private readonly IHttpApplication _application; private readonly PathString _pathBase; - private readonly IHttpContextFactory _httpContextFactory; - internal WebSocketClient(RequestDelegate next, PathString pathBase, IHttpContextFactory httpContextFactory) + internal WebSocketClient(PathString pathBase, IHttpApplication application) { - if (next == null) + if (application == null) { - throw new ArgumentNullException(nameof(next)); + throw new ArgumentNullException(nameof(application)); } - if (httpContextFactory == null) - { - throw new ArgumentNullException(nameof(httpContextFactory)); - } - - _next = next; - _httpContextFactory = httpContextFactory; + + _application = application; // PathString.StartsWithSegments that we use below requires the base path to not end in a slash. if (pathBase.HasValue && pathBase.Value.EndsWith("/")) @@ -58,11 +53,11 @@ namespace Microsoft.AspNet.TestHost public async Task ConnectAsync(Uri uri, CancellationToken cancellationToken) { - var state = new RequestState(uri, _pathBase, cancellationToken, _httpContextFactory); + var state = new RequestState(uri, _pathBase, cancellationToken, _application); if (ConfigureRequest != null) { - ConfigureRequest(state.HttpContext.Request); + ConfigureRequest(state.Context.HttpContext.Request); } // Async offload, don't let the test code block the caller. @@ -70,12 +65,14 @@ namespace Microsoft.AspNet.TestHost { try { - await _next(state.HttpContext); + await _application.ProcessRequestAsync(state.Context); state.PipelineComplete(); + state.ServerCleanup(exception: null); } catch (Exception ex) { state.PipelineFailed(ex); + state.ServerCleanup(ex); } finally { @@ -88,24 +85,25 @@ namespace Microsoft.AspNet.TestHost private class RequestState : IDisposable, IHttpWebSocketFeature { + private readonly IHttpApplication _application; private TaskCompletionSource _clientWebSocketTcs; private WebSocket _serverWebSocket; - private IHttpContextFactory _factory; - public HttpContext HttpContext { get; private set; } + public Context Context { get; private set; } public Task WebSocketTask { get { return _clientWebSocketTcs.Task; } } - public RequestState(Uri uri, PathString pathBase, CancellationToken cancellationToken, IHttpContextFactory factory) + public RequestState(Uri uri, PathString pathBase, CancellationToken cancellationToken, IHttpApplication application) { - _factory = factory; _clientWebSocketTcs = new TaskCompletionSource(); + _application = application; // HttpContext - HttpContext = _factory.Create(new FeatureCollection()); + Context = _application.CreateContext(new FeatureCollection()); + var httpContext = Context.HttpContext; // Request - HttpContext.Features.Set(new RequestFeature()); - var request = HttpContext.Request; + httpContext.Features.Set(new RequestFeature()); + var request = httpContext.Request; request.Protocol = "HTTP/1.1"; var scheme = uri.Scheme; scheme = (scheme == "ws") ? "http" : scheme; @@ -132,18 +130,18 @@ namespace Microsoft.AspNet.TestHost request.Body = Stream.Null; // Response - HttpContext.Features.Set(new ResponseFeature()); - var response = HttpContext.Response; + httpContext.Features.Set(new ResponseFeature()); + var response = httpContext.Response; response.Body = Stream.Null; response.StatusCode = 200; // WebSocket - HttpContext.Features.Set(this); + httpContext.Features.Set(this); } public void PipelineComplete() { - PipelineFailed(new InvalidOperationException("Incomplete handshake, status code: " + HttpContext.Response.StatusCode)); + PipelineFailed(new InvalidOperationException("Incomplete handshake, status code: " + Context.HttpContext.Response.StatusCode)); } public void PipelineFailed(Exception ex) @@ -153,16 +151,17 @@ namespace Microsoft.AspNet.TestHost public void Dispose() { - if (HttpContext != null) - { - _factory.Dispose(HttpContext); - } if (_serverWebSocket != null) { _serverWebSocket.Dispose(); } } + internal void ServerCleanup(Exception exception) + { + _application.DisposeContext(Context, exception); + } + private string CreateRequestKey() { byte[] data = new byte[16]; @@ -181,7 +180,7 @@ namespace Microsoft.AspNet.TestHost Task IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context) { - HttpContext.Response.StatusCode = 101; // Switching Protocols + Context.HttpContext.Response.StatusCode = 101; // Switching Protocols var websockets = TestWebSocket.CreatePair(context.SubProtocol); _clientWebSocketTcs.SetResult(websockets.Item1); diff --git a/test/Microsoft.AspNet.Hosting.Tests/HostingEngineTests.cs b/test/Microsoft.AspNet.Hosting.Tests/HostingEngineTests.cs index 18640d122d..da2dd15336 100644 --- a/test/Microsoft.AspNet.Hosting.Tests/HostingEngineTests.cs +++ b/test/Microsoft.AspNet.Hosting.Tests/HostingEngineTests.cs @@ -14,7 +14,6 @@ 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; @@ -477,11 +476,21 @@ namespace Microsoft.AspNet.Hosting return new WebHostBuilder(config ?? new ConfigurationBuilder().Build()); } - public void Start(RequestDelegate requestDelegate) + public void Start(IHttpApplication application) { - var startInstance = new StartInstance(requestDelegate); + var startInstance = new StartInstance(); _startInstances.Add(startInstance); - requestDelegate(new DefaultHttpContext(Features)); + var context = application.CreateContext(Features); + try + { + application.ProcessRequestAsync(context); + } + catch (Exception ex) + { + application.DisposeContext(context, ex); + throw; + } + application.DisposeContext(context, null); } public void Dispose() @@ -504,13 +513,6 @@ namespace Microsoft.AspNet.Hosting private class StartInstance : IDisposable { - private readonly RequestDelegate _application; - - public StartInstance(RequestDelegate application) - { - _application = application; - } - public int DisposeCalls { get; set; } public void Dispose() diff --git a/test/Microsoft.AspNet.Hosting.Tests/WebHostBuilderTests.cs b/test/Microsoft.AspNet.Hosting.Tests/WebHostBuilderTests.cs index 7b3bdf7f82..cd7c1ee3b4 100644 --- a/test/Microsoft.AspNet.Hosting.Tests/WebHostBuilderTests.cs +++ b/test/Microsoft.AspNet.Hosting.Tests/WebHostBuilderTests.cs @@ -1,6 +1,7 @@ // 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; @@ -174,9 +175,22 @@ namespace Microsoft.AspNet.Hosting } - public void Start(RequestDelegate requestDelegate) + public void Start(IHttpApplication application) { - RequestDelegate = requestDelegate; + RequestDelegate = async ctx => + { + var httpContext = application.CreateContext(ctx.Features); + try + { + await application.ProcessRequestAsync(httpContext); + } + catch (Exception ex) + { + application.DisposeContext(httpContext, ex); + throw; + } + application.DisposeContext(httpContext, null); + }; } } } diff --git a/test/Microsoft.AspNet.TestHost.Tests/ClientHandlerTests.cs b/test/Microsoft.AspNet.TestHost.Tests/ClientHandlerTests.cs index adacdd9d8e..3108810ff9 100644 --- a/test/Microsoft.AspNet.TestHost.Tests/ClientHandlerTests.cs +++ b/test/Microsoft.AspNet.TestHost.Tests/ClientHandlerTests.cs @@ -7,22 +7,22 @@ using System.Linq; using System.Net.Http; using System.Threading; using System.Threading.Tasks; +using Microsoft.AspNet.Hosting.Server; using Microsoft.AspNet.Http; -using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Internal; +using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Testing.xunit; using Xunit; +using Context = Microsoft.AspNet.Hosting.Internal.HostingApplication.Context; namespace Microsoft.AspNet.TestHost { public class ClientHandlerTests { - private IHttpContextFactory _httpContextFactory = new HttpContextFactory(new HttpContextAccessor()); - [Fact] public Task ExpectedKeysAreAvailable() { - var handler = new ClientHandler(context => + var handler = new ClientHandler(new PathString("/A/Path/"), new DummyApplication(context => { // TODO: Assert.True(context.RequestAborted.CanBeCanceled); Assert.Equal("HTTP/1.1", context.Request.Protocol); @@ -40,7 +40,7 @@ namespace Microsoft.AspNet.TestHost Assert.Equal("example.com", context.Request.Host.Value); return Task.FromResult(0); - }, new PathString("/A/Path/"), _httpContextFactory); + })); var httpClient = new HttpClient(handler); return httpClient.GetAsync("https://example.com/A/Path/and/file.txt?and=query"); } @@ -48,13 +48,13 @@ namespace Microsoft.AspNet.TestHost [Fact] public Task SingleSlashNotMovedToPathBase() { - var handler = new ClientHandler(context => + var handler = new ClientHandler(new PathString(""), new DummyApplication(context => { Assert.Equal("", context.Request.PathBase.Value); Assert.Equal("/", context.Request.Path.Value); return Task.FromResult(0); - }, new PathString(""), _httpContextFactory); + })); var httpClient = new HttpClient(handler); return httpClient.GetAsync("https://example.com/"); } @@ -63,14 +63,14 @@ namespace Microsoft.AspNet.TestHost public async Task ResubmitRequestWorks() { int requestCount = 1; - var handler = new ClientHandler(context => + var handler = new ClientHandler(PathString.Empty, new DummyApplication(context => { 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, _httpContextFactory); + })); HttpMessageInvoker invoker = new HttpMessageInvoker(handler); HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, "https://example.com/"); @@ -86,11 +86,11 @@ namespace Microsoft.AspNet.TestHost [Fact] public async Task MiddlewareOnlySetsHeaders() { - var handler = new ClientHandler(context => + var handler = new ClientHandler(PathString.Empty, new DummyApplication(context => { context.Response.Headers["TestHeader"] = "TestValue"; return Task.FromResult(0); - }, PathString.Empty, _httpContextFactory); + })); var httpClient = new HttpClient(handler); HttpResponseMessage response = await httpClient.GetAsync("https://example.com/"); Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First()); @@ -100,11 +100,11 @@ namespace Microsoft.AspNet.TestHost public async Task BlockingMiddlewareShouldNotBlockClient() { ManualResetEvent block = new ManualResetEvent(false); - var handler = new ClientHandler(context => + var handler = new ClientHandler(PathString.Empty, new DummyApplication(context => { block.WaitOne(); return Task.FromResult(0); - }, PathString.Empty, _httpContextFactory); + })); var httpClient = new HttpClient(handler); Task task = httpClient.GetAsync("https://example.com/"); Assert.False(task.IsCompleted); @@ -117,13 +117,13 @@ namespace Microsoft.AspNet.TestHost public async Task HeadersAvailableBeforeBodyFinished() { ManualResetEvent block = new ManualResetEvent(false); - var handler = new ClientHandler(async context => + var handler = new ClientHandler(PathString.Empty, new DummyApplication(async context => { context.Response.Headers["TestHeader"] = "TestValue"; await context.Response.WriteAsync("BodyStarted,"); block.WaitOne(); await context.Response.WriteAsync("BodyFinished"); - }, PathString.Empty, _httpContextFactory); + })); var httpClient = new HttpClient(handler); HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", HttpCompletionOption.ResponseHeadersRead); @@ -136,13 +136,13 @@ namespace Microsoft.AspNet.TestHost public async Task FlushSendsHeaders() { ManualResetEvent block = new ManualResetEvent(false); - var handler = new ClientHandler(async context => + var handler = new ClientHandler(PathString.Empty, new DummyApplication(async context => { context.Response.Headers["TestHeader"] = "TestValue"; context.Response.Body.Flush(); block.WaitOne(); await context.Response.WriteAsync("BodyFinished"); - }, PathString.Empty, _httpContextFactory); + })); var httpClient = new HttpClient(handler); HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", HttpCompletionOption.ResponseHeadersRead); @@ -155,13 +155,13 @@ namespace Microsoft.AspNet.TestHost public async Task ClientDisposalCloses() { ManualResetEvent block = new ManualResetEvent(false); - var handler = new ClientHandler(context => + var handler = new ClientHandler(PathString.Empty, new DummyApplication(context => { context.Response.Headers["TestHeader"] = "TestValue"; context.Response.Body.Flush(); block.WaitOne(); return Task.FromResult(0); - }, PathString.Empty, _httpContextFactory); + })); var httpClient = new HttpClient(handler); HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", HttpCompletionOption.ResponseHeadersRead); @@ -180,13 +180,13 @@ namespace Microsoft.AspNet.TestHost public async Task ClientCancellationAborts() { ManualResetEvent block = new ManualResetEvent(false); - var handler = new ClientHandler(context => + var handler = new ClientHandler(PathString.Empty, new DummyApplication(context => { context.Response.Headers["TestHeader"] = "TestValue"; context.Response.Body.Flush(); block.WaitOne(); return Task.FromResult(0); - }, PathString.Empty, _httpContextFactory); + })); var httpClient = new HttpClient(handler); HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", HttpCompletionOption.ResponseHeadersRead); @@ -205,10 +205,10 @@ namespace Microsoft.AspNet.TestHost [Fact] public Task ExceptionBeforeFirstWriteIsReported() { - var handler = new ClientHandler(context => + var handler = new ClientHandler(PathString.Empty, new DummyApplication(context => { throw new InvalidOperationException("Test Exception"); - }, PathString.Empty, _httpContextFactory); + })); var httpClient = new HttpClient(handler); return Assert.ThrowsAsync(() => httpClient.GetAsync("https://example.com/", HttpCompletionOption.ResponseHeadersRead)); @@ -219,13 +219,13 @@ namespace Microsoft.AspNet.TestHost public async Task ExceptionAfterFirstWriteIsReported() { ManualResetEvent block = new ManualResetEvent(false); - var handler = new ClientHandler(async context => + var handler = new ClientHandler(PathString.Empty, new DummyApplication(async context => { context.Response.Headers["TestHeader"] = "TestValue"; await context.Response.WriteAsync("BodyStarted"); block.WaitOne(); throw new InvalidOperationException("Test Exception"); - }, PathString.Empty, _httpContextFactory); + })); var httpClient = new HttpClient(handler); HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", HttpCompletionOption.ResponseHeadersRead); @@ -234,5 +234,33 @@ namespace Microsoft.AspNet.TestHost var ex = await Assert.ThrowsAsync(() => response.Content.ReadAsStringAsync()); Assert.IsType(ex.GetBaseException()); } + + private class DummyApplication : IHttpApplication + { + RequestDelegate _application; + + public DummyApplication(RequestDelegate application) + { + _application = application; + } + + public Context CreateContext(IFeatureCollection contextFeatures) + { + return new Context() + { + HttpContext = new DefaultHttpContext(contextFeatures) + }; + } + + public void DisposeContext(Context context, Exception exception) + { + + } + + public Task ProcessRequestAsync(Context context) + { + return _application(context.HttpContext); + } + } } } diff --git a/test/Microsoft.AspNet.TestHost.Tests/TestServerTests.cs b/test/Microsoft.AspNet.TestHost.Tests/TestServerTests.cs index 9ca048c9e0..ad55d2ee69 100644 --- a/test/Microsoft.AspNet.TestHost.Tests/TestServerTests.cs +++ b/test/Microsoft.AspNet.TestHost.Tests/TestServerTests.cs @@ -3,13 +3,12 @@ using System; using System.Diagnostics; -using System.IO; using System.Net; using System.Net.Http; +using System.Threading; using System.Threading.Tasks; using Microsoft.AspNet.Builder; using Microsoft.AspNet.Hosting; -using Microsoft.AspNet.Hosting.Startup; using Microsoft.AspNet.Http; using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Features.Internal; @@ -393,6 +392,9 @@ namespace Microsoft.AspNet.TestHost diagnosticListener.SubscribeWithAdapter(listener); var result = await server.CreateClient().GetStringAsync("/path"); + // This ensures that all diagnostics are completely written to the diagnostic listener + Thread.Sleep(1000); + Assert.Equal("Hello World", result); Assert.NotNull(listener.BeginRequest?.HttpContext); Assert.NotNull(listener.EndRequest?.HttpContext); @@ -414,6 +416,9 @@ namespace Microsoft.AspNet.TestHost var listener = new TestDiagnosticListener(); diagnosticListener.SubscribeWithAdapter(listener); await Assert.ThrowsAsync(() => server.CreateClient().GetAsync("/path")); + + // This ensures that all diagnostics are completely written to the diagnostic listener + Thread.Sleep(1000); Assert.NotNull(listener.BeginRequest?.HttpContext); Assert.Null(listener.EndRequest?.HttpContext);