如何为XmlSerializer实例的嵌套派生属性正确使用XmlInclude?
我尝试将负载序列化为XML发送给外部API,涉及多层嵌套且属性存在差异的模型。以下是最初使用的模型代码:
初始模型代码
[Serializable] [XmlRoot(ElementName = "Envelope", Namespace = "soapenv")] [XmlInclude(typeof(Derived1))] [XmlInclude(typeof(Derived2))] [XmlInclude(typeof(Derived3))] public class SoapEnvelope<T> where T : TheBase<T> { [XmlNamespaceDeclarations] public XmlSerializerNamespaces Namespaces { get; set; } = new XmlSerializerNamespaces(new[] { new XmlQualifiedName("soapenv", "http://schemas.xmlsoap.org/soap/envelope/"), new XmlQualifiedName("impl", "http://v1.blahblah.com/") }); [XmlElement("Header", Namespace = "soapenv")] public object Header { get; set; } [XmlElement("Body", Namespace = "soapenv")] public SoapBody<T> Body { get; set; } }
[Serializable] public class SoapBody<T> where T : TheBase<T> { [XmlElement("InnerEnvelope")] public TheBase<T> Envelope { get; set; } }
[XmlInclude(typeof(Derived1))] [XmlInclude(typeof(Derived2))] [XmlInclude(typeof(Derived3))] public class TheBase<T> { [XmlAttribute("Version")] public string Version = "1.0"; [XmlElement("EnvelopeContext")] public EnvelopeContext EnvelopeContext { get; set; } }
public class Derived1 : TheBase<Derived1> { [XmlNamespaceDeclarations] public XmlSerializerNamespaces Namespaces { get; set; } = new XmlSerializerNamespaces(new[] { new XmlQualifiedName("sch", "http://www.blbl.com/schemas") }); [XmlElement("EnvelopeBodyList", Namespace = "sch")] public DerivedClass1Subclass EnvelopeBodyList { get; set; } }
public class Derived2 : TheBase<Derived2> { [XmlNamespaceDeclarations] public XmlSerializerNamespaces Namespaces { get; set; } = new XmlSerializerNamespaces(new[] { new XmlQualifiedName("sch2", "http://www.blbl.com/schemas") }); [XmlElement("EnvelopeBodyList", Namespace = "sch2")] public DerivedClass2Subclass EnvelopeBodyList { get; set; } }
public class Derived3 : TheBase<Derived3> { [XmlNamespaceDeclarations] public XmlSerializerNamespaces Namespaces { get; set; } = new XmlSerializerNamespaces(new[] { new XmlQualifiedName("sch3", "http://www.blbl.com/schemas") }); [XmlElement("EnvelopeBodyList", Namespace = "sch3")] public DerivedClass3Subclass EnvelopeBodyList { get; set; } }
序列化方法
服务类通过以下方法生成XML:
public string ConvertRequestToXml<T>(T request) where T : TheBase<T> { string returnString; var envelope = new SoapEnvelope<T>(); envelope.Body = new SoapBody<T>(); envelope.Body.Envelope = request; try { var xmlSerializer = new XmlSerializer(typeof(SoapEnvelope<T>)); using (var stringWriter = new StringWriter()) { xmlSerializer.Serialize(stringWriter, envelope); returnString = stringWriter.ToString(); } } catch (Exception ex) { _logger.LogError($"{ex.Source} {ex.Message} {ex.InnerException?.Message}"); throw; } return returnString; }
报错信息
传入Derived3调用该方法时,出现错误:
"The type namespaceblah.namespaceblah.Derived3 was not expected. Use the XmlInclude or SoapInclude attribute to specify types that are not known statically."
已尝试的无效操作
- 在
SoapEnvelope和TheBase类上添加Derived1、Derived2、Derived3的XmlInclude特性,单独或同时添加 - 用
SoapInclude替换或补充XmlInclude特性 - 移除泛型标记
- 添加或移除
Serializable特性 - 为
Derived3的所有属性类添加Serializable特性
替代实现(仍报错)
根据建议修改后,代码如下,但仍收到相同错误:
[Serializable] [XmlRoot(ElementName = "Envelope", Namespace = "soapenv")] [XmlInclude(typeof(Derived1))] [XmlInclude(typeof(Derived2))] [XmlInclude(typeof(Derived3))] public class SoapEnvelope { [XmlNamespaceDeclarations] public XmlSerializerNamespaces Namespaces { get; set; } = new XmlSerializerNamespaces(new[] { new XmlQualifiedName("soapenv", "http://schemas.xmlsoap.org/soap/envelope/"), new XmlQualifiedName("impl", "http://blhablhab.com/") }); [XmlElement("Header", Namespace = "soapenv", IsNullable = true)] public object Header { get; set; } [XmlElement("Body", Namespace = "soapenv")] public SoapBody Body { get; set; } }
[Serializable] public class SoapBody { [XmlElement("InnerEnvelope")] public TheBase Envelope { get; set; } }
[Serializable] [XmlInclude(typeof(Derived1))] [XmlInclude(typeof(Derived2))] [XmlInclude(typeof(Derived3))] public abstract class TheBase{ }
[Serializable] public class Derived1: TheBase { [XmlNamespaceDeclarations] public XmlSerializerNamespaces Namespaces { get; set; } = new XmlSerializerNamespaces(new[] { new XmlQualifiedName("sch", "http://www.vendorcompany.com/schemas") }); [XmlAttribute("Version")] public string Version = "1.0"; [XmlElement("EnvelopeContext")] public EnvelopeContext EnvelopeContext { get; set; } [XmlElement("EnvelopeBodyList", Namespace = "sch")] public EnvelopeBodyList EnvelopeBodyList { get; set; } }
[Serializable] public class Derived2 : TheBase { [XmlNamespaceDeclarations] public XmlSerializerNamespaces Namespaces { get; set; } = new XmlSerializerNamespaces(new[] { new XmlQualifiedName("sch2", "http://www.vendorcompany.com/schemas") }); [XmlAttribute("Version")] public string Version = "1.0"; [XmlElement("EnvelopeContext")] public EnvelopeContext EnvelopeContext { get; set; } [XmlElement("EnvelopeBodyList", Namespace = "sch2")] public EnvelopeBodyList2 EnvelopeBodyList { get; set; } }
[Serializable] public class Derived3 : TheBase { [XmlNamespaceDeclarations] public XmlSerializerNamespaces Namespaces { get; set; } = new XmlSerializerNamespaces(new[] { new XmlQualifiedName("sch3", "http://www.vendorcompany.com/schemas") }); [XmlAttribute("Version")] public string Version = "1.0"; [XmlElement("EnvelopeContext")] public EnvelopeContext EnvelopeContext { get; set; } [XmlElement("EnvelopeBodyList", Namespace = "sch3")] public EnvelopeBodyList3 EnvelopeBodyList { get; set; } }
public string ConvertRequestToXml(TheBase request) { string returnString; XmlSerializer xmlSerializer = new XmlSerializer(typeof(SoapEnvelope)); var envelope = new SoapEnvelope(); envelope.Body = new SoapBody(); envelope.Body.Envelope = request; try { using (var stringWriter = new StringWriter()) { xmlSerializer.Serialize(stringWriter, envelope); returnString = stringWriter.ToString(); } } catch (Exception ex) { _logger.LogError($"{ex.Source} {ex.Message} {ex.InnerException?.Message}"); throw; } return returnString; }
报错信息仍为:
"The type {namespace}.{namespace}.{namespace}.Derived3 was not expected. Use the XmlInclude or SoapInclude attribute to specify types that are not known statically."
关键问题分析
问题核心在于XmlSerializer的多态类型识别逻辑:当序列化包含基类属性的对象时,序列化器需要明确知道所有可能的派生类型。即使基类TheBase添加了XmlInclude,如果持有基类属性的容器类(比如SoapBody)没有被序列化器正确关联到这些派生类型,或者初始化序列化器时未显式声明,就会出现类型识别失败。
具体修复步骤
方案1:初始化序列化器时显式指定派生类型
在创建XmlSerializer时,通过构造函数的第二个参数传入所有TheBase的派生类型,强制序列化器识别这些类型:
public string ConvertRequestToXml(TheBase request) { string returnString; // 显式传入所有需要序列化的派生类型 Type[] extraTypes = new Type[] { typeof(Derived1), typeof(Derived2), typeof(Derived3) }; XmlSerializer xmlSerializer = new XmlSerializer(typeof(SoapEnvelope), extraTypes); var envelope = new SoapEnvelope(); envelope.Body = new SoapBody(); envelope.Body.Envelope = request; try { using (var stringWriter = new StringWriter()) { xmlSerializer.Serialize(stringWriter, envelope); returnString = stringWriter.ToString(); } } catch (Exception ex) { _logger.LogError($"{ex.Source} {ex.Message} {ex.InnerException?.Message}"); throw; } return returnString; }
方案2:在SoapBody类上添加XmlInclude特性
因为SoapBody直接持有TheBase类型的属性,在SoapBody上添加派生类的XmlInclude,确保序列化器解析SoapBody时能识别所有派生类型:
[Serializable] [XmlInclude(typeof(Derived1))] [XmlInclude(typeof(Derived2))] [XmlInclude(typeof(Derived3))] public class SoapBody { [XmlElement("InnerEnvelope")] public TheBase Envelope { get; set; } }
方案3:清理冗余特性并检查类型引用
- 移除所有不必要的
Serializable特性:XmlSerializer不依赖该特性,仅关注Xml*系列特性,冗余标记可能导致混淆。 - 确认
XmlInclude中的类型引用正确:确保typeof(Derived3)指向的是实际的派生类,若派生类在不同命名空间下,需使用完全限定类型名。
额外优化建议
- 将命名空间声明统一移到最外层的
SoapEnvelope中:一个XML文档只能有一个命名空间声明集合,多个派生类中的XmlNamespaceDeclarations会导致序列化冲突。 - 优先使用非泛型模型:泛基类的多态序列化更容易出现类型识别问题,非泛型的替代实现更稳定可靠。
内容的提问来源于stack exchange,提问作者stevestevesteve

