採用「傳統」方式獲取當前HttpContext
- 2020 年 11 月 17 日
- 筆記
- .NET Core, Asp.Net Core, HttpContext, IHttpContextAccessor
我們知道「依賴注入」已經成為了.NET Core的基本編程模式,表示當前請求上下文的HttpContext可以通過注入的IHttpContextAccessor服務來提取。有時候我們會使用一些由於某些原因無法使用依賴注入的組件,我們如何提取當前HttpContext呢?
要回答這個問題,就得先來了解表示當前HTTP請求上下文的HttpContext對象被存儲在什麼地方?既然我們可以利用注入的IHttpContextAccessor服務來得到當前HttpContext,針對HttpContext的獲取邏輯自然就體現在該介面的實現類型HttpContextAccessor上。於是反編譯(也可以直接從github上獲取源程式碼)該類型,得到它的源程式碼。
public class HttpContextAccessor : IHttpContextAccessor { // Fields private static AsyncLocal<HttpContextHolder> _httpContextCurrent = new AsyncLocal<HttpContextHolder>(); // Properties public HttpContext HttpContext { get { HttpContextHolder local1 = _httpContextCurrent.Value; if (local1 != null) { return local1.Context; } HttpContextHolder local2 = local1; return null; } set { HttpContextHolder holder = _httpContextCurrent.Value; if (holder != null) { holder.Context = null; } if (value != null) { HttpContextHolder holder1 = new HttpContextHolder(); holder1.Context = value; _httpContextCurrent.set_Value(holder1); } } } // Nested Types private class HttpContextHolder { // Fields public HttpContext Context; } }
上程式碼片段可以看出,當前HttpContext被存儲在靜態欄位表示的一個AsyncLocal<HttpContextHolder> 對象上(HttpContext被HttpContextHolder對象進一步封裝),這也是為何ASP.NET Core處理請求非同步調用鏈(通過await關鍵字)總是可以獲取當前HttpContext的原因所在。但是這裡涉及到的HttpContextHolder是一個內嵌私有類型,所以我們只有通過反射的方式來獲取它封裝的HttpContext對象。但是我們又不原因承受反射帶來的性能代價,那個表達式樹自然成為了我們的首選解決方案。
public static class HttpContextUtility { private static Func<object> _asyncLocalAccessor; private static Func<object, object> _holderAccessor; private static Func<object, HttpContext> _httpContextAccessor; public static HttpContext GetCurrentHttpContext() { var asyncLocal = (_asyncLocalAccessor ??= CreateAsyncLocalAccessor())(); if (asyncLocal == null) { return null; } var holder = (_holderAccessor ??= CreateHolderAccessor(asyncLocal))(asyncLocal); if (holder == null) { return null; } return (_httpContextAccessor ??= CreateHttpContextAccessor(holder))(holder); static Func<object> CreateAsyncLocalAccessor() { var fieldInfo = typeof(HttpContextAccessor).GetField("_httpContextCurrent", BindingFlags.Static | BindingFlags.NonPublic); var field = Expression.Field(null, fieldInfo); return Expression.Lambda<Func<object>>(field).Compile(); } static Func<object, object> CreateHolderAccessor(object asyncLocal) { var holderType = asyncLocal.GetType().GetGenericArguments()[0]; var method = typeof(AsyncLocal<>).MakeGenericType(holderType).GetProperty("Value").GetGetMethod(); var target = Expression.Parameter(typeof(object)); var convert = Expression.Convert(target, asyncLocal.GetType()); var getValue = Expression.Call(convert, method); return Expression.Lambda<Func<object, object>>(getValue, target).Compile(); } static Func<object, HttpContext> CreateHttpContextAccessor(object holder) { var target = Expression.Parameter(typeof(object)); var convert = Expression.Convert(target, holder.GetType()); var field = Expression.Field(convert, "Context"); var convertAsResult = Expression.Convert(field, typeof(HttpContext)); return Expression.Lambda<Func<object, HttpContext>>(convertAsResult, target).Compile(); } } }
上面的程式碼體現了採用表達式樹實現的針對當前HttpContext的獲取邏輯。具體來說,靜態方法GetCurrentHttpContext利用表達式創建的Func<object>對象得到HttpContextAccessor靜態欄位_httpContextAccessor存儲的AsyncLocal<HttpContextHolder>,然後再利用表達式創建的Func<object, object>得到該對象Value屬性表示的HttpContextHolder對象。我們最終獲得的HttpContext是通過由表達式創建的另一個Func<object,object>從HttpContextHolder對象中提取出來的。GetCurrentHttpContext針對當前HttpContext的提取可以通過如下的程式來驗證。
public class Program { public static void Main(string[] args) { Host.CreateDefaultBuilder(args).ConfigureWebHostDefaults(web => web .ConfigureServices(svcs => svcs.AddHttpContextAccessor()) .Configure(app => app.Run(httpContext => { var httpContextAccessor = httpContext.RequestServices.GetRequiredService<IHttpContextAccessor>(); Debug.Assert(ReferenceEquals(httpContext, HttpContextUtility.GetCurrentHttpContext())); Debug.Assert(ReferenceEquals(httpContextAccessor.HttpContext, HttpContextUtility.GetCurrentHttpContext())); return httpContext.Response.WriteAsync("Hello world."); }))) .Build() .Run(); } }