如何用MessagePack序列化反序列化字典列表并仅保留Results字段类型信息
问题场景
使用MessagePack传输SQL查询结果,定义了如下响应消息类:
public class ExecuteDatabaseQueryResponse : IResponseMessage { [Key(0)] public Guid RequestId { get; set; } [Key(1)] public required List<Dictionary<string, object?>>? Results { get; set; } }
功能可正常运行,但接收端的decimal、DateTime、Guid类型值会被序列化为字符串,且无法自动还原类型。尝试给Results字段添加[MessagePackFormatter(typeof(TypelessFormatter))]或[MessagePackFormatter(typeof(DynamicObjectTypeFallbackFormatter))]特性未生效,现需仅为Results字段保留类型信息。
解决方案
1. 自定义字典值类型格式化器(推荐)
核心思路是针对Dictionary<string, object?>实现自定义格式化器,在序列化时为每个值附带类型标识,反序列化时根据标识还原原始类型,精准控制仅Results字段的类型保留。
步骤1:实现自定义格式化器
using MessagePack; using System; using System.Collections.Generic; public class TypedObjectDictionaryFormatter : IMessagePackFormatter<Dictionary<string, object?>> { public void Serialize(ref MessagePackWriter writer, Dictionary<string, object?> value, MessagePackSerializerOptions options) { writer.WriteMapHeader(value.Count); foreach (var kvp in value) { writer.Write(kvp.Key); if (kvp.Value == null) { writer.WriteNil(); continue; } // 写入类型全名作为标识 var valueType = kvp.Value.GetType(); writer.Write(valueType.FullName); // 序列化实际值 MessagePackSerializer.Serialize(ref writer, kvp.Value, valueType, options); } } public Dictionary<string, object?> Deserialize(ref MessagePackReader reader, MessagePackSerializerOptions options) { var mapCount = reader.ReadMapHeader(); var resultDict = new Dictionary<string, object?>(mapCount); for (int i = 0; i < mapCount; i++) { var key = reader.ReadString(); if (reader.TryReadNil()) { resultDict[key] = null; continue; } // 读取类型标识并获取对应Type var typeName = reader.ReadString(); var targetType = Type.GetType(typeName); if (targetType == null) { // 类型无法识别时的降级处理 resultDict[key] = reader.ReadString(); continue; } // 反序列化回原始类型 var value = MessagePackSerializer.Deserialize(ref reader, targetType, options); resultDict[key] = value; } return resultDict; } }
步骤2:为Results字段绑定格式化器
修改响应消息类,给Results字段指定自定义格式化器:
public class ExecuteDatabaseQueryResponse : IResponseMessage { [Key(0)] public Guid RequestId { get; set; } [Key(1)] [MessagePackFormatter(typeof(TypedObjectDictionaryFormatter))] public required List<Dictionary<string, object?>>? Results { get; set; } }
2. 局部启用类型保留配置
若不想编写自定义格式化器,可针对Results字段单独使用带类型保留的序列化选项。例如在序列化/反序列化时,为Results字段单独配置TypelessSerializerOptions:
// 序列化时 var response = new ExecuteDatabaseQueryResponse { RequestId = Guid.NewGuid(), Results = queryResults }; // 全局配置(不保留类型)用于RequestId var globalOptions = MessagePackSerializerOptions.Standard; // 局部配置(保留类型)用于Results var typelessOptions = TypelessSerializerOptions.Standard; // 手动序列化Response,分别处理不同字段 using (var writer = new MessagePackWriter(new MemoryStream())) { writer.WriteMapHeader(2); // 序列化RequestId(用全局配置) writer.Write(0); MessagePackSerializer.Serialize(ref writer, response.RequestId, globalOptions); // 序列化Results(用类型保留配置) writer.Write(1); MessagePackSerializer.Serialize(ref writer, response.Results, typelessOptions); writer.Flush(); var bytes = ((MemoryStream)writer.BaseStream).ToArray(); // 发送bytes... } // 反序列化时同理,分别处理字段 using (var reader = new MessagePackReader(receivedBytes)) { reader.ReadMapHeader(); var response = new ExecuteDatabaseQueryResponse(); // 读取RequestId reader.ReadInt32(); // 对应Key(0) response.RequestId = MessagePackSerializer.Deserialize<Guid>(ref reader, globalOptions); // 读取Results reader.ReadInt32(); // 对应Key(1) response.Results = MessagePackSerializer.Deserialize<List<Dictionary<string, object?>>>(ref reader, typelessOptions); }
为什么之前的特性未生效
TypelessFormatter和DynamicObjectTypeFallbackFormatter是针对整个字段类型的格式化器,而你的Results字段类型是List<Dictionary<string, object?>>,这些格式化器无法深入处理字典内部的object?值类型,因此无法保留嵌套的decimal、DateTime等类型信息。自定义格式化器则直接处理字典中每个值的类型序列化,因此能解决问题。
内容的提问来源于stack exchange,提问作者Marko

