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; }
}

ASP.NET Core管道详解[2]: HttpContext本质论的更多相关文章

  1. ASP.NET Core管道详解[3]: Pipeline = IServer + IHttpApplication

    ASP.NET Core的请求处理管道由一个服务器和一组中间件构成,但对于面向传输层的服务器来说,它其实没有中间件的概念.当服务器接收到请求之后,会将该请求分发给一个处理器进行处理,对服务器而言,这个 ...

  2. ASP.NET Core管道详解[6]: ASP.NET Core应用是如何启动的?[下篇]

    要承载一个ASP.NET Core应用,只需要将GenericWebHostService服务注册到承载系统中即可.但GenericWebHostService服务具有针对其他一系列服务的依赖,所以在 ...

  3. ASP.NET Core 中间件详解及项目实战

    前言 在上篇文章主要介绍了DotNetCore项目状况,本篇文章是我们在开发自己的项目中实际使用的,比较贴合实际应用,算是对中间件的一个深入使用了,不是简单的Hello World,如果你觉得本篇文章 ...

  4. [转]ASP.NET Core 中间件详解及项目实战

    本文转自:http://www.cnblogs.com/savorboard/p/5586229.html 前言 在上篇文章主要介绍了DotNetCore项目状况,本篇文章是我们在开发自己的项目中实际 ...

  5. 【转载】ASP.NET Core 中间件详解及项目实战

    前言 在上篇文章主要介绍了DotNetCore项目状况,本篇文章是我们在开发自己的项目中实际使用的,比较贴合实际应用,算是对中间件的一个深入使用了,不是简单的Hello World,如果你觉得本篇文章 ...

  6. 第十节:Asp.Net Core 配置详解和选项模式

    一. 各种文件的读取 1.说明 在.Net Core中,各种配置文件的读取都需要依赖[Microsoft.Extensions.Configuration]程序集,当然在Asp.Net Core中已经 ...

  7. ASP.NET Core真实管道详解[2]:Server是如何完成针对请求的监听、接收与响应的【上】

    Server是ASP .NET Core管道的第一个节点,负责完整请求的监听和接收,最终对请求的响应同样也由它完成.Server是我们对所有实现了IServer接口的所有类型以及对应对象的统称,如下面 ...

  8. ASP.NET Core真实管道详解[1]:中间件是个什么东西?

    ASP.NET Core管道虽然在结构组成上显得非常简单,但是在具体实现上却涉及到太多的对象,所以我们在 <ASP.NET Core管道深度剖析[共4篇]> 中围绕着一个经过极度简化的模拟 ...

  9. ASP.NET Core真实管道详解[1]

    ASP.NET Core管道虽然在结构组成上显得非常简单,但是在具体实现上却涉及到太多的对象,所以我们在 <ASP.NET Core管道深度剖析[共4篇]> 中围绕着一个经过极度简化的模拟 ...

随机推荐

  1. java安全编码指南之:ThreadPool的使用

    目录 简介 java自带的线程池 提交给线程池的线程要是可以被中断的 正确处理线程池中线程的异常 线程池中使用ThreadLocal一定要注意清理 简介 在java中,除了单个使用Thread之外,我 ...

  2. IntelliJ IDEA 2020.2 x64 激活 2020-09-18亲测有效

    idea 激活,查阅许多资源和文章,激活码都失效,无意发现该资源(https://macwk.com/article/jetbrains-crack),亲测有效(2020-09-18),在此记录,以备 ...

  3. springcloud中使用dubbo开发rpc服务及调用

    spring cloud中基于springboot开发的微服务,是基于http的rest接口,也可以开发基于dubbo的rpc接口. 一,创建goodsService模块 1, 在创建的goodsSe ...

  4. jmeter 使用总结

    安装和启动(mac) 下载jmeter安装包并解压,进入其bin目录,执行./jmeter.sh或sh jmeter,打开就会出现如下画面 Thread Group 新建线程组,如下图 需要设置的选项 ...

  5. MongoDB 监控 --- MongoDB基础用法(八)

    MongoDB 监控 在你已经安装部署并允许MongoDB服务后,你必须要了解MongoDB的运行情况,并查看MongoDB的性能.这样在大流量得情况下可以很好的应对并保证MongoDB正常运作. M ...

  6. 如何安装一个高可用K3s集群?

    作者介绍 Janakiram MSV是Janakiram & Associates的首席分析师,也是国际信息技术学院的兼职教师.他也是Google Qualified Developer.亚马 ...

  7. Git系列:常用命令

    一.背景 作为一名程序员,怎么能不懂Git那些常用命令呢?于是花费一点时间来总结Git命令.关于安装的话,就不讲了. 二.常用命令 1.配置全局的用户名称和用户邮箱 git config --glob ...

  8. net core webapi多版本控制与swagger(nswag)配置

    前言 首先希望webapi 支持多版本,swagger针对不同的版本可进行交互.多版本控制基于Microsoft.AspNetCore.Mvc.Versioning.ApiExplorer 包,swa ...

  9. 【Kata Daily 190920】Square(n) Sum(平方加总)

    题目: Complete the square sum function so that it squares each number passed into it and then sums the ...

  10. 使用sql导出数据_mysql

    在mysql中 使用sql 脚本导出数据的方式之一: select * from table_name where x=y  INFO OUTFILE "/tmp/table_name.tx ...