我们知道“依赖注入”已经成为了.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(); } }