You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

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());
    }
}

必看的注意事项

  1. 数值解析一定要用InvariantCulture:避免不同区域设置下(比如欧洲用逗号当小数点)解析失败,这个坑踩一次就记一辈子!
  2. 异常兜底必须有:旧数据里可能有各种脏数据,比如拼写错误的币种代码,一定要用默认值兜底,不能让整个反序列化失败。
  3. 测试覆盖所有场景:一定要测这几种情况:
    • 新格式的XML(带Currency属性的Price节点)
    • 旧格式的XML(嵌套Amount和Currency的Price节点)
    • 旧格式缺失Currency节点的XML
    • 币种代码错误的XML

要是还有啥细节没搞懂,或者需要调整的地方,随时喊我哈~

火山引擎 最新活动