郑州网站制作郑州网站制作,微信机器人wordpress,电子商务网站建设技术解决方案,seo专业技术培训前言周五在群里面有小伙伴问#xff0c;ASP.NET Core这个HttpContextAccessor为什么改成了这个样子#xff1f;在印象中#xff0c;这已经是第三次遇到有小伙伴问这个问题了#xff0c;特意来写一篇记录#xff0c;来回答一下这个问题。聊一聊历史关于HttpContext其实我们… 前言周五在群里面有小伙伴问ASP.NET Core这个HttpContextAccessor为什么改成了这个样子在印象中这已经是第三次遇到有小伙伴问这个问题了特意来写一篇记录来回答一下这个问题。聊一聊历史关于HttpContext其实我们大家都不陌生它封装了HttpRequest和HttpResponse在处理Http请求时起着至关重要的作用。CallContext时代那么如何访问HttpContext对象呢回到await/async出现以前的ASP.NET的时代我们可以通过HttpContext.Current方法直接访问当前Http请求的HttpContext对象因为当时基本都是同步的代码一个Http请求只会在一个线程中处理所以我们可以使用能在当前线程中传播的CallContext.HostContext来保存HttpContext对象它的代码长这个样子。namespace System.Web.Hosting {using System.Web;using System.Web.Configuration;using System.Runtime.Remoting.Messaging;using System.Security.Permissions;internal class ContextBase {internal static Object Current {get {// CallContext在不同的线程中不一样return CallContext.HostContext;}[SecurityPermission(SecurityAction.Demand, Unrestricted true)]set {CallContext.HostContext value;}}......}
}}一切都很美好但是后面微软在C#为了进一步增强增强了异步IO的性能从而实现的stackless协程加入了await/async关键字感兴趣的小伙伴可以阅读黑洞的这一系列文章同一个方法内的代码await前与后不一定在同一个线程中执行那么就会造成在await之后的代码使用HttpContext.Current的时候访问不到当前的HttpContext对象下面有一段这个问题简单的复现代码。// 设置当前线程HostContext
CallContext.HostContext new Dictionarystring, string
{[ContextKey] ContextValue
};
// await前可以正常访问
Console.Write($[{Thread.CurrentThread.ManagedThreadId}] await before);
Console.WriteLine(((Dictionarystring,string)CallContext.HostContext)[ContextKey]);await Task.Delay(100);// await后切换了线程无法访问
Console.Write($[{Thread.CurrentThread.ManagedThreadId}] await after);
Console.WriteLine(((Dictionarystring,string)CallContext.HostContext)[ContextKey]);可以看到await执行之前HostContext是可以正确的输出赋值的对象和数据但是await以后的代码由于线程从16切换到29所以访问不到上面代码给HostContext设置的对象了。AsyncLocal时代为了解决这个问题微软在.NET 4.6中引入了AsyncLocalT类后面重新设计的ASP.NET Core自然就用上了AsyncLocalT来存储当前Http请求的HttpContext对象也就是开头截图的代码一样我们来尝试一下。var asyncLocal new AsyncLocalDictionarystring,string();// 设置当前线程HostContext
asyncLocal.Value new Dictionarystring, string
{[ContextKey] ContextValue
};
// await前可以正常访问
Console.Write($[{Thread.CurrentThread.ManagedThreadId}] await before);
Console.WriteLine(asyncLocal.Value[ContextKey]);await Task.Delay(100);// await后切换了线程可以访问
Console.Write($[{Thread.CurrentThread.ManagedThreadId}] await after);
Console.WriteLine(asyncLocal.Value[ContextKey]);没有任何问题线程从16切换到了17一样的可以访问。对AsyncLocal感兴趣的小伙伴可以看黑洞的这篇文章。简单的说就是AsyncLocal默认会将当前线程保存的上下对象在发生await的时候传播到后续的线程上。这看起来就非常的美好了既能开开心心的用await/async又不用担心上下文数据访问不到那为什么ASP.NET Core的后续版本需要修改HttpContextAccesor呢我们自己来实现ContextAccessor大家看下面一段代码。// 给Context赋值一下
var accessor new ContextAccessor();
accessor.Context ContextValue;
Console.WriteLine($[{Thread.CurrentThread.ManagedThreadId}] Main-1{accessor.Context});// 执行方法
await Method();// 再打印一下
Console.WriteLine($[{Thread.CurrentThread.ManagedThreadId}] Main-2{accessor.Context});async Task Method()
{// 输出Context内容Console.WriteLine($[{Thread.CurrentThread.ManagedThreadId}] Method-1{accessor.Context});await Task.Delay(100);// 注意我在这里将Context对象清空Console.WriteLine($[{Thread.CurrentThread.ManagedThreadId}] Method-2{accessor.Context});accessor.Context null;Console.WriteLine($[{Thread.CurrentThread.ManagedThreadId}] Method-3{accessor.Context});
}// 实现一个简单的Context Accessor
public class ContextAccessor
{static AsyncLocalstring _contextCurrent new AsyncLocalstring();public string Context{get _contextCurrent.Value;set _contextCurrent.Value value;}
}奇怪的事情就发生了为什么明明在Method中把Context对象置为null了Method-3中已经输出为null了为啥在Main-2输出中还是ContextValue呢AsyncLocal使用的问题其实这已经解答了上面的问题就是为什么在ASP.NET Core 6.0中的实现方式突然变了有这样一种场景已经当前线程中把HttpContext置空了但是其它线程仍然能访问HttpContext对象导致后续的行为可能不一致。那为什么会造成这个问题呢首先我们得知道AsyncLocal是如何实现的这里我就不在赘述详细可以看我前面给的链接黑洞大佬的文章。这里只简单的说一下我们只需要知道AsyncLocal底层是通过ExecutionContext实现的每次设置Value时都会用新的Context对象来覆盖原有的代码如下所示(有删减)。public sealed class AsyncLocalT : IAsyncLocal
{public T Value{[SecuritySafeCritical]get{// 从ExecutionContext中获取当前线程的值object obj ExecutionContext.GetLocalValue(this);return (obj null) ? default(T) : (T)obj;}[SecuritySafeCritical]set{// 设置值 ExecutionContext.SetLocalValue(this, value, m_valueChangedHandler ! null);}}
}......
public sealed class ExecutionContext : IDisposable, ISerializable
{internal static void SetLocalValue(IAsyncLocal local, object newValue, bool needChangeNotifications){var current Thread.CurrentThread.GetMutableExecutionContext();object previousValue null;if (previousValue newValue)return;var newValues current._localValues;// 无论是AsyncLocalValueMap.Create 还是 newValues.Set // 都会创建一个新的IAsyncLocalValueMap对象来覆盖原来的值if (newValues null){newValues AsyncLocalValueMap.Create(local, newValue, treatNullValueAsNonexistent: !needChangeNotifications);}else{newValues newValues.Set(local, newValue, treatNullValueAsNonexistent: !needChangeNotifications);}current._localValues newValues;......}
}接下来我们需要避开await/async语法糖的影响反编译一下IL代码使用C# 1.0来重新组织代码(使用ilspy或者dnspy之类都可以)。可以看到原本的语法糖已经被拆解成stackless状态机这里我们重点关注Start方法。进入Start方法内部我们可以看到以下代码源码链接。......
// Start方法
public static void StartTStateMachine(ref TStateMachine stateMachine) where TStateMachine : IAsyncStateMachine
{if (stateMachine null){ThrowHelper.ThrowArgumentNullException(ExceptionArgument.stateMachine);}Thread currentThread Thread.CurrentThread;// 备份当前线程的 executionContextExecutionContext? previousExecutionCtx currentThread._executionContext;SynchronizationContext? previousSyncCtx currentThread._synchronizationContext;try{// 执行状态机stateMachine.MoveNext();}finally{if (previousSyncCtx ! currentThread._synchronizationContext){// Restore changed SynchronizationContext back to previouscurrentThread._synchronizationContext previousSyncCtx;}ExecutionContext? currentExecutionCtx currentThread._executionContext;// 如果executionContext发生变化那么调用RestoreChangedContextToThread方法还原if (previousExecutionCtx ! currentExecutionCtx){ExecutionContext.RestoreChangedContextToThread(currentThread, previousExecutionCtx, currentExecutionCtx);}}
}
......
// 调用RestoreChangedContextToThread方法
internal static void RestoreChangedContextToThread(Thread currentThread, ExecutionContext? contextToRestore, ExecutionContext? currentContext)
{Debug.Assert(currentThread Thread.CurrentThread);Debug.Assert(contextToRestore ! currentContext);// 将改变后的ExecutionContext恢复到之前的状态currentThread._executionContext contextToRestore;......
}通过上面的代码我们就不难看出为什么会存在这样的问题了是因为状态机的Start方法会备份当前线程的ExecuteContext如果ExecuteContext在状态机内方法调用时发生了改变那么就会还原回去。又因为上文提到的AsyncLocal底层实现是ExecuteContext每次SetValue时都会生成一个新的IAsyncLocalValueMap对象覆盖当前的ExecuteContext必然修改就会被还原回去了。ASP.NET Core的解决方案在ASP.NET Core中解决这个问题的方法也很巧妙就是简单的包了一层。我们也可以简单的包一层对象。public class ContextHolder
{ public string Context {get;set;}
}public class ContextAccessor
{static AsyncLocalContextHolder _contextCurrent new AsyncLocalContextHolder();public string Context{get _contextCurrent.Value?.Context;set { var holder _contextCurrent.Value;// 拿到原来的holder 直接修改成新的value// asp.net core源码是设置为null 因为在它的逻辑中执行到了这个Set方法// 就必然是一个新的http请求需要把以前的清空if (holder ! null) holder.Context value;// 如果没有holder 那么新建else _contextCurrent.Value new ContextHolder { Context value};}}
}最终结果就和我们预期的一致了流程也如下图一样。自始至终都是修改的同一个ContextHolder对象。总结由上可见ASP.NET Core 6.0的HttpContextAccessor那样设计的原因就是为了解决AsyncLocal在await环境中会发生复制导致不能及时清除历史的HttpContext的问题。笔者水平有限如果错漏欢迎指出感谢各位的阅读作者InCerry出处https://www.cnblogs.com/InCerry/p/Why-The-Design-HttpContextAccessor.html版权本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。声明本博客版权归「InCerry」所有。