使用XmlSerializer序列化Protobuf消息时触发InvalidOperationException异常
问题描述
我定义了如下Protobuf消息:
message ClientMessage{ oneof data{ CheckAlive alive = 1; Login login = 2; SendMessage sendMessage = 3; Logout logout = 4; } }
当尝试使用XmlSerializer序列化ClientMessage时,触发了如下异常:
System.InvalidOperationException: 'To be XML serializable, types which inherit from IEnumerable must have an implementation of Add(System.Object) at all levels of their inheritance hierarchy. Google.Protobuf.ByteString does not implement Add(System.Object).'
序列化所用代码:
using (var stream = MemoryUtils.MemoryStreamManager.GetStream()) using (var xml = new XmlTextWriter(stream, Encoding.UTF8)) { var xs = new XmlSerializer(typeof(T)); xs.Serialize(xml, item); using (var reader = new StreamReader(stream, Encoding.UTF8, true)) { stream.Seek(0, SeekOrigin.Begin); return reader.ReadToEnd(); } }
问题原因
这个异常的核心是**Google.Protobuf.ByteString与XmlSerializer的兼容性冲突**:
ByteString实现了IEnumerable<byte>接口XmlSerializer对所有继承自IEnumerable的类型有强制要求:必须在整个继承层级上实现Add(System.Object)方法(用于反序列化时动态添加元素)- 但
ByteString是不可变的字节序列类型,并没有提供Add方法,因此序列化时直接抛出了错误。
解决方案
这里给你三种可行的解决思路,按推荐程度排序:
1. 用Protobuf官方JSON序列化中转(最简单)
Protobuf自带了JsonFormatter可以把Protobuf对象序列化成JSON,你可以把JSON字符串封装到XML中,完全绕开XmlSerializer处理Protobuf特殊类型的麻烦:
// 第一步:将Protobuf对象转为JSON字符串 var jsonContent = JsonFormatter.Default.Format(yourClientMessageInstance); // 第二步:构建XML结构(可按需调整节点名称和层级) var xmlDoc = new XmlDocument(); var rootElement = xmlDoc.CreateElement("ClientMessage"); var jsonNode = xmlDoc.CreateElement("ProtobufContent"); jsonNode.InnerText = jsonContent; rootElement.AppendChild(jsonNode); xmlDoc.AppendChild(rootElement); // 第三步:输出最终XML字符串 using (var stringWriter = new StringWriter()) using (var xmlWriter = XmlWriter.Create(stringWriter)) { xmlDoc.WriteTo(xmlWriter); xmlWriter.Flush(); string finalXml = stringWriter.ToString(); // 这里就是你需要的XML结果 }
2. 定义DTO类映射后序列化(最灵活,适合自定义XML结构)
手动创建和Protobuf消息结构一致的普通C# DTO(数据传输对象),把Protobuf对象的数据映射到DTO中,再用XmlSerializer序列化DTO——这样彻底避开Protobuf的特殊类型:
// 定义对应ClientMessage的DTO public class ClientMessageDto { public CheckAliveDto? Alive { get; set; } public LoginDto? Login { get; set; } public SendMessageDto? SendMessage { get; set; } public LogoutDto? Logout { get; set; } } // 嵌套类型DTO示例(根据你的Protobuf实际字段补充) public class CheckAliveDto { public long Timestamp { get; set; } } public class LoginDto { public string Username { get; set; } = string.Empty; public string PasswordBase64 { get; set; } = string.Empty; // 将ByteString转成Base64字符串 } // 映射Protobuf对象到DTO(手动映射或用AutoMapper简化) var dto = new ClientMessageDto(); switch (yourClientMessageInstance.DataCase) { case ClientMessage.DataCase.Alive: dto.Alive = new CheckAliveDto { Timestamp = yourClientMessageInstance.Alive.Timestamp }; break; case ClientMessage.DataCase.Login: dto.Login = new LoginDto { Username = yourClientMessageInstance.Login.Username, PasswordBase64 = yourClientMessageInstance.Login.Password.ToBase64() }; break; // 其他oneof分支同理处理 } // 序列化DTO(和你原来的代码几乎一致,仅类型替换为DTO) using (var stream = MemoryUtils.MemoryStreamManager.GetStream()) using (var xml = new XmlTextWriter(stream, Encoding.UTF8)) { var xs = new XmlSerializer(typeof(ClientMessageDto)); xs.Serialize(xml, dto); stream.Seek(0, SeekOrigin.Begin); using (var reader = new StreamReader(stream, Encoding.UTF8, true)) { return reader.ReadToEnd(); } }
3. 为ByteString自定义XML序列化逻辑(繁琐但适合特殊场景)
如果必须直接序列化Protobuf对象,可以为ByteString实现IXmlSerializable接口,指定其以Base64字符串形式序列化:
// 自定义ByteString的XML序列化代理 public class ByteStringXmlSerializer : IXmlSerializable { public ByteString Value { get; set; } = ByteString.Empty; public XmlSchema? GetSchema() => null; public void ReadXml(XmlReader reader) { string base64 = reader.ReadElementContentAsString(); Value = ByteString.FromBase64(base64); } public void WriteXml(XmlWriter writer) { writer.WriteString(Value.ToBase64()); } // 隐式转换,方便在Protobuf对象和代理类间切换 public static implicit operator ByteString(ByteStringXmlSerializer serializer) => serializer.Value; public static implicit operator ByteStringXmlSerializer(ByteString value) => new() { Value = value }; }
之后需要通过XmlAttributeOverrides指定Protobuf生成类中ByteString字段的序列化方式,或者修改Protobuf生成代码替换类型——这种方法需要处理大量细节,仅在必须直接序列化Protobuf对象时使用。
内容的提问来源于stack exchange,提问作者julzzzz

