From 1623dcfd2723eebe88d9c685f4dc9f6830a5decf Mon Sep 17 00:00:00 2001 From: David Fowler Date: Sat, 5 Jan 2019 15:33:22 -0800 Subject: [PATCH 1/3] Reuse HttpContext object per connection. - Today in Kestrel, we reuse the IFeatureCollection per connection and per Http2Stream. This PR aims to take advantage of that same technique and affinitize the HttpContext and friends so that they are only allocated per connection. - ReusableHttpContext and friends mimic the functionality of DefaultHttpContext but is sealed and has no overridable methods. --- src/Http/Http/src/HttpContextFactory.cs | 10 +- .../Http/src/Internal/DefaultHttpRequest.cs | 2 +- .../Http/src/Internal/DefaultHttpResponse.cs | 2 +- .../src/Internal/ReusableConnectionInfo.cs | 89 +++++++++ .../Http/src/Internal/ReusableHttpContext.cs | 165 +++++++++++++++++ .../Http/src/Internal/ReusableHttpRequest.cs | 173 ++++++++++++++++++ .../Http/src/Internal/ReusableHttpResponse.cs | 138 ++++++++++++++ .../src/Internal/ReusableWebSocketManager.cs | 71 +++++++ .../Core/src/Internal/Http/HttpProtocol.cs | 22 ++- .../src/Internal/KestrelHttpContextFactory.cs | 36 ++++ .../src/WebHostBuilderKestrelExtensions.cs | 2 + 11 files changed, 703 insertions(+), 7 deletions(-) create mode 100644 src/Http/Http/src/Internal/ReusableConnectionInfo.cs create mode 100644 src/Http/Http/src/Internal/ReusableHttpContext.cs create mode 100644 src/Http/Http/src/Internal/ReusableHttpRequest.cs create mode 100644 src/Http/Http/src/Internal/ReusableHttpResponse.cs create mode 100644 src/Http/Http/src/Internal/ReusableWebSocketManager.cs create mode 100644 src/Servers/Kestrel/Core/src/Internal/KestrelHttpContextFactory.cs diff --git a/src/Http/Http/src/HttpContextFactory.cs b/src/Http/Http/src/HttpContextFactory.cs index 8236a388a564..8c54de2643a6 100644 --- a/src/Http/Http/src/HttpContextFactory.cs +++ b/src/Http/Http/src/HttpContextFactory.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Internal; using Microsoft.Extensions.Options; namespace Microsoft.AspNetCore.Http @@ -35,7 +36,7 @@ public HttpContext Create(IFeatureCollection featureCollection) throw new ArgumentNullException(nameof(featureCollection)); } - var httpContext = new DefaultHttpContext(featureCollection); + var httpContext = CreateHttpContext(featureCollection); if (_httpContextAccessor != null) { _httpContextAccessor.HttpContext = httpContext; @@ -47,6 +48,11 @@ public HttpContext Create(IFeatureCollection featureCollection) return httpContext; } + protected virtual HttpContext CreateHttpContext(IFeatureCollection featureCollection) + { + return new ReusableHttpContext(featureCollection); + } + public void Dispose(HttpContext httpContext) { if (_httpContextAccessor != null) @@ -55,4 +61,4 @@ public void Dispose(HttpContext httpContext) } } } -} \ No newline at end of file +} diff --git a/src/Http/Http/src/Internal/DefaultHttpRequest.cs b/src/Http/Http/src/Internal/DefaultHttpRequest.cs index e2512f60dc1b..cf8ac92a3b5f 100644 --- a/src/Http/Http/src/Internal/DefaultHttpRequest.cs +++ b/src/Http/Http/src/Internal/DefaultHttpRequest.cs @@ -171,4 +171,4 @@ struct FeatureInterfaces public IRouteValuesFeature RouteValues; } } -} \ No newline at end of file +} diff --git a/src/Http/Http/src/Internal/DefaultHttpResponse.cs b/src/Http/Http/src/Internal/DefaultHttpResponse.cs index 3ca05035f5c0..6a812426d850 100644 --- a/src/Http/Http/src/Internal/DefaultHttpResponse.cs +++ b/src/Http/Http/src/Internal/DefaultHttpResponse.cs @@ -136,4 +136,4 @@ struct FeatureInterfaces public IResponseCookiesFeature Cookies; } } -} \ No newline at end of file +} diff --git a/src/Http/Http/src/Internal/ReusableConnectionInfo.cs b/src/Http/Http/src/Internal/ReusableConnectionInfo.cs new file mode 100644 index 000000000000..9891ee6cbc80 --- /dev/null +++ b/src/Http/Http/src/Internal/ReusableConnectionInfo.cs @@ -0,0 +1,89 @@ +using System; +using System.Collections.Generic; +using System.Net; +using System.Security.Cryptography.X509Certificates; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public sealed class ReusableConnectionInfo : ConnectionInfo + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _newHttpConnectionFeature = f => new HttpConnectionFeature(); + private readonly static Func _newTlsConnectionFeature = f => new TlsConnectionFeature(); + + private FeatureReferences _features; + + public ReusableConnectionInfo(IFeatureCollection features) + { + Initialize(features); + } + + public void Initialize(IFeatureCollection features) + { + _features = new FeatureReferences(features); + } + + public void Uninitialize() + { + _features = default(FeatureReferences); + } + + private IHttpConnectionFeature HttpConnectionFeature => + _features.Fetch(ref _features.Cache.Connection, _newHttpConnectionFeature); + + private ITlsConnectionFeature TlsConnectionFeature => + _features.Fetch(ref _features.Cache.TlsConnection, _newTlsConnectionFeature); + + /// + public override string Id + { + get { return HttpConnectionFeature.ConnectionId; } + set { HttpConnectionFeature.ConnectionId = value; } + } + + public override IPAddress RemoteIpAddress + { + get { return HttpConnectionFeature.RemoteIpAddress; } + set { HttpConnectionFeature.RemoteIpAddress = value; } + } + + public override int RemotePort + { + get { return HttpConnectionFeature.RemotePort; } + set { HttpConnectionFeature.RemotePort = value; } + } + + public override IPAddress LocalIpAddress + { + get { return HttpConnectionFeature.LocalIpAddress; } + set { HttpConnectionFeature.LocalIpAddress = value; } + } + + public override int LocalPort + { + get { return HttpConnectionFeature.LocalPort; } + set { HttpConnectionFeature.LocalPort = value; } + } + + public override X509Certificate2 ClientCertificate + { + get { return TlsConnectionFeature.ClientCertificate; } + set { TlsConnectionFeature.ClientCertificate = value; } + } + + public override Task GetClientCertificateAsync(CancellationToken cancellationToken = default) + { + return TlsConnectionFeature.GetClientCertificateAsync(cancellationToken); + } + + struct FeatureInterfaces + { + public IHttpConnectionFeature Connection; + public ITlsConnectionFeature TlsConnection; + } + } +} diff --git a/src/Http/Http/src/Internal/ReusableHttpContext.cs b/src/Http/Http/src/Internal/ReusableHttpContext.cs new file mode 100644 index 000000000000..29a7d7f9077c --- /dev/null +++ b/src/Http/Http/src/Internal/ReusableHttpContext.cs @@ -0,0 +1,165 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Text; +using System.Threading; +using Microsoft.AspNetCore.Http.Authentication; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Features.Authentication; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public sealed class ReusableHttpContext : HttpContext + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _newItemsFeature = f => new ItemsFeature(); + private readonly static Func _newServiceProvidersFeature = f => new ServiceProvidersFeature(); + private readonly static Func _newHttpAuthenticationFeature = f => new HttpAuthenticationFeature(); + private readonly static Func _newHttpRequestLifetimeFeature = f => new HttpRequestLifetimeFeature(); + private readonly static Func _newSessionFeature = f => new DefaultSessionFeature(); + private readonly static Func _nullSessionFeature = f => null; + private readonly static Func _newHttpRequestIdentifierFeature = f => new HttpRequestIdentifierFeature(); + + private FeatureReferences _features; + + private ReusableHttpRequest _request; + private ReusableHttpResponse _response; + + private ReusableConnectionInfo _connection; + private ReusableWebSocketManager _websockets; + + public ReusableHttpContext(IFeatureCollection features) + { + _features = new FeatureReferences(features); + _request = new ReusableHttpRequest(this); + _response = new ReusableHttpResponse(this); + } + + public void Initialize(IFeatureCollection features) + { + _features = new FeatureReferences(features); + _request.Initialize(this); + _response.Initialize(this); + _connection?.Initialize(features); + _websockets?.Initialize(features); + } + + public void Uninitialize() + { + _features = default; + + _request.Uninitialize(); + _response.Uninitialize(); + _connection?.Uninitialize(); + _websockets?.Uninitialize(); + } + + private IItemsFeature ItemsFeature => + _features.Fetch(ref _features.Cache.Items, _newItemsFeature); + + private IServiceProvidersFeature ServiceProvidersFeature => + _features.Fetch(ref _features.Cache.ServiceProviders, _newServiceProvidersFeature); + + private IHttpAuthenticationFeature HttpAuthenticationFeature => + _features.Fetch(ref _features.Cache.Authentication, _newHttpAuthenticationFeature); + + private IHttpRequestLifetimeFeature LifetimeFeature => + _features.Fetch(ref _features.Cache.Lifetime, _newHttpRequestLifetimeFeature); + + private ISessionFeature SessionFeature => + _features.Fetch(ref _features.Cache.Session, _newSessionFeature); + + private ISessionFeature SessionFeatureOrNull => + _features.Fetch(ref _features.Cache.Session, _nullSessionFeature); + + + private IHttpRequestIdentifierFeature RequestIdentifierFeature => + _features.Fetch(ref _features.Cache.RequestIdentifier, _newHttpRequestIdentifierFeature); + + public override IFeatureCollection Features => _features.Collection; + + public override HttpRequest Request => _request; + + public override HttpResponse Response => _response; + + public override ConnectionInfo Connection => _connection ?? (_connection = new ReusableConnectionInfo(_features.Collection)); + + [Obsolete("This is obsolete and will be removed in a future version. The recommended alternative is to use Microsoft.AspNetCore.Authentication.AuthenticationHttpContextExtensions. See https://go.microsoft.com/fwlink/?linkid=845470.")] + public override AuthenticationManager Authentication => throw new NotSupportedException(); + + public override WebSocketManager WebSockets => _websockets ?? (_websockets = new ReusableWebSocketManager(_features.Collection)); + + + public override ClaimsPrincipal User + { + get + { + var user = HttpAuthenticationFeature.User; + if (user == null) + { + user = new ClaimsPrincipal(new ClaimsIdentity()); + HttpAuthenticationFeature.User = user; + } + return user; + } + set { HttpAuthenticationFeature.User = value; } + } + + public override IDictionary Items + { + get { return ItemsFeature.Items; } + set { ItemsFeature.Items = value; } + } + + public override IServiceProvider RequestServices + { + get { return ServiceProvidersFeature.RequestServices; } + set { ServiceProvidersFeature.RequestServices = value; } + } + + public override CancellationToken RequestAborted + { + get { return LifetimeFeature.RequestAborted; } + set { LifetimeFeature.RequestAborted = value; } + } + + public override string TraceIdentifier + { + get { return RequestIdentifierFeature.TraceIdentifier; } + set { RequestIdentifierFeature.TraceIdentifier = value; } + } + + public override ISession Session + { + get + { + var feature = SessionFeatureOrNull; + if (feature == null) + { + throw new InvalidOperationException("Session has not been configured for this application " + + "or request."); + } + return feature.Session; + } + set + { + SessionFeature.Session = value; + } + } + + public override void Abort() + { + LifetimeFeature.Abort(); + } + + struct FeatureInterfaces + { + public IItemsFeature Items; + public IServiceProvidersFeature ServiceProviders; + public IHttpAuthenticationFeature Authentication; + public IHttpRequestLifetimeFeature Lifetime; + public ISessionFeature Session; + public IHttpRequestIdentifierFeature RequestIdentifier; + } + } +} diff --git a/src/Http/Http/src/Internal/ReusableHttpRequest.cs b/src/Http/Http/src/Internal/ReusableHttpRequest.cs new file mode 100644 index 000000000000..f491acd41c23 --- /dev/null +++ b/src/Http/Http/src/Internal/ReusableHttpRequest.cs @@ -0,0 +1,173 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Routing; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public sealed class ReusableHttpRequest : HttpRequest + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _nullRequestFeature = f => null; + private readonly static Func _newQueryFeature = f => new QueryFeature(f); + private readonly static Func _newFormFeature = r => new FormFeature(r); + private readonly static Func _newRequestCookiesFeature = f => new RequestCookiesFeature(f); + private readonly static Func _newRouteValuesFeature = f => new RouteValuesFeature(); + + private HttpContext _context; + private FeatureReferences _features; + + public ReusableHttpRequest(HttpContext context) + { + Initialize(context); + } + + public void Initialize(HttpContext context) + { + _context = context; + _features = new FeatureReferences(context.Features); + } + + public void Uninitialize() + { + _context = null; + _features = default; + } + + public override HttpContext HttpContext => _context; + + private IHttpRequestFeature HttpRequestFeature => + _features.Fetch(ref _features.Cache.Request, _nullRequestFeature); + + private IQueryFeature QueryFeature => + _features.Fetch(ref _features.Cache.Query, _newQueryFeature); + + private IFormFeature FormFeature => + _features.Fetch(ref _features.Cache.Form, this, _newFormFeature); + + private IRequestCookiesFeature RequestCookiesFeature => + _features.Fetch(ref _features.Cache.Cookies, _newRequestCookiesFeature); + + private IRouteValuesFeature RouteValuesFeature => + _features.Fetch(ref _features.Cache.RouteValues, _newRouteValuesFeature); + + public override PathString PathBase + { + get { return new PathString(HttpRequestFeature.PathBase); } + set { HttpRequestFeature.PathBase = value.Value; } + } + + public override PathString Path + { + get { return new PathString(HttpRequestFeature.Path); } + set { HttpRequestFeature.Path = value.Value; } + } + + public override QueryString QueryString + { + get { return new QueryString(HttpRequestFeature.QueryString); } + set { HttpRequestFeature.QueryString = value.Value; } + } + + public override long? ContentLength + { + get { return Headers.ContentLength; } + set { Headers.ContentLength = value; } + } + + public override Stream Body + { + get { return HttpRequestFeature.Body; } + set { HttpRequestFeature.Body = value; } + } + + public override string Method + { + get { return HttpRequestFeature.Method; } + set { HttpRequestFeature.Method = value; } + } + + public override string Scheme + { + get { return HttpRequestFeature.Scheme; } + set { HttpRequestFeature.Scheme = value; } + } + + public override bool IsHttps + { + get { return string.Equals(Constants.Https, Scheme, StringComparison.OrdinalIgnoreCase); } + set { Scheme = value ? Constants.Https : Constants.Http; } + } + + public override HostString Host + { + get { return HostString.FromUriComponent(Headers["Host"]); } + set { Headers["Host"] = value.ToUriComponent(); } + } + + public override IQueryCollection Query + { + get { return QueryFeature.Query; } + set { QueryFeature.Query = value; } + } + + public override string Protocol + { + get { return HttpRequestFeature.Protocol; } + set { HttpRequestFeature.Protocol = value; } + } + + public override IHeaderDictionary Headers + { + get { return HttpRequestFeature.Headers; } + } + + public override IRequestCookieCollection Cookies + { + get { return RequestCookiesFeature.Cookies; } + set { RequestCookiesFeature.Cookies = value; } + } + + public override string ContentType + { + get { return Headers[HeaderNames.ContentType]; } + set { Headers[HeaderNames.ContentType] = value; } + } + + public override bool HasFormContentType + { + get { return FormFeature.HasFormContentType; } + } + + public override IFormCollection Form + { + get { return FormFeature.ReadForm(); } + set { FormFeature.Form = value; } + } + + public override Task ReadFormAsync(CancellationToken cancellationToken) + { + return FormFeature.ReadFormAsync(cancellationToken); + } + + public override RouteValueDictionary RouteValues + { + get { return RouteValuesFeature.RouteValues; } + set { RouteValuesFeature.RouteValues = value; } + } + + struct FeatureInterfaces + { + public IHttpRequestFeature Request; + public IQueryFeature Query; + public IFormFeature Form; + public IRequestCookiesFeature Cookies; + public IRouteValuesFeature RouteValues; + } + } +} diff --git a/src/Http/Http/src/Internal/ReusableHttpResponse.cs b/src/Http/Http/src/Internal/ReusableHttpResponse.cs new file mode 100644 index 000000000000..fd816351a82d --- /dev/null +++ b/src/Http/Http/src/Internal/ReusableHttpResponse.cs @@ -0,0 +1,138 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public sealed class ReusableHttpResponse : HttpResponse + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _nullResponseFeature = f => null; + private readonly static Func _newResponseCookiesFeature = f => new ResponseCookiesFeature(f); + + private HttpContext _context; + private FeatureReferences _features; + + public ReusableHttpResponse(HttpContext context) + { + Initialize(context); + } + + public void Initialize(HttpContext context) + { + _context = context; + _features = new FeatureReferences(context.Features); + } + + public void Uninitialize() + { + _context = null; + _features = default; + } + + private IHttpResponseFeature HttpResponseFeature => + _features.Fetch(ref _features.Cache.Response, _nullResponseFeature); + + private IResponseCookiesFeature ResponseCookiesFeature => + _features.Fetch(ref _features.Cache.Cookies, _newResponseCookiesFeature); + + + public override HttpContext HttpContext { get { return _context; } } + + public override int StatusCode + { + get { return HttpResponseFeature.StatusCode; } + set { HttpResponseFeature.StatusCode = value; } + } + + public override IHeaderDictionary Headers + { + get { return HttpResponseFeature.Headers; } + } + + public override Stream Body + { + get { return HttpResponseFeature.Body; } + set { HttpResponseFeature.Body = value; } + } + + public override long? ContentLength + { + get { return Headers.ContentLength; } + set { Headers.ContentLength = value; } + } + + public override string ContentType + { + get + { + return Headers[HeaderNames.ContentType]; + } + set + { + if (string.IsNullOrEmpty(value)) + { + HttpResponseFeature.Headers.Remove(HeaderNames.ContentType); + } + else + { + HttpResponseFeature.Headers[HeaderNames.ContentType] = value; + } + } + } + + public override IResponseCookies Cookies + { + get { return ResponseCookiesFeature.Cookies; } + } + + public override bool HasStarted + { + get { return HttpResponseFeature.HasStarted; } + } + + public override void OnStarting(Func callback, object state) + { + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + HttpResponseFeature.OnStarting(callback, state); + } + + public override void OnCompleted(Func callback, object state) + { + if (callback == null) + { + throw new ArgumentNullException(nameof(callback)); + } + + HttpResponseFeature.OnCompleted(callback, state); + } + + public override void Redirect(string location, bool permanent) + { + if (permanent) + { + HttpResponseFeature.StatusCode = 301; + } + else + { + HttpResponseFeature.StatusCode = 302; + } + + Headers[HeaderNames.Location] = location; + } + + struct FeatureInterfaces + { + public IHttpResponseFeature Response; + public IResponseCookiesFeature Cookies; + } + } +} diff --git a/src/Http/Http/src/Internal/ReusableWebSocketManager.cs b/src/Http/Http/src/Internal/ReusableWebSocketManager.cs new file mode 100644 index 000000000000..fcc144f188dd --- /dev/null +++ b/src/Http/Http/src/Internal/ReusableWebSocketManager.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Net.WebSockets; +using System.Text; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.Net.Http.Headers; + +namespace Microsoft.AspNetCore.Http.Internal +{ + public sealed class ReusableWebSocketManager : WebSocketManager + { + // Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624 + private readonly static Func _nullRequestFeature = f => null; + private readonly static Func _nullWebSocketFeature = f => null; + + private FeatureReferences _features; + + public ReusableWebSocketManager(IFeatureCollection features) + { + Initialize(features); + } + + public void Initialize(IFeatureCollection features) + { + _features = new FeatureReferences(features); + } + + public void Uninitialize() + { + _features = default; + } + + private IHttpRequestFeature HttpRequestFeature => + _features.Fetch(ref _features.Cache.Request, _nullRequestFeature); + + private IHttpWebSocketFeature WebSocketFeature => + _features.Fetch(ref _features.Cache.WebSockets, _nullWebSocketFeature); + + public override bool IsWebSocketRequest + { + get + { + return WebSocketFeature != null && WebSocketFeature.IsWebSocketRequest; + } + } + + public override IList WebSocketRequestedProtocols + { + get + { + return ParsingHelpers.GetHeaderSplit(HttpRequestFeature.Headers, HeaderNames.WebSocketSubProtocols); + } + } + + public override Task AcceptWebSocketAsync(string subProtocol) + { + if (WebSocketFeature == null) + { + throw new NotSupportedException("WebSockets are not supported"); + } + return WebSocketFeature.AcceptAsync(new WebSocketAcceptContext() { SubProtocol = subProtocol }); + } + + struct FeatureInterfaces + { + public IHttpRequestFeature Request; + public IHttpWebSocketFeature WebSockets; + } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 773e5cc112b2..503db868df89 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -17,6 +17,7 @@ using Microsoft.AspNetCore.Hosting.Server; using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Http.Internal; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Infrastructure; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Primitives; @@ -63,6 +64,7 @@ public abstract partial class HttpProtocol : IHttpResponseControl private long _responseBytesWritten; private readonly HttpConnectionContext _context; + private ReusableHttpContext _httpContext; protected string _methodText = null; private string _scheme = null; @@ -275,6 +277,8 @@ public CancellationToken RequestAborted protected HttpResponseHeaders HttpResponseHeaders { get; } = new HttpResponseHeaders(); + internal ReusableHttpContext HttpContext => _httpContext; + public void InitializeStreams(MessageBody messageBody) { if (_streams == null) @@ -359,6 +363,8 @@ public void Reset() _responseBytesWritten = 0; + _httpContext?.Uninitialize(); + OnReset(); } @@ -535,14 +541,24 @@ private async Task ProcessRequests(IHttpApplication applicat InitializeStreams(messageBody); - var httpContext = application.CreateContext(this); + // Initialize the HttpContext before we call into the IHttpApplication + if (_httpContext is null) + { + _httpContext = new ReusableHttpContext(this); + } + else + { + _httpContext.Initialize(this); + } + + var context = application.CreateContext(this); try { KestrelEventSource.Log.RequestStart(this); // Run the application code for this request - await application.ProcessRequestAsync(httpContext); + await application.ProcessRequestAsync(context); if (!_requestAborted) { @@ -609,7 +625,7 @@ private async Task ProcessRequests(IHttpApplication applicat await FireOnCompleted(); } - application.DisposeContext(httpContext, _applicationException); + application.DisposeContext(context, _applicationException); // Even for non-keep-alive requests, try to consume the entire body to avoid RSTs. if (!_requestAborted && _requestRejectedException == null && !messageBody.IsEmpty) diff --git a/src/Servers/Kestrel/Core/src/Internal/KestrelHttpContextFactory.cs b/src/Servers/Kestrel/Core/src/Internal/KestrelHttpContextFactory.cs new file mode 100644 index 000000000000..e67a368b14da --- /dev/null +++ b/src/Servers/Kestrel/Core/src/Internal/KestrelHttpContextFactory.cs @@ -0,0 +1,36 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Http.Features; +using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; +using Microsoft.Extensions.Options; + +namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal +{ + public class KestrelHttpContextFactory : HttpContextFactory + { + public KestrelHttpContextFactory(IOptions formOptions) + : this(formOptions, httpContextAccessor: null) + { + } + + + public KestrelHttpContextFactory(IOptions formOptions, IHttpContextAccessor httpContextAccessor) : base(formOptions, httpContextAccessor) + { + } + + protected override HttpContext CreateHttpContext(IFeatureCollection featureCollection) + { + if (featureCollection is HttpProtocol protocol) + { + Debug.Assert(protocol.HttpContext != null, "The HttpContext should be initialized by the request processing loop"); + return protocol.HttpContext; + } + + // Since Kestrel is registered by default, we need to fallback to the default behavior + return base.CreateHttpContext(featureCollection); + } + } +} diff --git a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs index 62411b168d44..2b8430e6810f 100644 --- a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs +++ b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs @@ -3,6 +3,7 @@ using System; using Microsoft.AspNetCore.Hosting.Server; +using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal; @@ -32,6 +33,7 @@ public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder) services.TryAddSingleton(); services.AddTransient, KestrelServerOptionsSetup>(); + services.AddSingleton(); services.AddSingleton(); }); } From cea02170b035f0227d28f007a7a36f35dd76a793 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Tue, 8 Jan 2019 12:36:14 -0800 Subject: [PATCH 2/3] Introduce IHttpContextContainer - Allows servers to cache the HttpContext and friends across requests. - Remove KestrelHttpContextFactory --- src/Http/Http/src/HttpContextFactory.cs | 7 +++- src/Http/Http/src/IHttpContextContainer.cs | 11 ++++++ .../Core/src/Internal/Http/HttpProtocol.cs | 4 +-- .../src/Internal/KestrelHttpContextFactory.cs | 36 ------------------- .../src/WebHostBuilderKestrelExtensions.cs | 1 - 5 files changed, 19 insertions(+), 40 deletions(-) create mode 100644 src/Http/Http/src/IHttpContextContainer.cs delete mode 100644 src/Servers/Kestrel/Core/src/Internal/KestrelHttpContextFactory.cs diff --git a/src/Http/Http/src/HttpContextFactory.cs b/src/Http/Http/src/HttpContextFactory.cs index 8c54de2643a6..9d7f3a1ad187 100644 --- a/src/Http/Http/src/HttpContextFactory.cs +++ b/src/Http/Http/src/HttpContextFactory.cs @@ -48,8 +48,13 @@ public HttpContext Create(IFeatureCollection featureCollection) return httpContext; } - protected virtual HttpContext CreateHttpContext(IFeatureCollection featureCollection) + private static HttpContext CreateHttpContext(IFeatureCollection featureCollection) { + if (featureCollection is IHttpContextContainer container) + { + return container.HttpContext; + } + return new ReusableHttpContext(featureCollection); } diff --git a/src/Http/Http/src/IHttpContextContainer.cs b/src/Http/Http/src/IHttpContextContainer.cs new file mode 100644 index 000000000000..f77b36ec1de9 --- /dev/null +++ b/src/Http/Http/src/IHttpContextContainer.cs @@ -0,0 +1,11 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.AspNetCore.Http +{ + public interface IHttpContextContainer + { + HttpContext HttpContext { get; } + } +} diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index 503db868df89..a6ff5140b65e 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -24,7 +24,7 @@ namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http { - public abstract partial class HttpProtocol : IHttpResponseControl + public abstract partial class HttpProtocol : IHttpContextContainer, IHttpResponseControl { private static readonly byte[] _bytesConnectionClose = Encoding.ASCII.GetBytes("\r\nConnection: close"); private static readonly byte[] _bytesConnectionKeepAlive = Encoding.ASCII.GetBytes("\r\nConnection: keep-alive"); @@ -277,7 +277,7 @@ public CancellationToken RequestAborted protected HttpResponseHeaders HttpResponseHeaders { get; } = new HttpResponseHeaders(); - internal ReusableHttpContext HttpContext => _httpContext; + HttpContext IHttpContextContainer.HttpContext => _httpContext; public void InitializeStreams(MessageBody messageBody) { diff --git a/src/Servers/Kestrel/Core/src/Internal/KestrelHttpContextFactory.cs b/src/Servers/Kestrel/Core/src/Internal/KestrelHttpContextFactory.cs deleted file mode 100644 index e67a368b14da..000000000000 --- a/src/Servers/Kestrel/Core/src/Internal/KestrelHttpContextFactory.cs +++ /dev/null @@ -1,36 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Text; -using Microsoft.AspNetCore.Http; -using Microsoft.AspNetCore.Http.Features; -using Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http; -using Microsoft.Extensions.Options; - -namespace Microsoft.AspNetCore.Server.Kestrel.Core.Internal -{ - public class KestrelHttpContextFactory : HttpContextFactory - { - public KestrelHttpContextFactory(IOptions formOptions) - : this(formOptions, httpContextAccessor: null) - { - } - - - public KestrelHttpContextFactory(IOptions formOptions, IHttpContextAccessor httpContextAccessor) : base(formOptions, httpContextAccessor) - { - } - - protected override HttpContext CreateHttpContext(IFeatureCollection featureCollection) - { - if (featureCollection is HttpProtocol protocol) - { - Debug.Assert(protocol.HttpContext != null, "The HttpContext should be initialized by the request processing loop"); - return protocol.HttpContext; - } - - // Since Kestrel is registered by default, we need to fallback to the default behavior - return base.CreateHttpContext(featureCollection); - } - } -} diff --git a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs index 2b8430e6810f..26297563137e 100644 --- a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs +++ b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs @@ -33,7 +33,6 @@ public static IWebHostBuilder UseKestrel(this IWebHostBuilder hostBuilder) services.TryAddSingleton(); services.AddTransient, KestrelServerOptionsSetup>(); - services.AddSingleton(); services.AddSingleton(); }); } From 9aaad68fd21465b2fbd27c91f070a39a86c3d5b9 Mon Sep 17 00:00:00 2001 From: David Fowler Date: Tue, 8 Jan 2019 13:02:09 -0800 Subject: [PATCH 3/3] More cleanup --- .../Core/src/Internal/Http/HttpProtocol.cs | 27 +++++++++++-------- .../src/WebHostBuilderKestrelExtensions.cs | 1 - 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs index a6ff5140b65e..a9ee694a7dc3 100644 --- a/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs +++ b/src/Servers/Kestrel/Core/src/Internal/Http/HttpProtocol.cs @@ -277,7 +277,22 @@ public CancellationToken RequestAborted protected HttpResponseHeaders HttpResponseHeaders { get; } = new HttpResponseHeaders(); - HttpContext IHttpContextContainer.HttpContext => _httpContext; + HttpContext IHttpContextContainer.HttpContext + { + get + { + if (_httpContext is null) + { + _httpContext = new ReusableHttpContext(this); + } + else + { + _httpContext.Initialize(this); + } + + return _httpContext; + } + } public void InitializeStreams(MessageBody messageBody) { @@ -541,16 +556,6 @@ private async Task ProcessRequests(IHttpApplication applicat InitializeStreams(messageBody); - // Initialize the HttpContext before we call into the IHttpApplication - if (_httpContext is null) - { - _httpContext = new ReusableHttpContext(this); - } - else - { - _httpContext.Initialize(this); - } - var context = application.CreateContext(this); try diff --git a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs index 26297563137e..62411b168d44 100644 --- a/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs +++ b/src/Servers/Kestrel/Kestrel/src/WebHostBuilderKestrelExtensions.cs @@ -3,7 +3,6 @@ using System; using Microsoft.AspNetCore.Hosting.Server; -using Microsoft.AspNetCore.Http; using Microsoft.AspNetCore.Server.Kestrel.Core; using Microsoft.AspNetCore.Server.Kestrel.Core.Internal; using Microsoft.AspNetCore.Server.Kestrel.Transport.Abstractions.Internal;