New IServer design with IHttpApplication added #395

This commit is contained in:
John Luo 2015-11-02 13:55:44 -08:00
parent f600604140
commit 8c256a0d87
14 changed files with 332 additions and 230 deletions

View File

@ -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
{
/// <summary>
/// Represents an HttpApplication.
/// </summary>
public interface IHttpApplication<TContext>
{
/// <summary>
/// Create a TContext given a collection of HTTP features.
/// </summary>
/// <param name="contextFeatures">A collection of HTTP features to be used for creating the TContext.</param>
/// <returns>The created TContext.</returns>
TContext CreateContext(IFeatureCollection contextFeatures);
/// <summary>
/// Asynchronously processes an TContext.
/// </summary>
/// <param name="context">The TContext that the operation will process.</param>
Task ProcessRequestAsync(TContext context);
/// <summary>
/// Dispose a given TContext.
/// </summary>
/// <param name="context">The TContext to be disposed.</param>
/// <param name="exception">The Exception thrown when processing did not complete successfully, otherwise null.</param>
void DisposeContext(TContext context, Exception exception);
}
}

View File

@ -2,7 +2,6 @@
// 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 Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Features;
namespace Microsoft.AspNet.Hosting.Server namespace Microsoft.AspNet.Hosting.Server
@ -18,9 +17,9 @@ namespace Microsoft.AspNet.Hosting.Server
IFeatureCollection Features { get; } IFeatureCollection Features { get; }
/// <summary> /// <summary>
/// Start the server with the given function that processes an HTTP request. /// Start the server with an HttpApplication.
/// </summary> /// </summary>
/// <param name="requestDelegate">A function that processes an HTTP request.</param> /// <param name="application">An instance of <see cref="IHttpApplication"/>.</param>
void Start(RequestDelegate requestDelegate); void Start<TContext>(IHttpApplication<TContext> application);
} }
} }

View File

@ -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<HostingApplication.Context>
{
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; }
}
}
}

View File

@ -6,7 +6,6 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Threading; using System.Threading;
using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Hosting.Builder; using Microsoft.AspNet.Hosting.Builder;
using Microsoft.AspNet.Hosting.Server; using Microsoft.AspNet.Hosting.Server;
using Microsoft.AspNet.Hosting.Startup; using Microsoft.AspNet.Hosting.Startup;
@ -90,45 +89,11 @@ namespace Microsoft.AspNet.Hosting.Internal
var logger = _applicationServices.GetRequiredService<ILogger<HostingEngine>>(); var logger = _applicationServices.GetRequiredService<ILogger<HostingEngine>>();
var diagnosticSource = _applicationServices.GetRequiredService<DiagnosticSource>(); var diagnosticSource = _applicationServices.GetRequiredService<DiagnosticSource>();
var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>();
logger.Starting(); logger.Starting();
Server.Start( Server.Start(new HostingApplication(application, logger, diagnosticSource, httpContextFactory));
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 });
}
});
_applicationLifetime.NotifyStarted(); _applicationLifetime.NotifyStarted();
logger.Started(); logger.Started();

View File

@ -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)) 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( logger.Log(
logLevel: LogLevel.Information, logLevel: LogLevel.Information,
eventId: LoggerEventIds.RequestFinished, 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) public static void ApplicationError(this ILogger logger, Exception exception)
{ {
logger.LogError( logger.LogError(
@ -221,48 +210,6 @@ namespace Microsoft.AspNet.Hosting.Internal
return _cachedGetValues; return _cachedGetValues;
} }
} }
private class HostingRequestFailed
{
internal static readonly Func<object, Exception, string> Callback = (state, exception) => ((HostingRequestFailed)state).ToString();
private readonly HttpContext _httpContext;
private readonly TimeSpan _elapsed;
private IEnumerable<KeyValuePair<string, object>> _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<KeyValuePair<string, object>> GetValues()
{
if (_cachedGetValues == null)
{
_cachedGetValues = new[]
{
new KeyValuePair<string, object>("ElapsedMilliseconds", _elapsed.TotalMilliseconds),
new KeyValuePair<string, object>("StatusCode", 500),
new KeyValuePair<string, object>("ContentType", null),
};
}
return _cachedGetValues;
}
}
} }
} }

View File

@ -7,10 +7,9 @@ namespace Microsoft.AspNet.Hosting.Internal
{ {
public const int RequestStarting = 1; public const int RequestStarting = 1;
public const int RequestFinished = 2; public const int RequestFinished = 2;
public const int RequestFailed = 3; public const int Starting = 3;
public const int Starting = 4; public const int Started = 4;
public const int Started = 5; public const int Shutdown = 5;
public const int Shutdown = 6; public const int ApplicationStartupException = 6;
public const int ApplicationStartupException = 7;
} }
} }

View File

@ -10,7 +10,6 @@ using Microsoft.AspNet.Hosting.Server;
using Microsoft.AspNet.Hosting.Startup; using Microsoft.AspNet.Hosting.Startup;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Http.Internal;
using Microsoft.Extensions.CompilationAbstractions;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.DependencyInjection.Extensions; using Microsoft.Extensions.DependencyInjection.Extensions;

View File

@ -11,9 +11,10 @@ using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Hosting.Server;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Http.Internal; using Context = Microsoft.AspNet.Hosting.Internal.HostingApplication.Context;
namespace Microsoft.AspNet.TestHost namespace Microsoft.AspNet.TestHost
{ {
@ -23,27 +24,21 @@ namespace Microsoft.AspNet.TestHost
/// </summary> /// </summary>
public class ClientHandler : HttpMessageHandler public class ClientHandler : HttpMessageHandler
{ {
private readonly RequestDelegate _next; private readonly IHttpApplication<Context> _application;
private readonly PathString _pathBase; private readonly PathString _pathBase;
private readonly IHttpContextFactory _factory;
/// <summary> /// <summary>
/// Create a new handler. /// Create a new handler.
/// </summary> /// </summary>
/// <param name="next">The pipeline entry point.</param> /// <param name="next">The pipeline entry point.</param>
public ClientHandler(RequestDelegate next, PathString pathBase, IHttpContextFactory httpContextFactory) public ClientHandler(PathString pathBase, IHttpApplication<Context> application)
{ {
if (next == null) if (application == null)
{ {
throw new ArgumentNullException(nameof(next)); throw new ArgumentNullException(nameof(application));
} }
if (httpContextFactory == null)
{ _application = application;
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. // PathString.StartsWithSegments that we use below requires the base path to not end in a slash.
if (pathBase.HasValue && pathBase.Value.EndsWith("/")) if (pathBase.HasValue && pathBase.Value.EndsWith("/"))
@ -69,7 +64,7 @@ namespace Microsoft.AspNet.TestHost
throw new ArgumentNullException(nameof(request)); 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 requestContent = request.Content ?? new StreamContent(Stream.Null);
var body = await requestContent.ReadAsStreamAsync(); var body = await requestContent.ReadAsStreamAsync();
if (body.CanSeek) if (body.CanSeek)
@ -77,7 +72,7 @@ namespace Microsoft.AspNet.TestHost
// This body may have been consumed before, rewind it. // This body may have been consumed before, rewind it.
body.Seek(0, SeekOrigin.Begin); body.Seek(0, SeekOrigin.Begin);
} }
state.HttpContext.Request.Body = body; state.Context.HttpContext.Request.Body = body;
var registration = cancellationToken.Register(state.AbortRequest); var registration = cancellationToken.Register(state.AbortRequest);
// Async offload, don't let the test code block the caller. // Async offload, don't let the test code block the caller.
@ -85,16 +80,17 @@ namespace Microsoft.AspNet.TestHost
{ {
try try
{ {
await _next(state.HttpContext); await _application.ProcessRequestAsync(state.Context);
state.CompleteResponse(); state.CompleteResponse();
state.ServerCleanup(exception: null);
} }
catch (Exception ex) catch (Exception ex)
{ {
state.Abort(ex); state.Abort(ex);
state.ServerCleanup(ex);
} }
finally finally
{ {
state.ServerCleanup();
registration.Dispose(); registration.Dispose();
} }
}); });
@ -105,20 +101,20 @@ namespace Microsoft.AspNet.TestHost
private class RequestState private class RequestState
{ {
private readonly HttpRequestMessage _request; private readonly HttpRequestMessage _request;
private readonly IHttpApplication<Context> _application;
private TaskCompletionSource<HttpResponseMessage> _responseTcs; private TaskCompletionSource<HttpResponseMessage> _responseTcs;
private ResponseStream _responseStream; private ResponseStream _responseStream;
private ResponseFeature _responseFeature; private ResponseFeature _responseFeature;
private CancellationTokenSource _requestAbortedSource; private CancellationTokenSource _requestAbortedSource;
private IHttpContextFactory _factory;
private bool _pipelineFinished; private bool _pipelineFinished;
internal RequestState(HttpRequestMessage request, PathString pathBase, IHttpContextFactory factory) internal RequestState(HttpRequestMessage request, PathString pathBase, IHttpApplication<Context> application)
{ {
_request = request; _request = request;
_application = application;
_responseTcs = new TaskCompletionSource<HttpResponseMessage>(); _responseTcs = new TaskCompletionSource<HttpResponseMessage>();
_requestAbortedSource = new CancellationTokenSource(); _requestAbortedSource = new CancellationTokenSource();
_pipelineFinished = false; _pipelineFinished = false;
_factory = factory;
if (request.RequestUri.IsDefaultPort) if (request.RequestUri.IsDefaultPort)
{ {
@ -129,12 +125,13 @@ namespace Microsoft.AspNet.TestHost
request.Headers.Host = request.RequestUri.GetComponents(UriComponents.HostAndPort, UriFormat.UriEscaped); request.Headers.Host = request.RequestUri.GetComponents(UriComponents.HostAndPort, UriFormat.UriEscaped);
} }
HttpContext = _factory.Create(new FeatureCollection()); Context = application.CreateContext(new FeatureCollection());
var httpContext = Context.HttpContext;
HttpContext.Features.Set<IHttpRequestFeature>(new RequestFeature());
httpContext.Features.Set<IHttpRequestFeature>(new RequestFeature());
_responseFeature = new ResponseFeature(); _responseFeature = new ResponseFeature();
HttpContext.Features.Set<IHttpResponseFeature>(_responseFeature); httpContext.Features.Set<IHttpResponseFeature>(_responseFeature);
var serverRequest = HttpContext.Request; var serverRequest = httpContext.Request;
serverRequest.Protocol = "HTTP/" + request.Version.ToString(2); serverRequest.Protocol = "HTTP/" + request.Version.ToString(2);
serverRequest.Scheme = request.RequestUri.Scheme; serverRequest.Scheme = request.RequestUri.Scheme;
serverRequest.Method = request.Method.ToString(); serverRequest.Method = request.Method.ToString();
@ -168,12 +165,12 @@ namespace Microsoft.AspNet.TestHost
} }
_responseStream = new ResponseStream(ReturnResponseMessage, AbortRequest); _responseStream = new ResponseStream(ReturnResponseMessage, AbortRequest);
HttpContext.Response.Body = _responseStream; httpContext.Response.Body = _responseStream;
HttpContext.Response.StatusCode = 200; httpContext.Response.StatusCode = 200;
HttpContext.RequestAborted = _requestAbortedSource.Token; httpContext.RequestAborted = _requestAbortedSource.Token;
} }
public HttpContext HttpContext { get; private set; } public Context Context { get; private set; }
public Task<HttpResponseMessage> ResponseTask public Task<HttpResponseMessage> ResponseTask
{ {
@ -212,16 +209,17 @@ namespace Microsoft.AspNet.TestHost
private HttpResponseMessage GenerateResponse() private HttpResponseMessage GenerateResponse()
{ {
_responseFeature.FireOnSendingHeaders(); _responseFeature.FireOnSendingHeaders();
var httpContext = Context.HttpContext;
var response = new HttpResponseMessage(); var response = new HttpResponseMessage();
response.StatusCode = (HttpStatusCode)HttpContext.Response.StatusCode; response.StatusCode = (HttpStatusCode)httpContext.Response.StatusCode;
response.ReasonPhrase = HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase; response.ReasonPhrase = httpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase;
response.RequestMessage = _request; response.RequestMessage = _request;
// response.Version = owinResponse.Protocol; // response.Version = owinResponse.Protocol;
response.Content = new StreamContent(_responseStream); 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<string>)header.Value)) if (!response.Headers.TryAddWithoutValidation(header.Key, (IEnumerable<string>)header.Value))
{ {
@ -239,12 +237,9 @@ namespace Microsoft.AspNet.TestHost
_responseTcs.TrySetException(exception); _responseTcs.TrySetException(exception);
} }
internal void ServerCleanup() internal void ServerCleanup(Exception exception)
{ {
if (HttpContext != null) _application.DisposeContext(Context, exception);
{
_factory.Dispose(HttpContext);
}
} }
} }
} }

View File

@ -11,6 +11,7 @@ using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Features;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.DependencyInjection;
using Context = Microsoft.AspNet.Hosting.Internal.HostingApplication.Context;
namespace Microsoft.AspNet.TestHost namespace Microsoft.AspNet.TestHost
{ {
@ -18,15 +19,13 @@ namespace Microsoft.AspNet.TestHost
{ {
private const string DefaultEnvironmentName = "Development"; private const string DefaultEnvironmentName = "Development";
private const string ServerName = nameof(TestServer); private const string ServerName = nameof(TestServer);
private RequestDelegate _appDelegate;
private IDisposable _appInstance; private IDisposable _appInstance;
private bool _disposed = false; private bool _disposed = false;
private IHttpContextFactory _httpContextFactory; private IHttpApplication<Context> _application;
public TestServer(WebHostBuilder builder) public TestServer(WebHostBuilder builder)
{ {
var hostingEngine = builder.UseServer(this).Build(); var hostingEngine = builder.UseServer(this).Build();
_httpContextFactory = hostingEngine.ApplicationServices.GetService<IHttpContextFactory>();
_appInstance = hostingEngine.Start(); _appInstance = hostingEngine.Start();
} }
@ -99,7 +98,7 @@ namespace Microsoft.AspNet.TestHost
public HttpMessageHandler CreateHandler() public HttpMessageHandler CreateHandler()
{ {
var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress);
return new ClientHandler(Invoke, pathBase, _httpContextFactory); return new ClientHandler(pathBase, _application);
} }
public HttpClient CreateClient() public HttpClient CreateClient()
@ -110,7 +109,7 @@ namespace Microsoft.AspNet.TestHost
public WebSocketClient CreateWebSocketClient() public WebSocketClient CreateWebSocketClient()
{ {
var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress); var pathBase = BaseAddress == null ? PathString.Empty : PathString.FromUriComponent(BaseAddress);
return new WebSocketClient(Invoke, pathBase, _httpContextFactory); return new WebSocketClient(pathBase, _application);
} }
/// <summary> /// <summary>
@ -123,24 +122,49 @@ namespace Microsoft.AspNet.TestHost
return new RequestBuilder(this, path); return new RequestBuilder(this, path);
} }
public Task Invoke(HttpContext context)
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().FullName);
}
return _appDelegate(context);
}
public void Dispose() public void Dispose()
{ {
_disposed = true; _disposed = true;
_appInstance.Dispose(); _appInstance.Dispose();
} }
void IServer.Start(RequestDelegate requestDelegate) void IServer.Start<TContext>(IHttpApplication<TContext> application)
{ {
_appDelegate = requestDelegate; _application = new ApplicationWrapper<Context>((IHttpApplication<Context>)application, () =>
{
if (_disposed)
{
throw new ObjectDisposedException(GetType().FullName);
}
});
}
private class ApplicationWrapper<TContext> : IHttpApplication<TContext>
{
IHttpApplication<TContext> _application;
Action _preProcessRequestAsync;
public ApplicationWrapper(IHttpApplication<TContext> 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);
}
} }
} }
} }

View File

@ -8,31 +8,26 @@ using System.Net.WebSockets;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Hosting.Server;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Http.Internal; using Context = Microsoft.AspNet.Hosting.Internal.HostingApplication.Context;
namespace Microsoft.AspNet.TestHost namespace Microsoft.AspNet.TestHost
{ {
public class WebSocketClient public class WebSocketClient
{ {
private readonly RequestDelegate _next; private readonly IHttpApplication<Context> _application;
private readonly PathString _pathBase; private readonly PathString _pathBase;
private readonly IHttpContextFactory _httpContextFactory;
internal WebSocketClient(RequestDelegate next, PathString pathBase, IHttpContextFactory httpContextFactory) internal WebSocketClient(PathString pathBase, IHttpApplication<Context> application)
{ {
if (next == null) if (application == null)
{ {
throw new ArgumentNullException(nameof(next)); throw new ArgumentNullException(nameof(application));
} }
if (httpContextFactory == null)
{ _application = application;
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. // PathString.StartsWithSegments that we use below requires the base path to not end in a slash.
if (pathBase.HasValue && pathBase.Value.EndsWith("/")) if (pathBase.HasValue && pathBase.Value.EndsWith("/"))
@ -58,11 +53,11 @@ namespace Microsoft.AspNet.TestHost
public async Task<WebSocket> ConnectAsync(Uri uri, CancellationToken cancellationToken) public async Task<WebSocket> ConnectAsync(Uri uri, CancellationToken cancellationToken)
{ {
var state = new RequestState(uri, _pathBase, cancellationToken, _httpContextFactory); var state = new RequestState(uri, _pathBase, cancellationToken, _application);
if (ConfigureRequest != null) if (ConfigureRequest != null)
{ {
ConfigureRequest(state.HttpContext.Request); ConfigureRequest(state.Context.HttpContext.Request);
} }
// Async offload, don't let the test code block the caller. // Async offload, don't let the test code block the caller.
@ -70,12 +65,14 @@ namespace Microsoft.AspNet.TestHost
{ {
try try
{ {
await _next(state.HttpContext); await _application.ProcessRequestAsync(state.Context);
state.PipelineComplete(); state.PipelineComplete();
state.ServerCleanup(exception: null);
} }
catch (Exception ex) catch (Exception ex)
{ {
state.PipelineFailed(ex); state.PipelineFailed(ex);
state.ServerCleanup(ex);
} }
finally finally
{ {
@ -88,24 +85,25 @@ namespace Microsoft.AspNet.TestHost
private class RequestState : IDisposable, IHttpWebSocketFeature private class RequestState : IDisposable, IHttpWebSocketFeature
{ {
private readonly IHttpApplication<Context> _application;
private TaskCompletionSource<WebSocket> _clientWebSocketTcs; private TaskCompletionSource<WebSocket> _clientWebSocketTcs;
private WebSocket _serverWebSocket; private WebSocket _serverWebSocket;
private IHttpContextFactory _factory;
public HttpContext HttpContext { get; private set; } public Context Context { get; private set; }
public Task<WebSocket> WebSocketTask { get { return _clientWebSocketTcs.Task; } } public Task<WebSocket> WebSocketTask { get { return _clientWebSocketTcs.Task; } }
public RequestState(Uri uri, PathString pathBase, CancellationToken cancellationToken, IHttpContextFactory factory) public RequestState(Uri uri, PathString pathBase, CancellationToken cancellationToken, IHttpApplication<Context> application)
{ {
_factory = factory;
_clientWebSocketTcs = new TaskCompletionSource<WebSocket>(); _clientWebSocketTcs = new TaskCompletionSource<WebSocket>();
_application = application;
// HttpContext // HttpContext
HttpContext = _factory.Create(new FeatureCollection()); Context = _application.CreateContext(new FeatureCollection());
var httpContext = Context.HttpContext;
// Request // Request
HttpContext.Features.Set<IHttpRequestFeature>(new RequestFeature()); httpContext.Features.Set<IHttpRequestFeature>(new RequestFeature());
var request = HttpContext.Request; var request = httpContext.Request;
request.Protocol = "HTTP/1.1"; request.Protocol = "HTTP/1.1";
var scheme = uri.Scheme; var scheme = uri.Scheme;
scheme = (scheme == "ws") ? "http" : scheme; scheme = (scheme == "ws") ? "http" : scheme;
@ -132,18 +130,18 @@ namespace Microsoft.AspNet.TestHost
request.Body = Stream.Null; request.Body = Stream.Null;
// Response // Response
HttpContext.Features.Set<IHttpResponseFeature>(new ResponseFeature()); httpContext.Features.Set<IHttpResponseFeature>(new ResponseFeature());
var response = HttpContext.Response; var response = httpContext.Response;
response.Body = Stream.Null; response.Body = Stream.Null;
response.StatusCode = 200; response.StatusCode = 200;
// WebSocket // WebSocket
HttpContext.Features.Set<IHttpWebSocketFeature>(this); httpContext.Features.Set<IHttpWebSocketFeature>(this);
} }
public void PipelineComplete() 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) public void PipelineFailed(Exception ex)
@ -153,16 +151,17 @@ namespace Microsoft.AspNet.TestHost
public void Dispose() public void Dispose()
{ {
if (HttpContext != null)
{
_factory.Dispose(HttpContext);
}
if (_serverWebSocket != null) if (_serverWebSocket != null)
{ {
_serverWebSocket.Dispose(); _serverWebSocket.Dispose();
} }
} }
internal void ServerCleanup(Exception exception)
{
_application.DisposeContext(Context, exception);
}
private string CreateRequestKey() private string CreateRequestKey()
{ {
byte[] data = new byte[16]; byte[] data = new byte[16];
@ -181,7 +180,7 @@ namespace Microsoft.AspNet.TestHost
Task<WebSocket> IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context) Task<WebSocket> IHttpWebSocketFeature.AcceptAsync(WebSocketAcceptContext context)
{ {
HttpContext.Response.StatusCode = 101; // Switching Protocols Context.HttpContext.Response.StatusCode = 101; // Switching Protocols
var websockets = TestWebSocket.CreatePair(context.SubProtocol); var websockets = TestWebSocket.CreatePair(context.SubProtocol);
_clientWebSocketTcs.SetResult(websockets.Item1); _clientWebSocketTcs.SetResult(websockets.Item1);

View File

@ -14,7 +14,6 @@ using Microsoft.AspNet.Hosting.Server;
using Microsoft.AspNet.Hosting.Startup; using Microsoft.AspNet.Hosting.Startup;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Server.Features; using Microsoft.AspNet.Server.Features;
using Microsoft.AspNet.Testing.xunit; using Microsoft.AspNet.Testing.xunit;
using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Configuration;
@ -477,11 +476,21 @@ namespace Microsoft.AspNet.Hosting
return new WebHostBuilder(config ?? new ConfigurationBuilder().Build()); return new WebHostBuilder(config ?? new ConfigurationBuilder().Build());
} }
public void Start(RequestDelegate requestDelegate) public void Start<TContext>(IHttpApplication<TContext> application)
{ {
var startInstance = new StartInstance(requestDelegate); var startInstance = new StartInstance();
_startInstances.Add(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() public void Dispose()
@ -504,13 +513,6 @@ namespace Microsoft.AspNet.Hosting
private class StartInstance : IDisposable private class StartInstance : IDisposable
{ {
private readonly RequestDelegate _application;
public StartInstance(RequestDelegate application)
{
_application = application;
}
public int DisposeCalls { get; set; } public int DisposeCalls { get; set; }
public void Dispose() public void Dispose()

View File

@ -1,6 +1,7 @@
// 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. // 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.Collections.Generic;
using System.IO; using System.IO;
using System.Threading.Tasks; using System.Threading.Tasks;
@ -174,9 +175,22 @@ namespace Microsoft.AspNet.Hosting
} }
public void Start(RequestDelegate requestDelegate) public void Start<TContext>(IHttpApplication<TContext> 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);
};
} }
} }
} }

View File

@ -7,22 +7,22 @@ using System.Linq;
using System.Net.Http; using System.Net.Http;
using System.Threading; using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Hosting.Server;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Http.Internal; using Microsoft.AspNet.Http.Internal;
using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Testing.xunit; using Microsoft.AspNet.Testing.xunit;
using Xunit; using Xunit;
using Context = Microsoft.AspNet.Hosting.Internal.HostingApplication.Context;
namespace Microsoft.AspNet.TestHost namespace Microsoft.AspNet.TestHost
{ {
public class ClientHandlerTests public class ClientHandlerTests
{ {
private IHttpContextFactory _httpContextFactory = new HttpContextFactory(new HttpContextAccessor());
[Fact] [Fact]
public Task ExpectedKeysAreAvailable() 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); // TODO: Assert.True(context.RequestAborted.CanBeCanceled);
Assert.Equal("HTTP/1.1", context.Request.Protocol); Assert.Equal("HTTP/1.1", context.Request.Protocol);
@ -40,7 +40,7 @@ namespace Microsoft.AspNet.TestHost
Assert.Equal("example.com", context.Request.Host.Value); Assert.Equal("example.com", context.Request.Host.Value);
return Task.FromResult(0); return Task.FromResult(0);
}, new PathString("/A/Path/"), _httpContextFactory); }));
var httpClient = new HttpClient(handler); var httpClient = new HttpClient(handler);
return httpClient.GetAsync("https://example.com/A/Path/and/file.txt?and=query"); return httpClient.GetAsync("https://example.com/A/Path/and/file.txt?and=query");
} }
@ -48,13 +48,13 @@ namespace Microsoft.AspNet.TestHost
[Fact] [Fact]
public Task SingleSlashNotMovedToPathBase() 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.PathBase.Value);
Assert.Equal("/", context.Request.Path.Value); Assert.Equal("/", context.Request.Path.Value);
return Task.FromResult(0); return Task.FromResult(0);
}, new PathString(""), _httpContextFactory); }));
var httpClient = new HttpClient(handler); var httpClient = new HttpClient(handler);
return httpClient.GetAsync("https://example.com/"); return httpClient.GetAsync("https://example.com/");
} }
@ -63,14 +63,14 @@ namespace Microsoft.AspNet.TestHost
public async Task ResubmitRequestWorks() public async Task ResubmitRequestWorks()
{ {
int requestCount = 1; 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); int read = context.Request.Body.Read(new byte[100], 0, 100);
Assert.Equal(11, read); Assert.Equal(11, read);
context.Response.Headers["TestHeader"] = "TestValue:" + requestCount++; context.Response.Headers["TestHeader"] = "TestValue:" + requestCount++;
return Task.FromResult(0); return Task.FromResult(0);
}, PathString.Empty, _httpContextFactory); }));
HttpMessageInvoker invoker = new HttpMessageInvoker(handler); HttpMessageInvoker invoker = new HttpMessageInvoker(handler);
HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, "https://example.com/"); HttpRequestMessage message = new HttpRequestMessage(HttpMethod.Post, "https://example.com/");
@ -86,11 +86,11 @@ namespace Microsoft.AspNet.TestHost
[Fact] [Fact]
public async Task MiddlewareOnlySetsHeaders() public async Task MiddlewareOnlySetsHeaders()
{ {
var handler = new ClientHandler(context => var handler = new ClientHandler(PathString.Empty, new DummyApplication(context =>
{ {
context.Response.Headers["TestHeader"] = "TestValue"; context.Response.Headers["TestHeader"] = "TestValue";
return Task.FromResult(0); return Task.FromResult(0);
}, PathString.Empty, _httpContextFactory); }));
var httpClient = new HttpClient(handler); var httpClient = new HttpClient(handler);
HttpResponseMessage response = await httpClient.GetAsync("https://example.com/"); HttpResponseMessage response = await httpClient.GetAsync("https://example.com/");
Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First()); Assert.Equal("TestValue", response.Headers.GetValues("TestHeader").First());
@ -100,11 +100,11 @@ namespace Microsoft.AspNet.TestHost
public async Task BlockingMiddlewareShouldNotBlockClient() public async Task BlockingMiddlewareShouldNotBlockClient()
{ {
ManualResetEvent block = new ManualResetEvent(false); ManualResetEvent block = new ManualResetEvent(false);
var handler = new ClientHandler(context => var handler = new ClientHandler(PathString.Empty, new DummyApplication(context =>
{ {
block.WaitOne(); block.WaitOne();
return Task.FromResult(0); return Task.FromResult(0);
}, PathString.Empty, _httpContextFactory); }));
var httpClient = new HttpClient(handler); var httpClient = new HttpClient(handler);
Task<HttpResponseMessage> task = httpClient.GetAsync("https://example.com/"); Task<HttpResponseMessage> task = httpClient.GetAsync("https://example.com/");
Assert.False(task.IsCompleted); Assert.False(task.IsCompleted);
@ -117,13 +117,13 @@ namespace Microsoft.AspNet.TestHost
public async Task HeadersAvailableBeforeBodyFinished() public async Task HeadersAvailableBeforeBodyFinished()
{ {
ManualResetEvent block = new ManualResetEvent(false); 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.Headers["TestHeader"] = "TestValue";
await context.Response.WriteAsync("BodyStarted,"); await context.Response.WriteAsync("BodyStarted,");
block.WaitOne(); block.WaitOne();
await context.Response.WriteAsync("BodyFinished"); await context.Response.WriteAsync("BodyFinished");
}, PathString.Empty, _httpContextFactory); }));
var httpClient = new HttpClient(handler); var httpClient = new HttpClient(handler);
HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", HttpResponseMessage response = await httpClient.GetAsync("https://example.com/",
HttpCompletionOption.ResponseHeadersRead); HttpCompletionOption.ResponseHeadersRead);
@ -136,13 +136,13 @@ namespace Microsoft.AspNet.TestHost
public async Task FlushSendsHeaders() public async Task FlushSendsHeaders()
{ {
ManualResetEvent block = new ManualResetEvent(false); 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.Headers["TestHeader"] = "TestValue";
context.Response.Body.Flush(); context.Response.Body.Flush();
block.WaitOne(); block.WaitOne();
await context.Response.WriteAsync("BodyFinished"); await context.Response.WriteAsync("BodyFinished");
}, PathString.Empty, _httpContextFactory); }));
var httpClient = new HttpClient(handler); var httpClient = new HttpClient(handler);
HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", HttpResponseMessage response = await httpClient.GetAsync("https://example.com/",
HttpCompletionOption.ResponseHeadersRead); HttpCompletionOption.ResponseHeadersRead);
@ -155,13 +155,13 @@ namespace Microsoft.AspNet.TestHost
public async Task ClientDisposalCloses() public async Task ClientDisposalCloses()
{ {
ManualResetEvent block = new ManualResetEvent(false); 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.Headers["TestHeader"] = "TestValue";
context.Response.Body.Flush(); context.Response.Body.Flush();
block.WaitOne(); block.WaitOne();
return Task.FromResult(0); return Task.FromResult(0);
}, PathString.Empty, _httpContextFactory); }));
var httpClient = new HttpClient(handler); var httpClient = new HttpClient(handler);
HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", HttpResponseMessage response = await httpClient.GetAsync("https://example.com/",
HttpCompletionOption.ResponseHeadersRead); HttpCompletionOption.ResponseHeadersRead);
@ -180,13 +180,13 @@ namespace Microsoft.AspNet.TestHost
public async Task ClientCancellationAborts() public async Task ClientCancellationAborts()
{ {
ManualResetEvent block = new ManualResetEvent(false); 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.Headers["TestHeader"] = "TestValue";
context.Response.Body.Flush(); context.Response.Body.Flush();
block.WaitOne(); block.WaitOne();
return Task.FromResult(0); return Task.FromResult(0);
}, PathString.Empty, _httpContextFactory); }));
var httpClient = new HttpClient(handler); var httpClient = new HttpClient(handler);
HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", HttpResponseMessage response = await httpClient.GetAsync("https://example.com/",
HttpCompletionOption.ResponseHeadersRead); HttpCompletionOption.ResponseHeadersRead);
@ -205,10 +205,10 @@ namespace Microsoft.AspNet.TestHost
[Fact] [Fact]
public Task ExceptionBeforeFirstWriteIsReported() public Task ExceptionBeforeFirstWriteIsReported()
{ {
var handler = new ClientHandler(context => var handler = new ClientHandler(PathString.Empty, new DummyApplication(context =>
{ {
throw new InvalidOperationException("Test Exception"); throw new InvalidOperationException("Test Exception");
}, PathString.Empty, _httpContextFactory); }));
var httpClient = new HttpClient(handler); var httpClient = new HttpClient(handler);
return Assert.ThrowsAsync<InvalidOperationException>(() => httpClient.GetAsync("https://example.com/", return Assert.ThrowsAsync<InvalidOperationException>(() => httpClient.GetAsync("https://example.com/",
HttpCompletionOption.ResponseHeadersRead)); HttpCompletionOption.ResponseHeadersRead));
@ -219,13 +219,13 @@ namespace Microsoft.AspNet.TestHost
public async Task ExceptionAfterFirstWriteIsReported() public async Task ExceptionAfterFirstWriteIsReported()
{ {
ManualResetEvent block = new ManualResetEvent(false); 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.Headers["TestHeader"] = "TestValue";
await context.Response.WriteAsync("BodyStarted"); await context.Response.WriteAsync("BodyStarted");
block.WaitOne(); block.WaitOne();
throw new InvalidOperationException("Test Exception"); throw new InvalidOperationException("Test Exception");
}, PathString.Empty, _httpContextFactory); }));
var httpClient = new HttpClient(handler); var httpClient = new HttpClient(handler);
HttpResponseMessage response = await httpClient.GetAsync("https://example.com/", HttpResponseMessage response = await httpClient.GetAsync("https://example.com/",
HttpCompletionOption.ResponseHeadersRead); HttpCompletionOption.ResponseHeadersRead);
@ -234,5 +234,33 @@ namespace Microsoft.AspNet.TestHost
var ex = await Assert.ThrowsAsync<HttpRequestException>(() => response.Content.ReadAsStringAsync()); var ex = await Assert.ThrowsAsync<HttpRequestException>(() => response.Content.ReadAsStringAsync());
Assert.IsType<InvalidOperationException>(ex.GetBaseException()); Assert.IsType<InvalidOperationException>(ex.GetBaseException());
} }
private class DummyApplication : IHttpApplication<Context>
{
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);
}
}
} }
} }

View File

@ -3,13 +3,12 @@
using System; using System;
using System.Diagnostics; using System.Diagnostics;
using System.IO;
using System.Net; using System.Net;
using System.Net.Http; using System.Net.Http;
using System.Threading;
using System.Threading.Tasks; using System.Threading.Tasks;
using Microsoft.AspNet.Builder; using Microsoft.AspNet.Builder;
using Microsoft.AspNet.Hosting; using Microsoft.AspNet.Hosting;
using Microsoft.AspNet.Hosting.Startup;
using Microsoft.AspNet.Http; using Microsoft.AspNet.Http;
using Microsoft.AspNet.Http.Features; using Microsoft.AspNet.Http.Features;
using Microsoft.AspNet.Http.Features.Internal; using Microsoft.AspNet.Http.Features.Internal;
@ -393,6 +392,9 @@ namespace Microsoft.AspNet.TestHost
diagnosticListener.SubscribeWithAdapter(listener); diagnosticListener.SubscribeWithAdapter(listener);
var result = await server.CreateClient().GetStringAsync("/path"); 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.Equal("Hello World", result);
Assert.NotNull(listener.BeginRequest?.HttpContext); Assert.NotNull(listener.BeginRequest?.HttpContext);
Assert.NotNull(listener.EndRequest?.HttpContext); Assert.NotNull(listener.EndRequest?.HttpContext);
@ -414,6 +416,9 @@ namespace Microsoft.AspNet.TestHost
var listener = new TestDiagnosticListener(); var listener = new TestDiagnosticListener();
diagnosticListener.SubscribeWithAdapter(listener); diagnosticListener.SubscribeWithAdapter(listener);
await Assert.ThrowsAsync<Exception>(() => server.CreateClient().GetAsync("/path")); await Assert.ThrowsAsync<Exception>(() => 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.NotNull(listener.BeginRequest?.HttpContext);
Assert.Null(listener.EndRequest?.HttpContext); Assert.Null(listener.EndRequest?.HttpContext);