XmlSerializer反序列化向后兼容问题:从自定义Money类切换为NodaMoney类型后的兼容处理
XmlSerializer反序列化向后兼容问题:从自定义Money类切换为NodaMoney类型后的兼容处理
兄弟我太懂这种坑了!换了第三方库结果旧数据炸锅的情况我遇过好多次,给你捋捋最靠谱的几种解决方案,都是我实际项目里用过的,稳得一批~
核心思路就是:绕开XmlSerializer的默认逻辑,手动接管Price节点的解析,把两种格式的XML都转换成NodaMoney的Money对象,同时给缺失Currency的旧数据补默认值。
方案一:辅助属性+自定义解析(最推荐,代码侵入性低)
这种方式不用改太多原有代码,只需要给Product类加个辅助属性,专门处理Price节点的序列化/反序列化,原有的Price属性标记为XmlIgnore就行,其他属性的默认序列化逻辑完全不受影响。
代码实现
using System.Xml; using System.Xml.Linq; using System.Globalization; using NodaMoney; public class Product { // 原有的NodaMoney属性,标记为XmlIgnore,不让XmlSerializer直接处理 [XmlIgnore] public Money Price { get; set; } // 辅助属性,专门对接XmlSerializer的<Price>节点 [XmlElement("Price")] public XElement PriceXml { get { // 序列化时输出NodaMoney要求的新格式 var priceElement = new XElement("Price", Price.Amount); priceElement.Add(new XAttribute("Currency", Price.Currency.Code)); return priceElement; } set { // 反序列化时同时兼容新旧两种格式,还处理Currency缺失的情况 Money parsedMoney; var defaultCurrencyCode = "USD"; // 这里改成你业务的默认币种 // 先尝试解析新格式:带Currency属性的<Price>节点 var currencyAttr = value.Attribute("Currency"); if (currencyAttr != null && !string.IsNullOrWhiteSpace(value.Value)) { var amount = decimal.Parse(value.Value, CultureInfo.InvariantCulture); try { var currency = Currency.FromCode(currencyAttr.Value); parsedMoney = new Money(amount, currency); } catch (UnknownCurrencyException) { // 遇到未知币种时用默认值兜底 parsedMoney = new Money(amount, Currency.FromCode(defaultCurrencyCode)); } } else { // 解析旧格式:嵌套<Amount>和<Currency>子节点的情况 decimal amount = 0; string currencyCode = defaultCurrencyCode; // 读取旧格式的子节点 var amountElement = value.Element("Amount"); if (amountElement != null && !string.IsNullOrWhiteSpace(amountElement.Value)) { amount = decimal.Parse(amountElement.Value, CultureInfo.InvariantCulture); } var currencyElement = value.Element("Currency"); if (currencyElement != null && !string.IsNullOrWhiteSpace(currencyElement.Value)) { currencyCode = currencyElement.Value; } // 兜底处理未知币种 try { var currency = Currency.FromCode(currencyCode); parsedMoney = new Money(amount, currency); } catch (UnknownCurrencyException) { parsedMoney = new Money(amount, Currency.FromCode(defaultCurrencyCode)); } } Price = parsedMoney; } } // 这里可以保留Product类的其他属性,比如Name、Id之类的,XmlSerializer会正常处理 public string Name { get; set; } public int Id { get; set; } }
为什么选这个方案?
- 代码侵入性极低,只加了一个辅助属性,原有逻辑完全不动
- 逻辑清晰,两种格式的解析分开处理,出问题好排查
- 自动兼容新数据的序列化(输出的是NodaMoney的标准格式)
- 各种异常情况都有兜底,比如未知币种、缺失Amount的情况
方案二:实现IXmlSerializable接口(适合需要全局控制的场景)
如果你的Product类属性不多,或者需要更精细地控制整个序列化流程,可以让Product直接实现IXmlSerializable接口,手动读写所有XML节点。这种方式的缺点是要自己处理所有属性的序列化,属性多的话容易漏。
代码实现
using System.Xml; using System.Globalization; using NodaMoney; public class Product : IXmlSerializable { public Money Price { get; set; } public string Name { get; set; } public int Id { get; set; } // IXmlSerializable接口要求的方法,直接返回null就行 public XmlSchema GetSchema() => null; public void ReadXml(XmlReader reader) { // 跳过<Product>起始标签 reader.ReadStartElement(); while (reader.NodeType != XmlNodeType.EndElement) { switch (reader.Name) { case "Price": ParsePriceNode(reader); break; case "Name": Name = reader.ReadElementContentAsString(); break; case "Id": Id = reader.ReadElementContentAsInt(); break; default: // 跳过未知节点,避免解析失败 reader.Skip(); break; } } // 跳过<Product>结束标签 reader.ReadEndElement(); } private void ParsePriceNode(XmlReader reader) { var defaultCurrencyCode = "USD"; reader.ReadStartElement("Price"); // 检查是否有Currency属性(新格式) string currencyCode = reader.GetAttribute("Currency"); if (!string.IsNullOrWhiteSpace(currencyCode)) { // 新格式:读取文本内容作为Amount decimal amount = decimal.Parse(reader.ReadContentAsString(), CultureInfo.InvariantCulture); try { Price = new Money(amount, Currency.FromCode(currencyCode)); } catch (UnknownCurrencyException) { Price = new Money(amount, Currency.FromCode(defaultCurrencyCode)); } } else { // 旧格式:读取嵌套的<Amount>和<Currency>子节点 decimal amount = 0; string parsedCurrency = defaultCurrencyCode; while (reader.NodeType != XmlNodeType.EndElement) { if (reader.Name == "Amount") { amount = decimal.Parse(reader.ReadElementContentAsString(), CultureInfo.InvariantCulture); } else if (reader.Name == "Currency") { parsedCurrency = reader.ReadElementContentAsString(); } else { reader.Skip(); } } try { Price = new Money(amount, Currency.FromCode(parsedCurrency)); } catch (UnknownCurrencyException) { Price = new Money(amount, Currency.FromCode(defaultCurrencyCode)); } } // 跳过</Price>结束标签 reader.ReadEndElement(); } public void WriteXml(XmlWriter writer) { // 序列化时输出新格式的<Price>节点 writer.WriteStartElement("Price"); writer.WriteAttributeString("Currency", Price.Currency.Code); writer.WriteValue(Price.Amount); writer.WriteEndElement(); // 手动序列化其他属性 writer.WriteElementString("Name", Name); writer.WriteElementString("Id", Id.ToString()); } }
必看的注意事项
- 数值解析一定要用InvariantCulture:避免不同区域设置下(比如欧洲用逗号当小数点)解析失败,这个坑踩一次就记一辈子!
- 异常兜底必须有:旧数据里可能有各种脏数据,比如拼写错误的币种代码,一定要用默认值兜底,不能让整个反序列化失败。
- 测试覆盖所有场景:一定要测这几种情况:
- 新格式的XML(带Currency属性的Price节点)
- 旧格式的XML(嵌套Amount和Currency的Price节点)
- 旧格式缺失Currency节点的XML
- 币种代码错误的XML
要是还有啥细节没搞懂,或者需要调整的地方,随时喊我哈~




