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.
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; }
/// <summary>
/// Start the server with the given function that processes an HTTP request.
/// Start the server with an HttpApplication.
/// </summary>
/// <param name="requestDelegate">A function that processes an HTTP request.</param>
void Start(RequestDelegate requestDelegate);
/// <param name="application">An instance of <see cref="IHttpApplication"/>.</param>
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.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<ILogger<HostingEngine>>();
var diagnosticSource = _applicationServices.GetRequiredService<DiagnosticSource>();
var httpContextFactory = _applicationServices.GetRequiredService<IHttpContextFactory>();
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();

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))
{
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<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 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;
}
}

View File

@ -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;

View File

@ -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
/// </summary>
public class ClientHandler : HttpMessageHandler
{
private readonly RequestDelegate _next;
private readonly IHttpApplication<Context> _application;
private readonly PathString _pathBase;
private readonly IHttpContextFactory _factory;
/// <summary>
/// Create a new handler.
/// </summary>
/// <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)
{
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<Context> _application;
private TaskCompletionSource<HttpResponseMessage> _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<Context> application)
{
_request = request;
_application = application;
_responseTcs = new TaskCompletionSource<HttpResponseMessage>();
_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<IHttpRequestFeature>(new RequestFeature());
Context = application.CreateContext(new FeatureCollection());
var httpContext = Context.HttpContext;
httpContext.Features.Set<IHttpRequestFeature>(new RequestFeature());
_responseFeature = new ResponseFeature();
HttpContext.Features.Set<IHttpResponseFeature>(_responseFeature);
var serverRequest = HttpContext.Request;
httpContext.Features.Set<IHttpResponseFeature>(_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<HttpResponseMessage> 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<IHttpResponseFeature>().ReasonPhrase;
response.StatusCode = (HttpStatusCode)httpContext.Response.StatusCode;
response.ReasonPhrase = httpContext.Features.Get<IHttpResponseFeature>().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<string>)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);
}
}
}

View File

@ -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<Context> _application;
public TestServer(WebHostBuilder builder)
{
var hostingEngine = builder.UseServer(this).Build();
_httpContextFactory = hostingEngine.ApplicationServices.GetService<IHttpContextFactory>();
_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);
}
/// <summary>
@ -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<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.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<Context> _application;
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)
{
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<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)
{
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<Context> _application;
private TaskCompletionSource<WebSocket> _clientWebSocketTcs;
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 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>();
_application = application;
// HttpContext
HttpContext = _factory.Create(new FeatureCollection());
Context = _application.CreateContext(new FeatureCollection());
var httpContext = Context.HttpContext;
// Request
HttpContext.Features.Set<IHttpRequestFeature>(new RequestFeature());
var request = HttpContext.Request;
httpContext.Features.Set<IHttpRequestFeature>(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<IHttpResponseFeature>(new ResponseFeature());
var response = HttpContext.Response;
httpContext.Features.Set<IHttpResponseFeature>(new ResponseFeature());
var response = httpContext.Response;
response.Body = Stream.Null;
response.StatusCode = 200;
// WebSocket
HttpContext.Features.Set<IHttpWebSocketFeature>(this);
httpContext.Features.Set<IHttpWebSocketFeature>(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<WebSocket> 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);

View File

@ -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<TContext>(IHttpApplication<TContext> 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()

View File

@ -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<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.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<HttpResponseMessage> 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<InvalidOperationException>(() => 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<HttpRequestException>(() => response.Content.ReadAsStringAsync());
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.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<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.Null(listener.EndRequest?.HttpContext);