context.Wait()不保留父类属性致节点链循环异常技术问询
解决Bot Framework中循环节点链的序列化与流程跳转问题
我之前也碰到过一模一样的循环节点链序列化坑,结合Bot Framework的状态序列化机制,给你两个可行的解决方案:
核心问题分析
你的问题根源在于循环引用的序列化冲突:
- 虽然给
Node加了[JsonObject(IsReference = true)],但Bot Framework的状态序列化逻辑会主动截断循环引用,导致OnCompleteNode这类属性被清空; - 重写基类虚拟属性后,破坏了序列化的契约,导致
context.Wait无法正确绑定后续节点的回调,直接回到RootDialog; - 直接持有节点对象引用的循环链,在序列化时会被框架判定为"不安全"结构,从而丢失关联关系。
方案一:用节点ID替代对象引用(推荐,最稳定)
放弃直接持有OnCompleteNode对象,改用节点ID来管理跳转关系,从根源上避免循环序列化问题。
1. 修改Node基类,改用ID管理跳转
[Serializable] [JsonObject(IsReference = true)] public class Node : INode { public virtual int ID { get; set; } public virtual string Name { get; set; } public virtual bool WaitForInput { get; set; } public virtual int OnCompleteID { get; set; } // 用ID替代直接节点引用 // 静态节点池:存储所有节点,也可以存在Dialog状态或外部存储中 public static Dictionary<int, Node> NodePool = new Dictionary<int, Node>(); public virtual async Task OnExecute(IDialogContext context, Activity activity) { // 通过ID从节点池获取下一个节点 if (NodePool.TryGetValue(OnCompleteID, out Node nextNode)) { if (WaitForInput) context.Wait(nextNode.OnExecuteAwait); else await nextNode.OnExecute(context, activity); } } public virtual async Task OnExecuteAwait(IDialogContext context, IAwaitable<object> result) { Activity activity = await result as Activity; await OnExecute(context, activity); } }
2. 简化MessageNode子类(无需重写基类属性)
public class MessageNode : Node { public virtual string Message { get; set; } public virtual List<Attachment> Attachments { get; set; } public virtual string AttachmentLayoutType { get; set; } public override async Task OnExecute(IDialogContext context, Activity activity) { IMessageActivity reply = context.MakeMessage(); reply.Text = Message; if (Attachments != null) { reply.Attachments = Attachments; reply.AttachmentLayout = AttachmentLayoutType; } await context.PostAsync(reply); await base.OnExecute(context, activity); } }
3. 在RootDialog中构建循环链并填充节点池
public Task StartAsync(IDialogContext context) { // 初始化节点池(如果是多会话场景,建议把节点池存在会话状态里) Node.NodePool.Clear(); // 创建节点并设置ID跳转关系 var messageNode = new Schema.MessageNode { Message = "test", ID = 0, OnCompleteID = 1, // 跳转到ID=1的节点 WaitForInput = true }; Node.NodePool.Add(messageNode.ID, messageNode); var preMN = new Schema.MessageNode { Message = "and then", ID = 1, OnCompleteID = 2, // 跳转到ID=2的节点 WaitForInput = true }; Node.NodePool.Add(preMN.ID, preMN); var preMN1 = new Schema.MessageNode { Message = "and then one more", ID = 2, OnCompleteID = 0, // 跳回ID=0的节点,形成循环 WaitForInput = true }; Node.NodePool.Add(preMN1.ID, preMN1); // 只需要把当前节点ID存入状态,无需序列化整个链 context.UserData.SetValue("CurrentNodeID", messageNode.ID); context.Wait(messageNode.OnExecuteAwait); return Task.CompletedTask; }
4. 可选:从状态中恢复节点(持久化场景)
如果需要跨会话保留节点状态,不要序列化整个链,只存当前节点ID,每次从节点池获取:
protected override async Task MessageReceivedAsync(IDialogContext context, IAwaitable<IMessageActivity> result) { if (context.UserData.TryGetValue("CurrentNodeID", out int currentNodeId) && Node.NodePool.TryGetValue(currentNodeId, out Node currentNode)) { await currentNode.OnExecuteAwait(context, result); } }
方案二:强制开启循环引用序列化(不推荐,仅临时场景用)
如果一定要保留对象引用,可以调整Bot的全局Json序列化配置,强制保留循环引用,但可能导致状态体积过大,且部分Bot版本仍可能出现截断问题:
1. 在Bot初始化时配置Json序列化
var config = new HttpConfiguration(); // 允许序列化循环引用 config.Formatters.JsonFormatter.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Serialize; // 保留对象引用标识 config.Formatters.JsonFormatter.SerializerSettings.PreserveReferencesHandling = PreserveReferencesHandling.Objects;
2. 确保Node类属性不被重写隐藏
不要在MessageNode中重写基类的虚拟属性,保持继承关系的完整性,让序列化框架能正确识别属性契约。
内容的提问来源于stack exchange,提问作者Manmax75
相关产品推荐
相关产品推荐

