New IServer design with IHttpApplication added #395
This commit is contained in:
parent
f600604140
commit
8c256a0d87
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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();
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
|
|
@ -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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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);
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue