ASP.NET Core管道详解[2]: HttpContext本质论
ASP.NET Core请求处理管道由一个服务器和一组有序排列的中间件构成,所有中间件针对请求的处理都在通过HttpContext对象表示的上下文中进行。由于应用程序总是利用服务器来完成对请求的接收和响应工作,所以原始请求上下文的描述由注册的服务器类型来决定。但是ASP.NET Core需要在上层提供具有一致性的编程模型,所以我们需要一个抽象的、不依赖具体服务器类型的请求上下文描述,这就是本章着重介绍的HttpContext。[本文节选自《ASP.NET Core 3框架揭秘》第13章, 更多关于ASP.NET Core的文章请点这里]
目录
一、HttpContext
二、服务器适配
三、获取HttpContext上下文
四、HttpContext上下文的创建与释放
五、针对请求的DI容器-RequestServices
一、HttpContext
在《模拟管道实现》创建的模拟管道中,我们定义了一个简易版的HttpContext类,它只包含表示请求和响应的两个属性,实际上,真正的HttpContext具有更加丰富的成员定义。对于一个HttpContext对象来说,除了描述请求和响应的Request属性与Response属性,我们还可以通过它获取与当前请求相关的其他上下文信息,如用来表示当前请求用户的ClaimsPrincipal对象、描述当前HTTP连接的ConnectionInfo对象和用于控制Web Socket的WebSocketManager对象等。除此之外,我们还可以通过Session属性获取并控制当前会话,也可以通过TraceIdentifier属性获取或者设置调试追踪的ID。
public abstract class HttpContext { public abstract HttpRequest Request { get; } public abstract HttpResponse Response { get; } public abstract ClaimsPrincipal User { get; set; } public abstract ConnectionInfo Connection { get; } public abstract WebSocketManager WebSockets { get; } public abstract ISession Session { get; set; } public abstract string TraceIdentifier { get; set; } public abstract IDictionary<object, object> Items { get; set; } public abstract CancellationToken RequestAborted { get; set; } public abstract IServiceProvider RequestServices { get; set; } ... }
当客户端中止请求(如请求超时)时,我们可以通过RequestAborted属性返回的CancellationToken对象接收到通知,进而及时中止正在进行的请求处理操作。如果需要针对整个管道共享一些与当前上下文相关的数据,我们可以将它保存在通过Items属性表示的字典中。HttpContext的RequestServices返回的是针对当前请求的IServiceProvider对象,换句话说,该对象的生命周期与表示当前请求上下文的HttpContext对象绑定。对于一个HttpContext对象来说,表示请求和响应的Request属性与Response属性是它最重要的两个成员,请求通过如下这个抽象类HttpRequest表示。
public abstract class HttpRequest { public abstract HttpContext HttpContext { get; } public abstract string Method { get; set; } public abstract string Scheme { get; set; } public abstract bool IsHttps { get; set; } public abstract HostString Host { get; set; } public abstract PathString PathBase { get; set; } public abstract PathString Path { get; set; } public abstract QueryString QueryString { get; set; } public abstract IQueryCollection Query { get; set; } public abstract string Protocol { get; set; } public abstract IHeaderDictionary Headers { get; } public abstract IRequestCookieCollection Cookies { get; set; } public abstract string ContentType { get; set; } public abstract Stream Body { get; set; } public abstract bool HasFormContentType { get; } public abstract IFormCollection Form { get; set; } public abstract Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken); }
在了解了表示请求的抽象类HttpRequest之后,下面介绍另一个与之相对的用于描述响应的HttpResponse类型。如下面的代码片段所示,HttpResponse依然是一个抽象类,我们可以通过它定义的属性和方法来控制对请求的响应。从原则上讲,我们对请求所做的任意形式的响应都可以利用它来实现。当通过表示当前上下文的HttpContext对象得到表示响应的HttpResponse对象之后,我们不仅可以将内容写入响应消息的主体部分,还可以设置响应状态码,并添加相应的报头。
public abstract class HttpResponse { public abstract HttpContext HttpContext { get; } public abstract int StatusCode { get; set; } public abstract IHeaderDictionary Headers { get; } public abstract Stream Body { get; set; } public abstract long? ContentLength { get; set; } public abstract IResponseCookies Cookies { get; } public abstract bool HasStarted { get; } public abstract void OnStarting(Func<object, Task> callback, object state); public virtual void OnStarting(Func<Task> callback); public abstract void OnCompleted(Func<object, Task> callback, object state); public virtual void RegisterForDispose(IDisposable disposable); public virtual void OnCompleted(Func<Task> callback); public virtual void Redirect(string location); public abstract void Redirect(string location, bool permanent); }
二、服务器适配
由于应用程序总是利用这个抽象的HttpContext上下文来获取与当前请求有关的信息,需要完成的所有响应操作也总是作用在这个HttpContext对象上,所以不同的服务器与这个抽象的HttpContext需要进行“适配”。通过《模拟管道实现》针对模拟框架的介绍可知,ASP.NET Core框架会采用一种针对特性(Feature)的适配方式。
如下图所示,ASP.NET Core框架为抽象的HttpContext定义了一系列标准的特性接口来对请求上下文的各个方面进行描述。在一系列标准的接口中,最核心的是用来描述请求的IHttpRequestFeature接口和描述响应的IHttpResponseFeature接口。我们在应用层使用的HttpContext上下文就是根据这样一组特性集合来创建的,对于某个具体的服务器来说,它需要提供这些特性接口的实现,并在接收到请求之后利用自行实现的特性来创建HttpContext上下文。
由于HttpContext上下文是利用服务器提供的特性集合创建的,所以可以统一使用抽象的HttpContext获取真实的请求信息,也能驱动服务器完成最终的响应工作。在ASP.NET Core框架中,由服务器提供的特性集合通过IFeatureCollection接口表示。《模拟管道实现》创建的模拟框架为IFeatureCollection接口提供了一个极简版的定义,实际上该接口具有更加丰富的成员定义。
public interface IFeatureCollection : IEnumerable<KeyValuePair<Type, object>> { TFeature Get<TFeature>(); void Set<TFeature>(TFeature instance); bool IsReadOnly { get; } object this[Type key] { get; set; } int Revision { get; } }
如上面的代码片段所示,一个IFeatureCollection对象本质上就是一个Key和Value类型分别为Type与Object的字典。通过调用Set方法可以将一个特性对象作为Value,以指定的类型(一般为特性接口)作为Key添加到这个字典中,并通过Get方法根据该类型获取它。除此之外,特性的注册和获取也可以利用定义的索引来完成。如果IsReadOnly属性返回True,就意味着不能注册新的特性或者修改已经注册的特性。整数类型的只读属性Revision可以视为IFeatureCollection对象的版本,不论是采用何种方式注册新的特性还是修改现有的特性,都将改变该属性的值。
具有如下定义的FeatureCollection类型是对IFeatureCollection接口的默认实现。它具有两个构造函数重载:默认无参构造函数帮助我们创建一个空的特性集合,另一个构造函数则需要指定一个IFeatureCollection对象来提供默认或者后备特性对象。对于采用第二个构造函数创建的 FeatureCollection对象来说,当我们通过指定的类型试图获取对应的特性对象时,如果没有注册到当前FeatureCollection对象上,它会从这个后备的IFeatureCollection对象中查找目标特性。
public class FeatureCollection : IFeatureCollection { //其他成员 public FeatureCollection(); public FeatureCollection(IFeatureCollection defaults); }
对于一个FeatureCollection对象来说,它的IsReadOnly属性总是返回False,所以它永远是可读可写的。对于调用默认无参构造函数创建的FeatureCollection对象来说,它的Revision属性默认返回零。如果我们通过指定另一个IFeatureCollection对象为参数调用第二个构造函数来创建一个FeatureCollection对象,前者的Revision属性值将成为后者同名属性的默认值。无论采用何种形式(调用Set方法或者索引)添加一个新的特性或者改变一个已经注册的特性,FeatureCollection对象的Revision属性都将自动递增。上述这些特性都体现在如下所示的调试断言中。
var defaults = new FeatureCollection(); Debug.Assert(defaults.Revision == 0); defaults.Set<IFoo>(new Foo()); Debug.Assert(defaults.Revision == 1); defaults[typeof(IBar)] = new Bar(); Debug.Assert(defaults.Revision == 2); FeatureCollection features = new FeatureCollection(defaults); Debug.Assert(features.Revision == 2); Debug.Assert(features.Get<IFoo>().GetType() == typeof(Foo)); features.Set<IBaz>(new Baz()); Debug.Assert(features.Revision == 3);
最初由服务器提供的IFeatureCollection对象体现在HttpContext类型的Features属性上。虽然特性最初是为了解决不同的服务器类型与统一的HttpContext上下文之间的适配设计的,但是它的作用不限于此。由于注册的特性是附加在代表当前请求的HttpContext上下文上,所以可以将任何基于当前请求的对象以特性的方式进行保存,它其实与Items属性的作用类似。
public abstract class HttpContext { public abstract IFeatureCollection Features { get; } ... }
上述这种基于特性来实现不同类型的服务器与统一请求上下文之间的适配体现在DefaultHttpContext类型上,它是对HttpContext这个抽象类型的默认实现。DefaultHttpContext具有一个如下所示的构造函数,作为参数的IFeatureCollection对象就是由服务器提供的特性集合。
public class DefaultHttpContext : HttpContext { public DefaultHttpContext(IFeatureCollection features); }
不论是组成管道的中间件还是建立在管道上的应用,在默认情况下都利用DefaultHttpContext对象来获取当前请求的相关信息,并利用这个对象完成针对请求的响应。但是DefaultHttpContext对象在这个过程中只是一个“代理”,针对它的调用(属性或者方法)最终都需要转发给由具体服务器创建的那个原始上下文,在构造函数中指定的IFeatureCollection对象所代表的特性集合成为这两个上下文对象进行沟通的唯一渠道。对于定义在DefaultHttpContext中的所有属性,它们几乎都具有一个对应的特性,这些特性都对应一个接口。
本章我们只介绍表示请求和响应的IHttpRequestFeature接口与IHttpResponseFeature接口。从下面给出的代码片段可以看出,这两个接口具有与抽象类HttpRequest和HttpResponse一致的定义。对于DefaultHttpContext类型来说,它的Request属性和Response属性返回的具体类型为DefaultHttpRequest与DefaultHttpResponse,它们分别利用这两个特性实现了定义在基类(HttpRequest和HttpResponse)的所有抽象成员。
public interface IHttpRequestFeature { Stream Body { get; set; } IHeaderDictionary Headers { get; set; } string Method { get; set; } string Path { get; set; } string PathBase { get; set; } string Protocol { get; set; } string QueryString { get; set; } string Scheme { get; set; } } public interface IHttpResponseFeature { Stream Body { get; set; } bool HasStarted { get; } IHeaderDictionary Headers { get; set; } string ReasonPhrase { get; set; } int StatusCode { get; set; } void OnCompleted(Func<object, Task> callback, object state); void OnStarting(Func<object, Task> callback, object state); }
三、获取HttpContext上下文
如果第三方组件需要获取表示当前请求上下文的HttpContext对象,就可以通过注入IHttpContextAccessor服务来实现。IHttpContextAccessor对象提供如下所示的HttpContext属性返回针对当前请求的HttpContext对象,由于该属性并不是只读的,所以当前的HttpContext也可以通过该属性进行设置。
public interface IHttpContextAccessor { HttpContext HttpContext { get; set; } }
ASP.NET Core框架提供的HttpContextAccessor类型可以作为IHttpContextAccessor接口的默认实现(真实实现稍有不同)。从如下所示的代码片段可以看出,HttpContextAccessor将提供的HttpContext对象以一个AsyncLocal<HttpContext>对象的方式存储起来,所以在整个请求处理的异步处理流程中都可以利用它得到同一个HttpContext对象。
public class HttpContextAccessor : IHttpContextAccessor { private static AsyncLocal<HttpContext> _httpContextCurrent = new AsyncLocal<HttpContext>(); public HttpContext HttpContext { get => _httpContextCurrent.Value; set => _httpContextCurrent.Value = value; } }
针对IHttpContextAccessor/HttpContextAccessor的服务注册可以通过如下所示的AddHttpContextAccessor扩展方法来完成。由于它调用的是IServiceCollection接口的TryAddSingleton<TService, TImplementation>扩展方法,所以不用担心多次调用该方法而出现服务的重复注册问题。
public static class HttpServiceCollectionExtensions { public static IServiceCollection AddHttpContextAccessor( this IServiceCollection services) { services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>(); return services; } }
四、HttpContext上下文的创建与释放
利用注入的IHttpContextAccessor服务的HttpContext属性得到当前HttpContext上下文的前提是该属性在此之前已经被赋值,在默认情况下,该属性是通过默认注册的IHttpContextFactory服务赋值的。管道在开始处理请求前对HttpContext上下文的创建,以及请求处理完成后对它的回收释放都是通过IHttpContextFactory对象完成的。IHttpContextFactory接口定义了如下两个方法:Create方法会根据提供的特性集合来创建HttpContext对象,Dispose方法则负责将提供的HttpContext对象释放。
public interface IHttpContextFactory { HttpContext Create(IFeatureCollection featureCollection); void Dispose(HttpContext httpContext); }
ASP.NET Core框架提供如下所示的DefaultHttpContextFactory类型作为对IHttpContextFactory接口的默认实现,作为默认HttpContext上下文的 DefaultHttpContext对象就是由它创建的。如下面的代码片段所示,在IHttpContextAccessor服务被注册的情况下,ASP.NET Core框架将调用第二个构造函数来创建HttpContextFactory对象。在Create方法中,它根据提供的IFeatureCollection对象创建一个DefaultHttpContext对象,在返回该对象之前,它会将该对象赋值给IHttpContextAccessor对象的HttpContext属性。
public class DefaultHttpContextFactory : IHttpContextFactory { private readonly IHttpContextAccessor _httpContextAccessor; private readonly FormOptions _formOptions; private readonly IServiceScopeFactory _serviceScopeFactory; public DefaultHttpContextFactory(IServiceProvider serviceProvider) { _httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>(); _formOptions = serviceProvider.GetRequiredService<IOptions<FormOptions>>().Value; _serviceScopeFactory = serviceProvider.GetRequiredService<IServiceScopeFactory>(); } public HttpContext Create(IFeatureCollection 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 IDefaultHttpContextContainer container) { return container.HttpContext; } return new DefaultHttpContext(featureCollection); } public void Dispose(HttpContext httpContext) { if (_httpContextAccessor != null) { _httpContextAccessor.HttpContext = null; } } }
如上面的代码片段所示,HttpContextFactory在创建出DefaultHttpContext对象并将它设置到IHttpContextAccessor对象的HttpContext属性上之后,它还会设置DefaultHttpContext对象的FormOptions属性和ServiceScopeFactory属性,前者表示针对表单的配置选项,后者是用来创建服务范围的工厂。当Dispose方法执行的时候,DefaultHttpContextFactory对象会将IHttpContextAccessor服务的HttpContext属性设置为Null。
五、针对请求的DI容器-RequestServices
ASP.NET Core框架中存在两个用于提供所需服务的依赖注入容器:一个针对应用程序,另一个针对当前请求。绑定到HttpContext上下文RequestServices属性上针对当前请求的IServiceProvider来源于通过IServiceProvidersFeature接口表示的特性。如下面的代码片段所示,IServiceProvidersFeature接口定义了唯一的属性RequestServices,可以利用它设置和获取与请求绑定的IServiceProvider对象。
public interface IServiceProvidersFeature { IServiceProvider RequestServices { get; set; } }
如下所示的RequestServicesFeature类型是对IServiceProvidersFeature接口的默认实现。如下面的代码片段所示,当我们创建一个RequestServicesFeature对象时,需要提供当前的HttpContext上下文和创建服务范围的IServiceScopeFactory工厂。RequestServicesFeature对象的RequestServices属性提供的IServiceProvider对象来源于IServiceScopeFactory对象创建的服务范围,在请求处理过程中提供的Scoped服务实例的生命周期被限定在此范围之内。
public class RequestServicesFeature : IServiceProvidersFeature, IDisposable, IAsyncDisposable { private readonly IServiceScopeFactory _scopeFactory; private IServiceProvider _requestServices; private IServiceScope _scope; private bool _requestServicesSet; private readonly HttpContext _context; public RequestServicesFeature(HttpContext context, IServiceScopeFactory scopeFactory) { _context = context; _scopeFactory = scopeFactory; } public IServiceProvider RequestServices { get { if (!_requestServicesSet && _scopeFactory != null) { _context.Response.RegisterForDisposeAsync(this); _scope = _scopeFactory.CreateScope(); _requestServices = _scope.ServiceProvider; _requestServicesSet = true; } return _requestServices; } set { _requestServices = value; _requestServicesSet = true; } } public ValueTask DisposeAsync() { switch (_scope) { case IAsyncDisposable asyncDisposable: var vt = asyncDisposable.DisposeAsync(); if (!vt.IsCompletedSuccessfully) { return Awaited(this, vt); } vt.GetAwaiter().GetResult(); break; case IDisposable disposable: disposable.Dispose(); break; } _scope = null; _requestServices = null; return default; static async ValueTask Awaited(RequestServicesFeature servicesFeature, ValueTask vt) { await vt; servicesFeature._scope = null; servicesFeature._requestServices = null; } } public void Dispose() => DisposeAsync().ConfigureAwait(false).GetAwaiter().GetResult(); }
为了在完成请求处理之后释放所有非Singleton服务实例,我们必须及时释放创建的服务范围。针对服务范围的释放实现在DisposeAsync方法中,该方法是针对IAsyncDisposable接口的实现。在服务范围被创建时,RequestServicesFeature对象会调用表示当前响应的HttpResponse对象的RegisterForDisposeAsync方法将自身添加到需要释放的对象列表中,当响应完成之后,DisposeAsync方法会自动被调用,进而将针对当前请求的服务范围联通该范围内的服务实例释放。
前面提及,除了创建返回的DefaultHttpContext对象,DefaultHttpContextFactory对象还会设置用于创建服务范围的工厂(对应如下所示的ServiceScopeFactory属性)。用来提供基于当前请求依赖注入容器的RequestServicesFeature特性正是根据IServiceScopeFactory对象创建的。
public sealed class DefaultHttpContext : HttpContext { public override IServiceProvider RequestServices {get;set} public IServiceScopeFactory ServiceScopeFactory { get; set; } }