Move request servies feature into DefaultHttpContext (#6541)

- This completely removes the per request allocation until the feature is used.
- In order to make this change viable, we need to introduce a new HttpContextFactory that can accept new services without adding 2^n constructors. As a result, this change introduces a DefaultHttpContextFactory that takes an IServiceProvider and resolves dependencies based on the needs of the DefaultHttpContext and features.
- Throw in the older HttpContextFactory constructor when the IServiceScopeFactory is null
- It also saves us from revving the feature collection version unnecessarily.
This commit is contained in:
David Fowler 2019-01-10 09:54:09 -08:00 committed by GitHub
parent 13841abd78
commit c458fe6ebe
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 283 additions and 217 deletions

View File

@ -89,13 +89,10 @@ namespace Microsoft.AspNetCore.Hosting.Internal
services.TryAddSingleton<DiagnosticListener>(listener);
services.TryAddSingleton<DiagnosticSource>(listener);
services.TryAddSingleton<IHttpContextFactory, HttpContextFactory>();
services.TryAddSingleton<IHttpContextFactory, DefaultHttpContextFactory>();
services.TryAddScoped<IMiddlewareFactory, MiddlewareFactory>();
services.TryAddSingleton<IApplicationBuilderFactory, ApplicationBuilderFactory>();
// Conjure up a RequestServices
services.TryAddTransient<IStartupFilter, AutoRequestServicesStartupFilter>();
// Support UseStartup(assemblyName)
if (!string.IsNullOrEmpty(webHostOptions.StartupAssembly))
{

View File

@ -1,20 +0,0 @@
// 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 Microsoft.AspNetCore.Builder;
namespace Microsoft.AspNetCore.Hosting.Internal
{
public class AutoRequestServicesStartupFilter : IStartupFilter
{
public Action<IApplicationBuilder> Configure(Action<IApplicationBuilder> next)
{
return builder =>
{
builder.UseMiddleware<RequestServicesContainerMiddleware>();
next(builder);
};
}
}
}

View File

@ -1,50 +0,0 @@
// 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.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Hosting.Internal
{
public class RequestServicesContainerMiddleware
{
private readonly RequestDelegate _next;
private readonly IServiceScopeFactory _scopeFactory;
public RequestServicesContainerMiddleware(RequestDelegate next, IServiceScopeFactory scopeFactory)
{
if (next == null)
{
throw new ArgumentNullException(nameof(next));
}
if (scopeFactory == null)
{
throw new ArgumentNullException(nameof(scopeFactory));
}
_next = next;
_scopeFactory = scopeFactory;
}
public Task Invoke(HttpContext httpContext)
{
Debug.Assert(httpContext != null);
var features = httpContext.Features;
var servicesFeature = features.Get<IServiceProvidersFeature>();
// All done if RequestServices is set
if (servicesFeature?.RequestServices != null)
{
return _next.Invoke(httpContext);
}
features.Set<IServiceProvidersFeature>(new RequestServicesFeature(httpContext, _scopeFactory));
return _next.Invoke(httpContext);
}
}
}

View File

@ -272,13 +272,11 @@ namespace Microsoft.AspNetCore.Hosting
services.AddSingleton<DiagnosticSource>(listener);
services.AddTransient<IApplicationBuilderFactory, ApplicationBuilderFactory>();
services.AddTransient<IHttpContextFactory, HttpContextFactory>();
services.AddTransient<IHttpContextFactory, DefaultHttpContextFactory>();
services.AddScoped<IMiddlewareFactory, MiddlewareFactory>();
services.AddOptions();
services.AddLogging();
// Conjure up a RequestServices
services.AddTransient<IStartupFilter, AutoRequestServicesStartupFilter>();
services.AddTransient<IServiceProviderFactory<IServiceCollection>, DefaultServiceProviderFactory>();
if (!string.IsNullOrEmpty(_options.StartupAssembly))

View File

@ -1,122 +0,0 @@
// Copyright (c) .NET Foundation. All rights reserved.
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Builder.Internal;
using Microsoft.AspNetCore.Hosting.Internal;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace Microsoft.AspNetCore.Hosting.Tests
{
public class RequestServicesContainerMiddlewareTests
{
[Fact]
public async Task RequestServicesAreSet()
{
var serviceProvider = new ServiceCollection()
.BuildServiceProvider();
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
var middleware = new RequestServicesContainerMiddleware(
ctx => Task.CompletedTask,
scopeFactory);
var context = new DefaultHttpContext();
await middleware.Invoke(context);
Assert.NotNull(context.RequestServices);
}
[Fact]
public async Task RequestServicesAreNotOverwrittenIfAlreadySet()
{
var serviceProvider = new ServiceCollection()
.BuildServiceProvider();
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
var middleware = new RequestServicesContainerMiddleware(
ctx => Task.CompletedTask,
scopeFactory);
var context = new DefaultHttpContext();
context.RequestServices = serviceProvider;
await middleware.Invoke(context);
Assert.Same(serviceProvider, context.RequestServices);
}
[Fact]
public async Task RequestServicesAreDisposedOnCompleted()
{
var serviceProvider = new ServiceCollection()
.AddTransient<DisposableThing>()
.BuildServiceProvider();
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
DisposableThing instance = null;
var middleware = new RequestServicesContainerMiddleware(
ctx =>
{
instance = ctx.RequestServices.GetRequiredService<DisposableThing>();
return Task.CompletedTask;
},
scopeFactory);
var context = new DefaultHttpContext();
var responseFeature = new TestHttpResponseFeature();
context.Features.Set<IHttpResponseFeature>(responseFeature);
await middleware.Invoke(context);
Assert.NotNull(context.RequestServices);
Assert.Single(responseFeature.CompletedCallbacks);
var callback = responseFeature.CompletedCallbacks[0];
await callback.callback(callback.state);
Assert.Null(context.RequestServices);
Assert.True(instance.Disposed);
}
private class DisposableThing : IDisposable
{
public bool Disposed { get; set; }
public void Dispose()
{
Disposed = true;
}
}
private class TestHttpResponseFeature : IHttpResponseFeature
{
public List<(Func<object, Task> callback, object state)> CompletedCallbacks = new List<(Func<object, Task> callback, object state)>();
public int StatusCode { get; set; }
public string ReasonPhrase { get; set; }
public IHeaderDictionary Headers { get; set; } = new HeaderDictionary();
public Stream Body { get; set; }
public bool HasStarted => false;
public void OnCompleted(Func<object, Task> callback, object state)
{
CompletedCallbacks.Add((callback, state));
}
public void OnStarting(Func<object, Task> callback, object state)
{
}
}
}
}

View File

@ -8,6 +8,7 @@ using System.Threading;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Features.Authentication;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Http
{
@ -15,7 +16,7 @@ namespace Microsoft.AspNetCore.Http
{
// Lambdas hoisted to static readonly fields to improve inlining https://github.com/dotnet/roslyn/issues/13624
private readonly static Func<IFeatureCollection, IItemsFeature> _newItemsFeature = f => new ItemsFeature();
private readonly static Func<IFeatureCollection, IServiceProvidersFeature> _newServiceProvidersFeature = f => new ServiceProvidersFeature();
private readonly static Func<DefaultHttpContext, IServiceProvidersFeature> _newServiceProvidersFeature = context => new RequestServicesFeature(context, context.ServiceScopeFactory);
private readonly static Func<IFeatureCollection, IHttpAuthenticationFeature> _newHttpAuthenticationFeature = f => new HttpAuthenticationFeature();
private readonly static Func<IFeatureCollection, IHttpRequestLifetimeFeature> _newHttpRequestLifetimeFeature = f => new HttpRequestLifetimeFeature();
private readonly static Func<IFeatureCollection, ISessionFeature> _newSessionFeature = f => new DefaultSessionFeature();
@ -64,11 +65,13 @@ namespace Microsoft.AspNetCore.Http
public FormOptions FormOptions { get; set; }
public IServiceScopeFactory ServiceScopeFactory { get; set; }
private IItemsFeature ItemsFeature =>
_features.Fetch(ref _features.Cache.Items, _newItemsFeature);
private IServiceProvidersFeature ServiceProvidersFeature =>
_features.Fetch(ref _features.Cache.ServiceProviders, _newServiceProvidersFeature);
_features.Fetch(ref _features.Cache.ServiceProviders, this, _newServiceProvidersFeature);
private IHttpAuthenticationFeature HttpAuthenticationFeature =>
_features.Fetch(ref _features.Cache.Authentication, _newHttpAuthenticationFeature);

View File

@ -0,0 +1,64 @@
// 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 Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Http
{
public class DefaultHttpContextFactory : IHttpContextFactory
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly FormOptions _formOptions;
private readonly IServiceScopeFactory _serviceScopeFactory;
// This takes the IServiceProvider because it needs to support an ever expanding
// set of services that flow down into HttpContext features
public DefaultHttpContextFactory(IServiceProvider serviceProvider)
{
// May be null
_httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
_formOptions = serviceProvider.GetRequiredService<IOptions<FormOptions>>().Value;
_serviceScopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
}
public HttpContext Create(IFeatureCollection featureCollection)
{
if (featureCollection == null)
{
throw new ArgumentNullException(nameof(featureCollection));
}
var httpContext = CreateHttpContext(featureCollection);
if (_httpContextAccessor != null)
{
_httpContextAccessor.HttpContext = httpContext;
}
httpContext.FormOptions = _formOptions;
httpContext.ServiceScopeFactory = _serviceScopeFactory;
return httpContext;
}
private static DefaultHttpContext CreateHttpContext(IFeatureCollection featureCollection)
{
if (featureCollection is IHttpContextContainer container)
{
return container.HttpContext;
}
return new DefaultHttpContext(featureCollection);
}
public void Dispose(HttpContext httpContext)
{
if (_httpContextAccessor != null)
{
_httpContextAccessor.HttpContext = null;
}
}
}
}

View File

@ -2,12 +2,9 @@
// Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.Diagnostics;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
namespace Microsoft.AspNetCore.Hosting.Internal
namespace Microsoft.AspNetCore.Http.Features
{
public class RequestServicesFeature : IServiceProvidersFeature, IDisposable
{
@ -19,7 +16,6 @@ namespace Microsoft.AspNetCore.Hosting.Internal
public RequestServicesFeature(HttpContext context, IServiceScopeFactory scopeFactory)
{
Debug.Assert(scopeFactory != null);
_context = context;
_scopeFactory = scopeFactory;
}
@ -28,7 +24,7 @@ namespace Microsoft.AspNetCore.Hosting.Internal
{
get
{
if (!_requestServicesSet)
if (!_requestServicesSet && _scopeFactory != null)
{
_context.Response.RegisterForDispose(this);
_scope = _scopeFactory.CreateScope();
@ -52,4 +48,4 @@ namespace Microsoft.AspNetCore.Hosting.Internal
_requestServices = null;
}
}
}
}

View File

@ -3,29 +3,47 @@
using System;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.AspNetCore.Http.Internal;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
namespace Microsoft.AspNetCore.Http
{
[Obsolete("This is obsolete and will be removed in a future version. Use DefaultHttpContextFactory instead.")]
public class HttpContextFactory : IHttpContextFactory
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly FormOptions _formOptions;
private readonly IServiceScopeFactory _serviceScopeFactory;
public HttpContextFactory(IOptions<FormOptions> formOptions)
: this(formOptions, httpContextAccessor: null)
: this(formOptions, serviceScopeFactory: null)
{
}
public HttpContextFactory(IOptions<FormOptions> formOptions, IServiceScopeFactory serviceScopeFactory)
: this(formOptions, serviceScopeFactory, httpContextAccessor: null)
{
}
public HttpContextFactory(IOptions<FormOptions> formOptions, IHttpContextAccessor httpContextAccessor)
: this(formOptions, serviceScopeFactory: null, httpContextAccessor: httpContextAccessor)
{
}
public HttpContextFactory(IOptions<FormOptions> formOptions, IServiceScopeFactory serviceScopeFactory, IHttpContextAccessor httpContextAccessor)
{
if (formOptions == null)
{
throw new ArgumentNullException(nameof(formOptions));
}
if (serviceScopeFactory == null)
{
throw new ArgumentNullException(nameof(serviceScopeFactory));
}
_formOptions = formOptions.Value;
_serviceScopeFactory = serviceScopeFactory;
_httpContextAccessor = httpContextAccessor;
}
@ -43,6 +61,7 @@ namespace Microsoft.AspNetCore.Http
}
httpContext.FormOptions = _formOptions;
httpContext.ServiceScopeFactory = _serviceScopeFactory;
return httpContext;
}

View File

@ -0,0 +1,87 @@
// 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.IO;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;
namespace Microsoft.AspNetCore.Http
{
public class DefaultHttpContextFactoryTests
{
[Fact]
public void CreateHttpContextSetsHttpContextAccessor()
{
// Arrange
var services = new ServiceCollection()
.AddOptions()
.AddHttpContextAccessor()
.BuildServiceProvider();
var accessor = services.GetRequiredService<IHttpContextAccessor>();
var contextFactory = new DefaultHttpContextFactory(services);
// Act
var context = contextFactory.Create(new FeatureCollection());
// Assert
Assert.Same(context, accessor.HttpContext);
}
[Fact]
public void DisposeHttpContextSetsHttpContextAccessorToNull()
{
// Arrange
var services = new ServiceCollection()
.AddOptions()
.AddHttpContextAccessor()
.BuildServiceProvider();
var accessor = services.GetRequiredService<IHttpContextAccessor>();
var contextFactory = new DefaultHttpContextFactory(services);
// Act
var context = contextFactory.Create(new FeatureCollection());
// Assert
Assert.Same(context, accessor.HttpContext);
contextFactory.Dispose(context);
Assert.Null(accessor.HttpContext);
}
[Fact]
public void AllowsCreatingContextWithoutSettingAccessor()
{
// Arrange
var services = new ServiceCollection()
.AddOptions()
.BuildServiceProvider();
var contextFactory = new DefaultHttpContextFactory(services);
// Act & Assert
var context = contextFactory.Create(new FeatureCollection());
contextFactory.Dispose(context);
}
[Fact]
public void SetsDefaultPropertiesOnHttpContext()
{
// Arrange
var services = new ServiceCollection()
.AddOptions()
.BuildServiceProvider();
var contextFactory = new DefaultHttpContextFactory(services);
// Act & Assert
var context = contextFactory.Create(new FeatureCollection()) as DefaultHttpContext;
Assert.NotNull(context);
Assert.NotNull(context.FormOptions);
Assert.NotNull(context.ServiceScopeFactory);
Assert.Same(services.GetRequiredService<IServiceScopeFactory>(), context.ServiceScopeFactory);
}
}
}

View File

@ -3,17 +3,19 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net.WebSockets;
using System.Reflection;
using System.Runtime.CompilerServices;
using System.Security.Claims;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Xunit;
namespace System.IO.Pipelines.Tests
namespace Microsoft.AspNetCore.Http
{
public class DefaultHttpContextTests
{
@ -188,6 +190,48 @@ namespace System.IO.Pipelines.Tests
Assert.NotEqual(3, newFeatures.Count());
}
[Fact]
public void RequestServicesAreNotOverwrittenIfAlreadySet()
{
var serviceProvider = new ServiceCollection()
.BuildServiceProvider();
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
var context = new DefaultHttpContext();
context.ServiceScopeFactory = scopeFactory;
context.RequestServices = serviceProvider;
Assert.Same(serviceProvider, context.RequestServices);
}
[Fact]
public async Task RequestServicesAreDisposedOnCompleted()
{
var serviceProvider = new ServiceCollection()
.AddTransient<DisposableThing>()
.BuildServiceProvider();
var scopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>();
DisposableThing instance = null;
var context = new DefaultHttpContext();
context.ServiceScopeFactory = scopeFactory;
var responseFeature = new TestHttpResponseFeature();
context.Features.Set<IHttpResponseFeature>(responseFeature);
Assert.NotNull(context.RequestServices);
Assert.Single(responseFeature.CompletedCallbacks);
instance = context.RequestServices.GetRequiredService<DisposableThing>();
var callback = responseFeature.CompletedCallbacks[0];
await callback.callback(callback.state);
Assert.Null(context.RequestServices);
Assert.True(instance.Disposed);
}
void TestAllCachedFeaturesAreNull(HttpContext context, IFeatureCollection features)
{
TestCachedFeaturesAreNull(context, features);
@ -237,7 +281,7 @@ namespace System.IO.Pipelines.Tests
var fields = type
.GetFields(BindingFlags.NonPublic | BindingFlags.Instance)
.Where(f => f.FieldType.GetTypeInfo().IsInterface);
.Where(f => f.FieldType.GetTypeInfo().IsInterface && f.GetCustomAttribute<CompilerGeneratedAttribute>() == null);
foreach (var field in fields)
{
@ -281,6 +325,36 @@ namespace System.IO.Pipelines.Tests
return context;
}
private class DisposableThing : IDisposable
{
public bool Disposed { get; set; }
public void Dispose()
{
Disposed = true;
}
}
private class TestHttpResponseFeature : IHttpResponseFeature
{
public List<(Func<object, Task> callback, object state)> CompletedCallbacks = new List<(Func<object, Task> callback, object state)>();
public int StatusCode { get; set; }
public string ReasonPhrase { get; set; }
public IHeaderDictionary Headers { get; set; } = new HeaderDictionary();
public Stream Body { get; set; }
public bool HasStarted => false;
public void OnCompleted(Func<object, Task> callback, object state)
{
CompletedCallbacks.Add((callback, state));
}
public void OnStarting(Func<object, Task> callback, object state)
{
}
}
private class TestSession : ISession
{
private Dictionary<string, byte[]> _store

View File

@ -1,9 +1,11 @@
#pragma warning disable CS0618 // Type or member is obsolete
// 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.IO;
using Microsoft.AspNetCore.Http.Features;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Options;
using Xunit;
@ -11,12 +13,24 @@ namespace Microsoft.AspNetCore.Http
{
public class HttpContextFactoryTests
{
[Fact]
public void ConstructorWithoutServiceScopeFactoryThrows()
{
// Arrange
var accessor = new HttpContextAccessor();
var exception1 = Assert.Throws<ArgumentNullException>(() => new HttpContextFactory(Options.Create(new FormOptions()), accessor));
var exception2 = Assert.Throws<ArgumentNullException>(() => new HttpContextFactory(Options.Create(new FormOptions())));
Assert.Equal("serviceScopeFactory", exception1.ParamName);
Assert.Equal("serviceScopeFactory", exception2.ParamName);
}
[Fact]
public void CreateHttpContextSetsHttpContextAccessor()
{
// Arrange
var accessor = new HttpContextAccessor();
var contextFactory = new HttpContextFactory(Options.Create(new FormOptions()), accessor);
var contextFactory = new HttpContextFactory(Options.Create(new FormOptions()), new MyServiceScopeFactory(), accessor);
// Act
var context = contextFactory.Create(new FeatureCollection());
@ -30,7 +44,7 @@ namespace Microsoft.AspNetCore.Http
{
// Arrange
var accessor = new HttpContextAccessor();
var contextFactory = new HttpContextFactory(Options.Create(new FormOptions()), accessor);
var contextFactory = new HttpContextFactory(Options.Create(new FormOptions()), new MyServiceScopeFactory(), accessor);
// Act
var context = contextFactory.Create(new FeatureCollection());
@ -47,11 +61,17 @@ namespace Microsoft.AspNetCore.Http
public void AllowsCreatingContextWithoutSettingAccessor()
{
// Arrange
var contextFactory = new HttpContextFactory(Options.Create(new FormOptions()));
var contextFactory = new HttpContextFactory(Options.Create(new FormOptions()), new MyServiceScopeFactory());
// Act & Assert
var context = contextFactory.Create(new FeatureCollection());
contextFactory.Dispose(context);
}
private class MyServiceScopeFactory : IServiceScopeFactory
{
public IServiceScope CreateScope() => null;
}
}
}
}
#pragma warning restore CS0618 // Type or member is obsolete