采用“传统”方式获取当前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();
    }
}