採用「傳統」方式獲取當前HttpContext

我們知道「依賴注入」已經成為了.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();
    }
}