HttpContext是ASP.NET中的核心對象,每一個請求都會創建一個對應的HttpContext對象,我們的應用程式便是通過HttpContext對象來獲取請求信息,最終生成響應,寫回到HttpContext中,完成一次請求處理。在前面幾章中也都有提到HttpContext,本章就來一起探索一 ...
HttpContext是ASP.NET中的核心對象,每一個請求都會創建一個對應的HttpContext對象,我們的應用程式便是通過HttpContext對象來獲取請求信息,最終生成響應,寫回到HttpContext中,完成一次請求處理。在前面幾章中也都有提到HttpContext,本章就來一起探索一下HttpContext的世界,揭開它的神秘面紗。
目錄
本系列文章從源碼分析的角度來探索 ASP.NET Core 的運行原理,分為以下幾個章節:
ASP.NET Core 運行原理解剖[1]:Hosting
ASP.NET Core 運行原理解剖[2]:Hosting補充之配置介紹
ASP.NET Core 運行原理解剖[3]:Middleware-請求管道的構成
ASP.NET Core 運行原理解剖[4]:進入HttpContext的世界(Current)
ASP.NET Core 運行原理解剖[5]:Authentication(待續)
IHttpContextFactory
在第一章中,我們介紹到,WebHost 在啟動 IServer 時,會傳入一個 IHttpApplication<TContext>
類型的對象,Server 負責對請求的監聽,在接收到請求時,會調用該對象的 ProcessRequestAsync
方法將請求轉交給我們的應用程式。IHttpApplication<TContext>
的預設實現為 HostingApplication
,有如下定義:
public class HostingApplication : IHttpApplication<HostingApplication.Context>
{
private readonly RequestDelegate _application;
private readonly IHttpContextFactory _httpContextFactory;
public Context CreateContext(IFeatureCollection contextFeatures)
{
var context = new Context();
var httpContext = _httpContextFactory.Create(contextFeatures);
_diagnostics.BeginRequest(httpContext, ref context);
context.HttpContext = httpContext;
return context;
}
public Task ProcessRequestAsync(Context context)
{
return _application(context.HttpContext);
}
public void DisposeContext(Context context, Exception exception)
{
var httpContext = context.HttpContext;
_diagnostics.RequestEnd(httpContext, exception, context);
_httpContextFactory.Dispose(httpContext);
_diagnostics.ContextDisposed(context);
}
}
首先使用 IHttpContextFactory
來創建 HttpContext 實例,然後在 ProcessRequestAsync
方法中調用上一章介紹的 RequestDelegate,由此進入到我們的應用程式當中。
IHttpContextFactory 負責對 HttpContext 的創建和釋放,分別對應著Create
和Dispose
方法,它的預設實現類為HttpContextFactory
,定義如下:
public class HttpContextFactory : IHttpContextFactory
{
private readonly IHttpContextAccessor _httpContextAccessor;
private readonly FormOptions _formOptions;
public HttpContext Create(IFeatureCollection featureCollection)
{
var httpContext = new DefaultHttpContext(featureCollection);
if (_httpContextAccessor != null)
{
_httpContextAccessor.HttpContext = httpContext;
}
var formFeature = new FormFeature(httpContext.Request, _formOptions);
featureCollection.Set<IFormFeature>(formFeature);
return httpContext;
}
public void Dispose(HttpContext httpContext)
{
if (_httpContextAccessor != null)
{
_httpContextAccessor.HttpContext = null;
}
}
}
如上,HttpContextFactory 只是簡單的使用 new DefaultHttpContext(featureCollection)
來創建 HttpContext 的實例,而這裡涉及到一個 IFeatureCollection
對象,它是由 Server 根據原始請求創建而來的,下麵就先介紹一下該對象。
IFeatureCollection
不過,在介紹 IFeatureCollection
之前,我們先需先回顧一下OWIN:
OWIN是 “Open Web Server Interface for .NET” 的首字母縮寫,它定義了一套Web Server和Web Application之間的標準介面,主要用於解除 ASP.NET 與 IIS 的緊密耦合。為此,OWIN 定義了四個核心組件:Host
, Server
, Middleware
, Application
,併為Server和Middleware的之間的交互提供了一個 Func<IDictionary<string,object>,Task>
類型的標準介面。
每一個OWIN中間件,都會接收到一個 IDictionary<string,object>
類型的變數,用來表示當前請求的相關信息,也稱為環境字典。每一個支持OWIN標準的 Web Server 都會根據請求的原始上下文信息,封裝成這個環境字典,然後在OWIN中間件之間傳遞,進而完成整個請求的處理。環境字典定義了一系列預先約定好的Key,比如:用 "owin.RequestBody" 來表示請求體,"owin.RequestHeaders" 來表示請求頭,"owin.RequestMethod" 來表示請求方法等。
OWIN是隨著ASP.NET MVC5進行到我們的視線中,在當時,ASP.NET WebAPI 2.0 也基於OWIN實現了自寄宿模式。再後來,提出了 ASP.NET 5 與 MVC6,完全是基於OWIN的模式來開發的,再到今天的 ASP.NET Core,OWIN的概念已被模糊化了,但是還是隨處可以見到OWIN的影子,並且也提供了對 OWIN 的擴展支持。
在 ASP.NET Core 中,提出了 IFeatureCollection
的概念,它本質上也是一個 IDictionary<string,object>
鍵值對,但是它具有面向對象的特點,相對於 IDictionary<string,object>
更加清晰,容易理解,並且Server構建成這樣一個對象也很容易,它有如下定義:
public interface IFeatureCollection : IEnumerable<KeyValuePair<Type, object>>
{
bool IsReadOnly { get; }
int Revision { get; }
object this[Type key] { get; set; }
TFeature Get<TFeature>();
void Set<TFeature>(TFeature instance);
}
它的定義非常簡單,由一系列以鍵值對來表示的標準特性對象(TFeature)組成,可以通過一個索引以及 Get
和 Set
方法來獲取或設置這些特性對象。
下麵,我們看一下在 ASP.NET Core 中的對它的一個模擬實現:
public class FeatureCollection : IFeatureCollection
{
private IDictionary<Type, object> _features;
private readonly IFeatureCollection _defaults;
private volatile int _containerRevision;
public virtual int Revision
{
get { return _containerRevision + (_defaults?.Revision ?? 0); }
}
public object this[Type key]
{
get
{
object result;
return _features != null && _features.TryGetValue(key, out result) ? result : _defaults?[key];
}
set
{
if (value == null)
{
if (_features != null && _features.Remove(key))
{
_containerRevision++;
}
return;
}
if (_features == null)
{
_features = new Dictionary<Type, object>();
}
_features[key] = value;
_containerRevision++;
}
}
public TFeature Get<TFeature>()
{
return (TFeature)this[typeof(TFeature)];
}
public void Set<TFeature>(TFeature instance)
{
this[typeof(TFeature)] = instance;
}
}
如上,它的內部屬性 _features
便是OWIN中的標準環境字典,並且提供了更加方便的泛型 Get
, Set
方法,以及一個索引器來訪問該環境字典。不過,如果只是這樣,那使用起來依然不夠方便,更為重要的是 ASP.NET Core 還提供了一系列的特性對象,並以這些特性對象的類型做為環境字典中的Key。
通過上面代碼,還可以發現,每次對該環境字典的修改,都會使 Revision
屬性遞增1。
這裡為什麼說FeatureCollection是一個模擬的實現呢?具我觀察,FeatureCollection對象只在ASP.NET Core的測試代碼中用到,而每個Server都有它自己的方式來構建
IFeatureCollection
,並不會使用FeatureCollection,關於Server中是如何創建IFeatureCollection
實例的,可以參考KestrelHttpServer中的實現,這裡就不再深究。
那特性對象又是什麼呢?我們先看一下請求特性的定義:
public interface IHttpRequestFeature
{
string Protocol { get; set; }
string Scheme { get; set; }
string Method { get; set; }
string PathBase { get; set; }
string Path { get; set; }
string QueryString { get; set; }
string RawTarget { get; set; }
IHeaderDictionary Headers { get; set; }
Stream Body { get; set; }
}
再看一下表單特性的定義:
public interface IFormFeature
{
bool HasFormContentType { get; }
IFormCollection Form { get; set; }
IFormCollection ReadForm();
Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken);
}
可以看到,這些特性對象與我們熟悉的 HttpContext 中的屬性非常相似,這也就大大簡化了在 IHttpRequestFeature
和 HttpContext
之間的轉換。我們可以通過這些特性介面定義的屬性來獲取到原始上下文中描述的信息,並通過特性對象提供的方法來操作原始上下文,它就像Web Server與我們的應用程式之間的橋梁,完成抽象和具體之間的轉換。
ASP.NET Core 提供了一系列豐富的特性對象,如 Session, Cookies, Query, Form, WebSocket, Request, Response 等等, 更詳細的列表可以查看 Microsoft.AspNetCore.Http.Features。
HttpContext
HttpContext 對象我們應該都很熟悉了,它用來表示一個抽象的HTTP上下文,而HttpContext對象的核心又體現在用於描述請求的Request和描述響應的Response屬性上。除此之外,它還包含一些與當前請求相關的其他上下文信息,如描述當前HTTP連接的ConnectionInfo對象,控制WebSocket的WebSocketManager,代表當前用戶的ClaimsPrincipal對象的Session,等等:
public abstract class HttpContext
{
public abstract IFeatureCollection Features { get; }
public abstract HttpRequest Request { get; }
public abstract HttpResponse Response { get; }
public abstract ConnectionInfo Connection { get; }
public abstract WebSocketManager WebSockets { get; }
public abstract ClaimsPrincipal User { get; set; }
public abstract IDictionary<object, object> Items { get; set; }
public abstract IServiceProvider RequestServices { get; set; }
public abstract CancellationToken RequestAborted { get; set; }
public abstract string TraceIdentifier { get; set; }
public abstract ISession Session { get; set; }
public abstract void Abort();
}
在我們處理請求時,如果希望終止該請求,可以通過 RequestAborted
屬性給請求管道發送一個終止信息。當需要對整個管道共用一些與當前上下文相關的數據,可以將它保存在 Items
字典中。而在 ASP.NET Coer 1.x 中還包含一個管理認證的AuthenticationManager對象,但是在 2.0 中,將它移到了 AuthenticationHttpContextExtensions
中,因為用戶認證本來就一個相對複雜且獨立的模塊,把它獨立出去會更加符合 ASP.NET Core 的簡潔模塊化特性。
在上文中,我們瞭解到 HttpContext 的預設實現使用的是 DefaultHttpContext
類型 ,而 DefaultHttpContext 便是對上面介紹的 IFeatureCollection
對象的封裝:
public class DefaultHttpContext : HttpContext
{
private FeatureReferences<FeatureInterfaces> _features;
private HttpRequest _request;
private HttpResponse _response;
public DefaultHttpContext(IFeatureCollection features)
{
Initialize(features);
}
public virtual void Initialize(IFeatureCollection features)
{
_features = new FeatureReferences<FeatureInterfaces>(features);
_request = InitializeHttpRequest();
_response = InitializeHttpResponse();
}
protected virtual HttpRequest InitializeHttpRequest() => new DefaultHttpRequest(this);
}
如上,DefaultHttpContext通過 Initialize
來完成從 IFeatureCollection 到 HttpContext 的轉換,而各個屬性的轉換又交給了它們自己。
HttpRequest
HttpRequest 可以用來獲取到描述當前請求的各種相關信息,比如請求的協議(HTTP或者HTTPS)、HTTP方法、地址,以及該請求的請求頭,請求體等:
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 long? ContentLength { 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 = new CancellationToken());
}
HttpRequest是一個抽象類,它的預設實現是DefaultHttpRequest:
public class DefaultHttpRequest : HttpRequest
{
private readonly static Func<IFeatureCollection, IHttpRequestFeature> _nullRequestFeature = f => null;
private FeatureReferences<FeatureInterfaces> _features;
public DefaultHttpRequest(HttpContext context)
{
Initialize(context);
}
public virtual void Initialize(HttpContext context)
{
_context = context;
_features = new FeatureReferences<FeatureInterfaces>(context.Features);
}
private IHttpRequestFeature HttpRequestFeature => _features.Fetch(ref _features.Cache.Request, _nullRequestFeature);
public override string Method
{
get { return HttpRequestFeature.Method; }
set { HttpRequestFeature.Method = value; }
}
public override Task<IFormCollection> ReadFormAsync(CancellationToken cancellationToken)
{
return FormFeature.ReadFormAsync(cancellationToken);
}
}
在 DefaultHttpRequest 中,並沒有額外的功能,它只是簡單的與 IHttpRequestFeature
中的同名屬性和方法做了一個映射,而 IHttpRequestFeature 對象的獲取又涉及到一個 FeatureReferences<FeatureInterfaces>
類型, 從字面意思來說,就是對Feature對象的一個引用,用來保存對應的Feature實例,併在上文介紹的 Revision 屬性發生變化時,清空Feature實例的緩存:
public struct FeatureReferences<TCache>
{
public IFeatureCollection Collection { get; private set; }
public int Revision { get; private set; }
public TCache Cache;
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public TFeature Fetch<TFeature, TState>(ref TFeature cached, TState state, Func<TState, TFeature> factory) where TFeature : class
{
var flush = false;
var revision = Collection.Revision;
if (Revision != revision)
{
cached = null;
flush = true;
}
return cached ?? UpdateCached(ref cached, state, factory, revision, flush);
}
private TFeature UpdateCached<TFeature, TState>(ref TFeature cached, TState state, Func<TState, TFeature> factory, int revision, bool flush) where TFeature : class
{
if (flush)
{
Cache = default(TCache);
}
cached = Collection.Get<TFeature>();
if (cached == null)
{
cached = factory(state);
Collection.Set(cached);
Revision = Collection.Revision;
}
else if (flush)
{
Revision = revision;
}
return cached;
}
public TFeature Fetch<TFeature>(ref TFeature cached, Func<IFeatureCollection, TFeature> factory)
where TFeature : class => Fetch(ref cached, Collection, factory);
}
如上,當 Revision 生成變化時,會將 Cache
設置為 null , 然後重新從 IFeatureCollection
中獲取,最後更新 Revision 為最新版本,相當於一個緩存工廠。
Fetch方法使用了
[MethodImpl(MethodImplOptions.AggressiveInlining)]
特性,表示該方法會儘可能的使用內聯方式來執行。而內聯是一種很重要的優化方式, 它允許編譯器在方法調用開銷比方法本身更大的情況下消除對方法調用的開銷,即直接將該方法體嵌入到調用者中。
HttpResponse
在瞭解了表示請求的抽象類 HttpRequest
之後,我們再來認識一下與它對應的,用來描述響應的 HttpResponse
類型:
public abstract class HttpResponse
{
private static readonly Func<object, Task> _callbackDelegate = callback => ((Func<Task>)callback)();
private static readonly Func<object, Task> _disposeDelegate = disposable =>
{
((IDisposable)disposable).Dispose();
return Task.CompletedTask;
};
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 string ContentType { 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) => OnStarting(_callbackDelegate, callback);
public abstract void OnCompleted(Func<object, Task> callback, object state);
public virtual void RegisterForDispose(IDisposable disposable) => OnCompleted(_disposeDelegate, disposable);
public virtual void OnCompleted(Func<Task> callback) => OnCompleted(_callbackDelegate, callback);
public virtual void Redirect(string location) => Redirect(location, permanent: false);
public abstract void Redirect(string location, bool permanent);
}
HttpResponse也是一個抽象類,我們使用它來輸出對請求的響應,如設置HTTP狀態碼,Cookies,HTTP響應報文頭,響應主體等,以及提供了一些將響應發送到客戶端時的相關事件。
其 HasStarted
屬性用來表示響應是否已開始發往客戶端,在我們第一次調用 response.Body.WriteAsync
方法時,該屬性便會被設置為 True
。需要註意的是,一旦 HasStarted
設置為 true
後,便不能再修改響應頭,否則將會拋出 InvalidOperationException
異常,也建議我們在HasStarted設置為true後,不要再對 Response 進行寫入,因為此時 content-length
的值已經確定,繼續寫入可能會造成協議衝突。
HttpResponse 的預設實現為 DefaultHttpResponse ,它與 DefaultHttpRequest 類似,只是對 IHttpResponseFeature
的封裝,不過 ASP.NET Core 也為我們提供了一些擴展方法,如:我們在寫入響應時,通常使用的是 Response 的擴展方法 WriteAsync
:
public static class HttpResponseWritingExtensions
{
public static Task WriteAsync(this HttpResponse response, string text, CancellationToken cancellationToken = default(CancellationToken))
{
return response.WriteAsync(text, Encoding.UTF8, cancellationToken);
}
public static Task WriteAsync(this HttpResponse response, string text, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken))
{
byte[] data = encoding.GetBytes(text);
return response.Body.WriteAsync(data, 0, data.Length, cancellationToken);
}
}
ASP.NET Core 還為 Response 提供了用來一個清空響應頭和響應體的擴展方法:
public static class ResponseExtensions
{
public static void Clear(this HttpResponse response)
{
if (response.HasStarted)
{
throw new InvalidOperationException("The response cannot be cleared, it has already started sending.");
}
response.StatusCode = 200;
response.HttpContext.Features.Get<IHttpResponseFeature>().ReasonPhrase = null;
response.Headers.Clear();
if (response.Body.CanSeek)
{
response.Body.SetLength(0);
}
}
}
還有比較常用的發送文件的擴展方法:SendFileAsync
,獲取響應頭的擴展方法:GetTypedHeaders
等等,就不再細說。
IHttpContextAccessor
在 ASP.NET 4.x 我們經常會通過 HttpContext.Current
來獲取當前請求的 HttpContext 對象,而在 ASP.NET Core 中,HttpContext 不再有 Current
屬性,並且在 ASP.NET Core 中一切皆註入,更加推薦使用註入的方式來獲取實例,而非使用靜態變數。因此,ASP.NET Core 提供了一個 IHttpContextAccessor
介面,用來統一獲取當前請求的 HttpContext 實例的方式:
public interface IHttpContextAccessor
{
HttpContext HttpContext { get; set; }
}
它的定義非常簡單,就只有一個 HttpContext 屬性,它在ASP.NET Core 中還有一個內置的實現類:HttpContextAccessor
。
public class HttpContextAccessor : IHttpContextAccessor
{
private static AsyncLocal<HttpContext> _httpContextCurrent = new AsyncLocal<HttpContext>();
public HttpContext HttpContext
{
get
{
return _httpContextCurrent.Value;
}
set
{
_httpContextCurrent.Value = value;
}
}
}
這裡使用了一個 AsyncLocal<T>
類型來保存 HttpContext 對象,可能很多人對 AsyncLocal
不太瞭解,這裡就來介紹一下:
在.NET 4.5 中引用了 async
await
等關鍵字,使我們可以像編寫同步方法一樣方便的來執行非同步操作,因此我們的大部分代碼都會使用非同步。以往我們所使用的 ThreadLocal
在同步方法中沒有問題,但是在 await
後有可能會創建新實的例(await 之後可能還交給之前的線程執行,也有可能是一個新的線程來執行),而不再適合用來保存線程內的唯一實例,因此在 .NET 4.6 中引用了 AsyncLocal<T>
類型,它類似於 ThreadLocal,但是在 await
之後就算切換線程也仍然可以保持同一實例。我們知道在 ASP.NET 4.x 中,HttpContext的 Current
實例是通過 CallContext
對象來保存的,但是 ASP.NET Core 中不再支持CallContext,故使用 AsyncLocal<T>
來保證線程內的唯一實例。
不過,ASP.NET Core 預設並沒有註入 IHttpContextAccessor
對象,如果我們想在應用程式中使用它,則需要手動來註冊:
public void ConfigureServices(IServiceCollection services)
{
ervices.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
}
在上面介紹的HttpContextFactory類的構造函數中會註入IHttpContextAccessor實例,併為其HttpContext屬性賦值,併在Dispose
方法中將其設置為null。
總結
在ASP.NET 4.x 中,我們就對 HttpContext 非常熟悉了,而在 ASP.NET Core 中,它的變化並不大,只是做了一些簡化,因此本文較為簡單,主要描述了一下 HttpContext 是如何創建的,以及它的構成,最後則介紹了一下在每個請求中獲取 HttpContext 唯一實例的方式,而在 ASP.NET Core 2.0 中 HttpContext 的 AuthenticationManager
對象已標記為過時,添加了一些擴展方法來實現AuthenticationManager中的功能,下一章就來介紹一下 ASP.NET Core 中的認證系統。