diff --git a/src/Microsoft.AspNetCore.Http.Features/FeatureReferences.cs b/src/Microsoft.AspNetCore.Http.Features/FeatureReferences.cs index 38bd2ec2..461327ba 100644 --- a/src/Microsoft.AspNetCore.Http.Features/FeatureReferences.cs +++ b/src/Microsoft.AspNetCore.Http.Features/FeatureReferences.cs @@ -15,6 +15,13 @@ public FeatureReferences(IFeatureCollection collection) Revision = collection.Revision; } + public FeatureReferences(IFeatureCollection collection, int revision) + { + Collection = collection; + Cache = default(TCache); + Revision = revision; + } + public IFeatureCollection Collection { get; private set; } public int Revision { get; private set; } @@ -63,6 +70,26 @@ public TFeature Fetch( return cached ?? UpdateCached(ref cached, state, factory, revision, flush); } + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public TFeature Fetch( + ref TFeature cached, + TState1 state1, + TState2 state2, + Func factory) where TFeature : class + { + var flush = false; + var revision = Collection.Revision; + if (Revision != revision) + { + // Clear cached value to force call to UpdateCached + cached = null; + // Collection changed, clear whole feature cache + flush = true; + } + + return cached ?? UpdateCached(ref cached, state1, state2, factory, revision, flush); + } + // Update and cache clearing logic, when the fast-path in Fetch isn't applicable private TFeature UpdateCached(ref TFeature cached, TState state, Func factory, int revision, bool flush) where TFeature : class { @@ -92,6 +119,35 @@ private TFeature UpdateCached(ref TFeature cached, TState stat return cached; } + // Update and cache clearing logic, when the fast-path in Fetch isn't applicable + private TFeature UpdateCached(ref TFeature cached, TState1 state1, TState2 state2, Func factory, int revision, bool flush) where TFeature : class + { + if (flush) + { + // Collection detected as changed, clear cache + Cache = default(TCache); + } + + cached = Collection.Get(); + if (cached == null) + { + // Item not in collection, create it with factory + cached = factory(state1, state2); + // Add item to IFeatureCollection + Collection.Set(cached); + // Revision changed by .Set, update revision to new value + Revision = Collection.Revision; + } + else if (flush) + { + // Cache was cleared, but item retrived from current Collection for version + // so use passed in revision rather than making another virtual call + Revision = revision; + } + + return cached; + } + public TFeature Fetch(ref TFeature cached, Func factory) where TFeature : class => Fetch(ref cached, Collection, factory); } diff --git a/src/Microsoft.AspNetCore.Http/DefaultHttpContext.cs b/src/Microsoft.AspNetCore.Http/DefaultHttpContext.cs index d02ad632..42d32e8f 100644 --- a/src/Microsoft.AspNetCore.Http/DefaultHttpContext.cs +++ b/src/Microsoft.AspNetCore.Http/DefaultHttpContext.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Generic; +using System.Runtime.CompilerServices; using System.Security.Claims; using System.Threading; using Microsoft.AspNetCore.Http.Authentication; @@ -24,6 +25,7 @@ public class DefaultHttpContext : HttpContext private readonly static Func _nullSessionFeature = f => null; private readonly static Func _newHttpRequestIdentifierFeature = f => new HttpRequestIdentifierFeature(); + private FormOptions _formOptions; private FeatureReferences _features; private HttpRequest _request; @@ -44,15 +46,26 @@ public DefaultHttpContext() } public DefaultHttpContext(IFeatureCollection features) + : this(features, FormOptions.Default) { - Initialize(features); } - public virtual void Initialize(IFeatureCollection features) + public DefaultHttpContext(IFeatureCollection features, FormOptions formOptions) + { + Initialize(features, formOptions); + } + + public virtual void Initialize(IFeatureCollection features, FormOptions formOptions) { _features = new FeatureReferences(features); _request = InitializeHttpRequest(); _response = InitializeHttpResponse(); + _formOptions = formOptions; + } + + public virtual void Initialize(IFeatureCollection features) + { + Initialize(features, FormOptions.Default); } public virtual void Uninitialize() @@ -85,6 +98,7 @@ public virtual void Uninitialize() UninitializeWebSocketManager(_websockets); _websockets = null; } + _formOptions = null; } private IItemsFeature ItemsFeature => @@ -185,18 +199,43 @@ public override ISession Session } } - - public override void Abort() { LifetimeFeature.Abort(); } + [MethodImpl(MethodImplOptions.NoInlining)] + private void InitializeRequestResponse() + { + var revision = _features.Revision; + var collection = _features.Collection; + + _request = new DefaultHttpRequest(this, revision, collection, _formOptions); + _response = new DefaultHttpResponse(this, revision, collection); + } + + protected virtual HttpRequest InitializeHttpRequest() + { + if (_request == null) + { + InitializeRequestResponse(); + } + + return _request; + } - protected virtual HttpRequest InitializeHttpRequest() => new DefaultHttpRequest(this); protected virtual void UninitializeHttpRequest(HttpRequest instance) { } - protected virtual HttpResponse InitializeHttpResponse() => new DefaultHttpResponse(this); + protected virtual HttpResponse InitializeHttpResponse() + { + if (_response == null) + { + InitializeRequestResponse(); + } + + return _response; + } + protected virtual void UninitializeHttpResponse(HttpResponse instance) { } protected virtual ConnectionInfo InitializeConnectionInfo() => new DefaultConnectionInfo(Features); diff --git a/src/Microsoft.AspNetCore.Http/Features/FormFeature.cs b/src/Microsoft.AspNetCore.Http/Features/FormFeature.cs index f091e3b1..295eb5d1 100644 --- a/src/Microsoft.AspNetCore.Http/Features/FormFeature.cs +++ b/src/Microsoft.AspNetCore.Http/Features/FormFeature.cs @@ -15,8 +15,6 @@ namespace Microsoft.AspNetCore.Http.Features { public class FormFeature : IFormFeature { - private static readonly FormOptions DefaultFormOptions = new FormOptions(); - private readonly HttpRequest _request; private readonly FormOptions _options; private Task _parsedFormTask; @@ -32,7 +30,7 @@ public FormFeature(IFormCollection form) Form = form; } public FormFeature(HttpRequest request) - : this(request, DefaultFormOptions) + : this(request, FormOptions.Default) { } diff --git a/src/Microsoft.AspNetCore.Http/Features/FormOptions.cs b/src/Microsoft.AspNetCore.Http/Features/FormOptions.cs index 17e521b2..a1d3dcb5 100644 --- a/src/Microsoft.AspNetCore.Http/Features/FormOptions.cs +++ b/src/Microsoft.AspNetCore.Http/Features/FormOptions.cs @@ -8,6 +8,8 @@ namespace Microsoft.AspNetCore.Http.Features { public class FormOptions { + internal static readonly FormOptions Default = new FormOptions(); + public const int DefaultMemoryBufferThreshold = 1024 * 64; public const int DefaultBufferBodyLengthLimit = 1024 * 1024 * 128; public const int DefaultMultipartBoundaryLengthLimit = 128; diff --git a/src/Microsoft.AspNetCore.Http/HttpContextFactory.cs b/src/Microsoft.AspNetCore.Http/HttpContextFactory.cs index 8236a388..78182846 100644 --- a/src/Microsoft.AspNetCore.Http/HttpContextFactory.cs +++ b/src/Microsoft.AspNetCore.Http/HttpContextFactory.cs @@ -24,7 +24,7 @@ public HttpContextFactory(IOptions formOptions, IHttpContextAccesso throw new ArgumentNullException(nameof(formOptions)); } - _formOptions = formOptions.Value; + _formOptions = formOptions.Value ?? FormOptions.Default; _httpContextAccessor = httpContextAccessor; } @@ -35,15 +35,12 @@ public HttpContext Create(IFeatureCollection featureCollection) throw new ArgumentNullException(nameof(featureCollection)); } - var httpContext = new DefaultHttpContext(featureCollection); + var httpContext = new DefaultHttpContext(featureCollection, _formOptions); if (_httpContextAccessor != null) { _httpContextAccessor.HttpContext = httpContext; } - var formFeature = new FormFeature(httpContext.Request, _formOptions); - featureCollection.Set(formFeature); - return httpContext; } diff --git a/src/Microsoft.AspNetCore.Http/Internal/DefaultHttpRequest.cs b/src/Microsoft.AspNetCore.Http/Internal/DefaultHttpRequest.cs index f216475d..039c7472 100644 --- a/src/Microsoft.AspNetCore.Http/Internal/DefaultHttpRequest.cs +++ b/src/Microsoft.AspNetCore.Http/Internal/DefaultHttpRequest.cs @@ -15,26 +15,39 @@ public class DefaultHttpRequest : 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 _newFormFeature = (r, o) => new FormFeature(r, o ?? FormOptions.Default); private readonly static Func _newRequestCookiesFeature = f => new RequestCookiesFeature(f); + private FormOptions _formOptions; private HttpContext _context; private FeatureReferences _features; public DefaultHttpRequest(HttpContext context) + : this (context, context.Features.Revision, context.Features, FormOptions.Default) { - Initialize(context); } - public virtual void Initialize(HttpContext context) + internal DefaultHttpRequest(HttpContext context, int revision, IFeatureCollection features, FormOptions formOptions) { _context = context; - _features = new FeatureReferences(context.Features); + Initialize(features, revision, formOptions); + } + + public virtual void Initialize(HttpContext context) + { + Initialize(context.Features, context.Features.Revision, FormOptions.Default); + } + + private void Initialize(IFeatureCollection features, int revision, FormOptions formOptions) + { + _features = new FeatureReferences(features, revision); + _formOptions = formOptions; } public virtual void Uninitialize() { _context = null; + _formOptions = null; _features = default(FeatureReferences); } @@ -47,7 +60,7 @@ public virtual void Uninitialize() _features.Fetch(ref _features.Cache.Query, _newQueryFeature); private IFormFeature FormFeature => - _features.Fetch(ref _features.Cache.Form, this, _newFormFeature); + _features.Fetch(ref _features.Cache.Form, this, _formOptions, _newFormFeature); private IRequestCookiesFeature RequestCookiesFeature => _features.Fetch(ref _features.Cache.Cookies, _newRequestCookiesFeature); diff --git a/src/Microsoft.AspNetCore.Http/Internal/DefaultHttpResponse.cs b/src/Microsoft.AspNetCore.Http/Internal/DefaultHttpResponse.cs index 3ca05035..96a1858d 100644 --- a/src/Microsoft.AspNetCore.Http/Internal/DefaultHttpResponse.cs +++ b/src/Microsoft.AspNetCore.Http/Internal/DefaultHttpResponse.cs @@ -19,14 +19,25 @@ public class DefaultHttpResponse : HttpResponse private FeatureReferences _features; public DefaultHttpResponse(HttpContext context) + : this(context, context.Features.Revision, context.Features) { - Initialize(context); + } + + internal DefaultHttpResponse(HttpContext context, int revision, IFeatureCollection features) + { + _context = context; + Initialize(features, revision); } public virtual void Initialize(HttpContext context) { _context = context; - _features = new FeatureReferences(context.Features); + Initialize(context.Features, context.Features.Revision); + } + + private void Initialize(IFeatureCollection features, int revision) + { + _features = new FeatureReferences(features, revision); } public virtual void Uninitialize() diff --git a/test/Microsoft.AspNetCore.Http.Tests/DefaultHttpContextTests.cs b/test/Microsoft.AspNetCore.Http.Tests/DefaultHttpContextTests.cs index 33f73cf1..6c00ef72 100644 --- a/test/Microsoft.AspNetCore.Http.Tests/DefaultHttpContextTests.cs +++ b/test/Microsoft.AspNetCore.Http.Tests/DefaultHttpContextTests.cs @@ -159,6 +159,8 @@ public void UpdateFeatures_ClearsCachedFeatures() // featurecollection is set. all cached interfaces are null. var context = new DefaultHttpContext(features); + // Trigger initalization + Assert.NotNull(context.Request); TestAllCachedFeaturesAreNull(context, features); Assert.Equal(3, features.Count()); @@ -179,6 +181,8 @@ public void UpdateFeatures_ClearsCachedFeatures() // featurecollection is set to newFeatures. all cached interfaces are null. context.Initialize(newFeatures); + // Trigger initalization + Assert.NotNull(context.Request); TestAllCachedFeaturesAreNull(context, newFeatures); Assert.Equal(3, newFeatures.Count());