天天看點

ASP.NET Core管道詳解[2]: HttpContext本質論一、HttpContext二、伺服器适配三、擷取HttpContext上下文四、HttpContext上下文的建立與釋放五、針對請求的DI容器-RequestServices

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上下文。

ASP.NET Core管道詳解[2]: HttpContext本質論一、HttpContext二、伺服器适配三、擷取HttpContext上下文四、HttpContext上下文的建立與釋放五、針對請求的DI容器-RequestServices

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

複制