为什么HttpContextAccessor要这么设计?

前言

周五在群里面有小伙伴问,ASP.NET Core这个HttpContextAccessor为什么改成了这个样子?
809e912bc97191dd358cc6592e6f3c07.png
在印象中,这已经是第三次遇到有小伙伴问这个问题了,特意来写一篇记录,来回答一下这个问题。

聊一聊历史

关于HttpContext其实我们大家都不陌生,它封装了HttpRequestHttpResponse,在处理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 Dictionary<string, string> 
{["ContextKey"] = "ContextValue"
};
// await前,可以正常访问
Console.Write($"[{Thread.CurrentThread.ManagedThreadId}] await before:");
Console.WriteLine(((Dictionary<string,string>)CallContext.HostContext)["ContextKey"]);await Task.Delay(100);// await后,切换了线程,无法访问
Console.Write($"[{Thread.CurrentThread.ManagedThreadId}] await after:");
Console.WriteLine(((Dictionary<string,string>)CallContext.HostContext)["ContextKey"]);

e969f3a2c6a7b7d40413871f758a500e.png
可以看到await执行之前HostContext是可以正确的输出赋值的对象和数据,但是await以后的代码由于线程从16切换到29,所以访问不到上面代码给HostContext设置的对象了。
1b3fc47c3c556aad3e1cd25a523f36bc.png

AsyncLocal时代

为了解决这个问题,微软在.NET 4.6中引入了AsyncLocal<T>类,后面重新设计的ASP.NET Core自然就用上了AsyncLocal<T>来存储当前Http请求的HttpContext对象,也就是开头截图的代码一样,我们来尝试一下。

var asyncLocal = new AsyncLocal<Dictionary<string,string>>();// 设置当前线程HostContext
asyncLocal.Value = new Dictionary<string, 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"]);

11608812381de185752704b5e9ff08df.png
没有任何问题,线程从16切换到了17,一样的可以访问。对AsyncLocal感兴趣的小伙伴可以看黑洞的这篇文章。简单的说就是AsyncLocal默认会将当前线程保存的上下对象在发生await的时候传播到后续的线程上。
3af85503550502e0a669041fd984877e.png
这看起来就非常的美好了,既能开开心心的用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 AsyncLocal<string> _contextCurrent = new AsyncLocal<string>();public string Context{get => _contextCurrent.Value;set => _contextCurrent.Value = value;}
}

b57c18b2c049d5cb58c34f6d8d70de47.png

奇怪的事情就发生了,为什么明明在Method中把Context对象置为null了,Method-3中已经输出为null了,为啥在Main-2输出中还是ContextValue呢?
295246c4151347788bf96d4558d93de8.png

AsyncLocal使用的问题

其实这已经解答了上面的问题,就是为什么在ASP.NET Core 6.0中的实现方式突然变了,有这样一种场景,已经当前线程中把HttpContext置空了,但是其它线程仍然能访问HttpContext对象,导致后续的行为可能不一致。

那为什么会造成这个问题呢?首先我们得知道AsyncLocal是如何实现的,这里我就不在赘述,详细可以看我前面给的链接(黑洞大佬的文章)。这里只简单的说一下,我们只需要知道AsyncLocal底层是通过ExecutionContext实现的,每次设置Value时都会用新的Context对象来覆盖原有的,代码如下所示(有删减)。

public sealed class AsyncLocal<T> : 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之类都可以)。
557f49f051ed4e83f2782f48375fbbdd.png
可以看到原本的语法糖已经被拆解成stackless状态机,这里我们重点关注Start方法。进入Start方法内部,我们可以看到以下代码,源码链接。

......
// Start方法
public static void Start<TStateMachine>(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,必然修改就会被还原回去了。

1dc674c9e491cc972aff040dd9c69dc9.png

ASP.NET Core的解决方案

在ASP.NET Core中,解决这个问题的方法也很巧妙,就是简单的包了一层。我们也可以简单的包一层对象。

public class ContextHolder
{ public string Context {get;set;}
}public class ContextAccessor
{static AsyncLocal<ContextHolder> _contextCurrent = new AsyncLocal<ContextHolder>();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};}}
}

07c36bc9f0be0687ca6a9bb111e410af.png

最终结果就和我们预期的一致了,流程也如下图一样。自始至终都是修改的同一个ContextHolder对象。
c3741040529109a7ff44a73ccafac869.png

总结

由上可见,ASP.NET Core 6.0的HttpContextAccessor那样设计的原因就是为了解决AsyncLocal在await环境中会发生复制,导致不能及时清除历史的HttpContext的问题。
笔者水平有限,如果错漏,欢迎指出,感谢各位的阅读!

作者:InCerry

出处:https://www.cnblogs.com/InCerry/p/Why-The-Design-HttpContextAccessor.html

版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。

声明:本博客版权归「InCerry」所有。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/288370.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

元素周期表排列的规律_中考化学:金属活动性顺序表和元素周期表规律总结

在我们初三学年的化学学习中&#xff0c;有两大重要规律需要同学们牢牢记住&#xff0c;这也是贯穿我们化学始终的化学规律&#xff0c;那就是金属活动性顺序表和化学元素周期表规律。一、金属活动性顺序表:金属活动性顺序由强至弱: K Ca na Mg Al Zn Fe Sn Pb(H)Cu Hg Ag Pt A…

solidity modifier函数修改器 智能合约开发知识浅学(三)

环境说明&#xff1a; Ide&#xff1a;在线remix Solidity IDE 语言&#xff1a;solidity solidity 版本号&#xff1a;0.4.20 Tip&#xff1a;如果一点都不懂的建议从头开始看 运行结果截图我不赘述&#xff0c;所有合约代码均通过个人检测。请按照标准进行操作&#xff0c;如…

百度网盘超级会员,年卡低至198元!百度官方直充,会员实时生效!

大家都喜欢用百度网盘来存储文件、照片&#xff0c;还用百度网盘分享文档&#xff0c;但没有会员的&#xff0c;容量就太小&#xff0c;传输速度也受限&#xff0c;还是咬牙充个会员吧&#xff01;幻海优品是一家正规的会员充值平台&#xff0c;价格很实惠&#xff01;百度网盘…

C#服务器编程:WebService、Ajax与回调函数(一)

目 录 1、结果展示 2、WebService 3、回调函数 本实例演示借助WebService、Ajax技术和回调函数,从MSSQL数据库中获取所需数据,并用JavaScript语言将数据结果显示到网页地图上。 1、结果展示 2、WebService (1)在工具箱的Ajax Extentions下面找到ScriptManager控件,拖…

SyntaxHighlighter行号显示错误问题解决方案

SyntaxHighlighter是根据代码中的换行符分配行号的。但是&#xff0c;如果一行代码或者注释比较长&#xff0c;在页面显示时需要分成多行显示&#xff0c;会出现行号对不上的问题&#xff0c;像这样&#xff1a; 通过设置CSS强制不换行&#xff0c;可以保证行号显示正常&#x…

mysql 一对多 关联一条最新的数据_不得不会的mysql锁

6. 多表之间的关系如图&#xff0c;实际业务数据库中的表之间都是有关系的&#xff0c;我们接下来主要要学习的就是如何分析表关系及建立表关系。分类表create table category( cid varchar(32) primary key, cname varchar(100) );商品表create table product( pid varchar(3…

C语言实现万年历记事本,简单实用的layui日历标注记事本代码

一款简单实用的layui日历标注记事本代码&#xff0c;响应式自适应电脑、平板跟手机移动端&#xff0c;可以在日历上设置每日事项标注记录&#xff0c;支持撤销、添加、修改标注记录。查看演示下载资源&#xff1a;52次 下载资源下载积分&#xff1a;20积分js代码 layui.use([la…

围棋经典棋谱_秀秀老师:茶艺师也要学好围棋

“引清风&#xff0c;邀明月&#xff0c;去来兮。省多少闲是闲非。临山近水&#xff0c;近些松竹向些梅。书院茶香几多般&#xff0c;诗酒琴棋。无萦无烦恼&#xff0c;无别离。于中国文人雅士而言&#xff0c;茶与棋&#xff0c;皆是清雅之物事。曹臣《舌花录》中&#xff0c;…

基于 Dapr 和 .NET 开发云原生应用(奉上视频+资料)

点击蓝字/关注我序言&#xff1a;今年是.NET20周年&#xff0c;为了传播.NET和营造.NET技术氛围举办了此次云原生开发挑战赛&#xff0c;请来众多业界大咖来给大家分享技术&#xff0c;为大家参赛做预热&#xff0c;参赛的朋友都可获得51Aspx 500积分和微软亲签证书&#xff0c…

【Spring-AOP-学习笔记-3】@Before前向增强处理简单示例

项目结构程序代码HelloImpl.javaWorldImpl.java定义切面类package org.crazyit.app.aspect;import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;// 定义一个切面Aspectpublic class AuthAspect{ // 匹配org.crazyit.app.service.impl包下所…

sklearn 线性回归_使用sklearn库做线性回归拟合

背景资料随着海拔高度的上升&#xff0c;温度越来越低&#xff0c;经过气象专家的研究&#xff0c;在一定的海拔高度范围内&#xff0c;高度和温度呈线性关系。现有一组实测资料&#xff0c;我们需要对这些数据进行处理拟合&#xff0c;获得此线性关系。解决思路采用sklearn库中…

VS2022之DebuggerVisualizer

在Debug程序时&#xff0c;面对一些大集合&#xff0c;之前是这样查看的&#xff0c;如下图&#xff0c;这样看起来不直观&#xff0c;集合中的数据只能一个一个实体查看&#xff1a;VS2022预览版带来一个新功能&#xff0c;集合表格可视化&#xff0c;比如下面这样一段代码&am…

SmartIDE支持开源国产IDE - 阿里蚂蚁的OpenSumi丨IDCF

作者&#xff1a;徐磊文章首发地址&#xff1a;https://smartide.cn/zh/blog/2022-0419-sprint16/SmartIDE v0.1.16 (Build 3137)已经在2022年4月19日发布到稳定版通道&#xff0c;我们在这个版本中增加了阿里和蚂蚁发布的国产IDE OpenSumi的支持&#xff0c;以及其他一些改进。…

js操作文件

在HTML表单中&#xff0c;可以上传文件的唯一控件就是<input type"file">。 注意&#xff1a;当一个表单中包含<input type"file">时&#xff0c;表单的enctype必须指定 为multipart/form-data,method必须指定为post&#xff0c;浏览器才能正确…

mycat 双主 热切换

为什么80%的码农都做不了架构师&#xff1f;>>> Mycat-server-1.6-RELEASE-20161028204710-linux.tar.gz schema.xml <?xml version"1.0"?> <!DOCTYPE mycat:schema SYSTEM "schema.dtd"> <mycat:schema xmlns:mycat"ht…

6 四大组件之Service

6-1 Servie概述 组件篇——Service 定义:  1.后台运行,不可见,没有界面  2.优先级高于Activity Service是Android系统的后台服务组件&#xff0c;适用于开发无界面、长时间运行的应用功能。 Service特点如下&#xff1a; 没有用户界面 不会轻易被Android系统终止 在系统…

WCF服务寄宿IIS时.SVC文件无法浏览的解决办法

在IIS中托管服务和经典的ASMX Web服务托管相似,需要在IIS下创建虚拟目录,并提供一个.svc文件和Web.config配置文件。另外,托管时,服务的基地址必须与.svc文件的地址相同。 一、操作步骤: (1)打开IIS,新建网站,选择物理路径 (2)打开IIS,在默认网站下添加应用程序,…

C#语法糖系列 —— 第一篇:聊聊 params 参数底层玩法

首先说说为什么要写这个系列&#xff0c;大概有两点原因。这种文章阅读量确实高...对 IL 和 汇编代码 的学习巩固所以就决定写一下这个系列&#xff0c;如果大家能从中有所收获&#xff0c;那就更好啦&#xff01;一&#xff1a;params 应用层玩法 首先上一段 测试代码。class …

nginx 修改配置文件使之支持pathinfo,且隐藏index.php

声明环境&#xff1a; nginx centos6.8 使用lnmp一键包搭建环境&#xff08;2019年2月19日 &#xff09;以前使用过别的办法去修改配置文件&#xff0c;但是过于繁琐&#xff0c;最近发现新版本中&#xff0c;在nginx 的 conf目录下发现了文件“enable-php-pathinfo.conf”&am…

关于在Windows下AndroidStudio.使用React-Native开发android报错红屏“run react-native start”解决

以下是报错&#xff0c;不过他已经给了解决办法&#xff0c;报错提示的大概中文译为“无法加载脚本&#xff0c;请确保你的Metro服务以及那个包正确”&#xff0c;由于我个人并不是专业安卓&#xff0c;公司项目没办法就上了&#xff0c;所以我就不关包了&#xff0c;包肯定是正…