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

1

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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM