为SerializableDictionary实现IXmlSerialization时遇序列化异常求助
问题背景
在为SerializableDictionary<TKey, TValue>类实现IXmlSerializable接口时,将对象序列化到Telerik.Windows.Zip创建的压缩包条目流(Entry Stream)时,抛出System.NotSupportedException,错误信息为Can't flush final block twice。异常发生在第二个序列化器对同一个XmlWriter调用Serialize方法时。
异常详情:
System.NotSupportedException HResult=0x80131515 Message=Can't flush final block twice Source=Telerik.Windows.Zip StackTrace: at Telerik.Windows.Zip.OperationStream.FlushFinalBlock() at System.Xml.XmlUtf8RawTextWriter.Flush() at System.Xml.XmlWellFormedWriter.Flush() at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o, XmlSerializerNamespaces namespaces, String encodingStyle, String id) at System.Xml.Serialization.XmlSerializer.Serialize(XmlWriter xmlWriter, Object o) at MP.SharedToolbox.XmlInOut.SerializableDictionary`2.WriteXml(XmlWriter writer) in C:\Users\britikov\Documents\Projects\SharedToolbox\XmlInOut\SerializableDictionary.cs:line 69 at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterRegulationItem.Write19_LoadCard(String n, String ns, LoadCard o, Boolean isNullable, Boolean needType) at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterRegulationItem.Write20_RegulationItem(String n, String ns, RegulationItem o, Boolean isNullable, Boolean needType) at Microsoft.Xml.Serialization.GeneratedAssembly.XmlSerializationWriterRegulationItem.Write21_Regulation(Object o)
无内部异常。
已定位问题:直接序列化到Zip Entry Stream时触发异常,目前通过先序列化到MemoryStream再复制到Entry Stream的方式临时解决,但不清楚两者差异导致问题的原因,寻求正确实现方式。
原因分析
Telerik的OperationStream(Zip条目流的底层实现)在调用FlushFinalBlock()后会标记流为已完成,不允许再次调用该方法。而XmlSerializer.Serialize()在完成序列化时会自动调用XmlWriter.Flush(),后者最终会触发OperationStream.FlushFinalBlock()。当SerializableDictionary.WriteXml()方法内部多次调用XmlSerializer.Serialize()时,第一次序列化会触发FlushFinalBlock(),第二次调用时流已经处于“最终块已刷新”的状态,因此抛出异常。
而MemoryStream没有这种“最终块只能刷新一次”的限制,它的Flush()是无操作(NOP)或者仅确保数据写入内存,不会锁定流状态,因此多次调用不会报错。
正确实现方式
方案1:避免在WriteXml中多次触发XmlWriter.Flush
修改SerializableDictionary<TKey, TValue>.WriteXml方法,禁止XmlSerializer.Serialize()自动刷新流。通过创建XmlWriter时配置XmlWriterSettings.CloseOutput = false,仅在所有序列化操作完成后统一调用一次Flush()。
示例代码:
public void WriteXml(XmlWriter writer) { var settings = new XmlWriterSettings { CloseOutput = false, OmitXmlDeclaration = writer.Settings.OmitXmlDeclaration, Encoding = writer.Settings.Encoding }; using (var subWriter = XmlWriter.Create(writer, settings)) { foreach (var kvp in this) { var keySerializer = new XmlSerializer(typeof(TKey)); var valueSerializer = new XmlSerializer(typeof(TValue)); subWriter.WriteStartElement("Item"); subWriter.WriteStartElement("Key"); keySerializer.Serialize(subWriter, kvp.Key); subWriter.WriteEndElement(); subWriter.WriteStartElement("Value"); valueSerializer.Serialize(subWriter, kvp.Value); subWriter.WriteEndElement(); subWriter.WriteEndElement(); } subWriter.Flush(); } }
方案2:使用Telerik Zip流的正确写入模式
确保在获取Zip条目流时使用正确的打开模式,在所有写入操作完成后再关闭流,避免在写入过程中触发流的最终刷新。
示例代码:
using (var zipStream = new FileStream("output.zip", FileMode.Create)) using (var zipArchive = new ZipArchive(zipStream, ZipArchiveMode.Create)) { var entry = zipArchive.CreateEntry("data.xml"); using (var entryStream = entry.Open()) { var serializer = new XmlSerializer(typeof(Regulation)); serializer.Serialize(entryStream, regulationObject); } }
方案3:优化MemoryStream中转(复杂场景推荐)
如果序列化逻辑涉及多层嵌套或多次调用,MemoryStream中转的方式简单可靠。可以通过ArrayPool<byte>复用内存块,减少GC压力。
示例代码:
public void WriteXml(XmlWriter writer) { foreach (var kvp in this) { using (var ms = new MemoryStream()) { var keySerializer = new XmlSerializer(typeof(TKey)); keySerializer.Serialize(ms, kvp.Key); ms.Position = 0; writer.WriteNode(XmlReader.Create(ms), true); ms.SetLength(0); var valueSerializer = new XmlSerializer(typeof(TValue)); valueSerializer.Serialize(ms, kvp.Value); ms.Position = 0; writer.WriteNode(XmlReader.Create(ms), true); } } }
总结
问题核心是Telerik Zip流的FlushFinalBlock()只能调用一次,而多次XmlSerializer.Serialize()会重复触发该操作。方案1、2可直接解决流限制问题,方案3是兼容性更好的折中方案,适合复杂序列化场景。
内容的提问来源于stack exchange,提问作者Snail

